1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-16 09:42:53 +08:00

Compare commits

...

266 Commits

140 changed files with 2865 additions and 973 deletions
+1 -1
View File
@@ -16,7 +16,7 @@
<EmbeddedResource Include="Resources\**\*.*" />
</ItemGroup>
<ItemGroup Label="Code Analysis">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.0" PrivateAssets="All" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.0.0" PrivateAssets="All" />
</ItemGroup>
+36 -37
View File
@@ -6,35 +6,36 @@ GEM
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
aws-eventstream (1.1.0)
aws-partitions (1.329.0)
aws-sdk-core (3.99.2)
aws-partitions (1.354.0)
aws-sdk-core (3.104.3)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.34.1)
aws-sdk-kms (1.36.0)
aws-sdk-core (~> 3, >= 3.99.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.68.1)
aws-sdk-core (~> 3, >= 3.99.0)
aws-sdk-s3 (1.78.0)
aws-sdk-core (~> 3, >= 3.104.3)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.4)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-sigv4 (1.2.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.3)
claide (1.0.3)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
declarative (0.0.10)
declarative (0.0.20)
declarative-option (0.1.0)
digest-crc (0.5.1)
digest-crc (0.6.1)
rake (~> 13.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.5)
emoji_regex (1.0.1)
excon (0.74.0)
dotenv (2.7.6)
emoji_regex (3.0.0)
excon (0.76.0)
faraday (1.0.1)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6)
@@ -42,34 +43,32 @@ GEM
http-cookie (~> 1.0.0)
faraday_middleware (1.0.0)
faraday (~> 1.0)
fastimage (2.1.7)
fastlane (2.149.1)
fastimage (2.2.0)
fastlane (2.156.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.2, < 2.0.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 2.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (>= 0.17, < 2.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (>= 0.13.1, < 2.0)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.37.0, < 0.39.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
json (< 3.0.0)
jwt (~> 2.1.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multi_xml (~> 0.5)
multipart-post (~> 2.0.0)
plist (>= 3.1.0, < 4.0.0)
public_suffix (~> 2.0.0)
rubyzip (>= 1.3.0, < 2.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
@@ -97,17 +96,17 @@ GEM
google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.3.2)
google-cloud-env (1.3.3)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.0.1)
google-cloud-storage (1.26.2)
google-cloud-storage (1.27.0)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
googleauth (0.12.0)
googleauth (0.13.1)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
@@ -119,29 +118,29 @@ GEM
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.4.0)
json (2.3.0)
jwt (2.1.0)
json (2.3.1)
jwt (2.2.1)
memoist (0.16.2)
mini_magick (4.10.1)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
multi_json (1.14.1)
multi_xml (0.6.0)
multi_json (1.15.0)
multipart-post (2.0.0)
nanaimo (0.2.6)
nanaimo (0.3.0)
naturally (2.2.0)
nokogiri (1.10.7)
nokogiri (1.10.10)
mini_portile2 (~> 2.4.0)
os (1.1.0)
os (1.1.1)
plist (3.5.0)
public_suffix (2.0.5)
public_suffix (4.0.5)
rake (13.0.1)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rouge (2.0.7)
rubyzip (1.3.0)
rubyzip (2.3.0)
security (0.1.3)
signet (0.14.0)
addressable (~> 2.3)
@@ -160,7 +159,7 @@ GEM
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
tty-cursor (0.7.1)
tty-screen (0.8.0)
tty-screen (0.8.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
@@ -169,12 +168,12 @@ GEM
unf_ext (0.0.7.7)
unicode-display_width (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.16.0)
xcodeproj (1.18.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.2.6)
nanaimo (~> 0.3.0)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.0)
+3 -1
View File
@@ -9,7 +9,9 @@
[![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu)
[![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)
Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename *osu!lazer*. Pew pew.
A free-to-win rhythm game. Rhythm is just a *click* away!
The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename *osu!lazer*. Pew pew.
## Status
+2 -2
View File
@@ -113,7 +113,7 @@ platform :ios do
souyuz(
platform: "ios",
plist_path: "../osu.iOS/Info.plist"
plist_path: "osu.iOS/Info.plist"
)
end
@@ -127,7 +127,7 @@ platform :ios do
end
lane :update_version do |options|
options[:plist_path] = '../osu.iOS/Info.plist'
options[:plist_path] = 'osu.iOS/Info.plist'
app_version(options)
end
+2 -2
View File
@@ -51,7 +51,7 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.731.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.730.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.812.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.819.0" />
</ItemGroup>
</Project>
+1 -1
View File
@@ -3,7 +3,7 @@
<TargetFramework>netcoreapp3.1</TargetFramework>
<OutputType>WinExe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Description>click the circles. to the beat.</Description>
<Description>A free-to-win rhythm game. Rhythm is just a *click* away!</Description>
<AssemblyName>osu!</AssemblyName>
<Title>osu!lazer</Title>
<Product>osu!lazer</Product>
+1 -2
View File
@@ -9,8 +9,7 @@
<projectUrl>https://osu.ppy.sh/</projectUrl>
<iconUrl>https://puu.sh/tYyXZ/9a01a5d1b0.ico</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>click the circles. to the beat.</description>
<summary>click the circles.</summary>
<description>A free-to-win rhythm game. Rhythm is just a *click* away!</description>
<releaseNotes>testing</releaseNotes>
<copyright>Copyright (c) 2020 ppy Pty Ltd</copyright>
<language>en-AU</language>
@@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
@@ -0,0 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public class TestSceneManiaModInvert : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[Test]
public void TestInversion() => CreateModTest(new ModTestData
{
Mod = new ManiaModInvert(),
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2
});
}
}
@@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
@@ -15,7 +16,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components
AccentColour.Value = colours.Yellow;
Background.Alpha = 0.5f;
Foreground.Alpha = 0;
}
protected override Drawable CreateForeground() => base.CreateForeground().With(d => d.Alpha = 0);
}
}
@@ -7,7 +7,7 @@ namespace osu.Game.Rulesets.Mania.Judgements
{
public class HoldNoteTickJudgement : ManiaJudgement
{
protected override int NumericResultFor(HitResult result) => 20;
protected override int NumericResultFor(HitResult result) => result == MaxResult ? 20 : 0;
protected override double HealthIncreaseFor(HitResult result)
{
+1
View File
@@ -220,6 +220,7 @@ namespace osu.Game.Rulesets.Mania
new ManiaModDualStages(),
new ManiaModMirror(),
new ManiaModDifficultyAdjust(),
new ManiaModInvert(),
};
case ModType.Automation:
@@ -0,0 +1,81 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModInvert : Mod, IApplicableAfterBeatmapConversion
{
public override string Name => "Invert";
public override string Acronym => "IN";
public override double ScoreMultiplier => 1;
public override string Description => "Hold the keys. To the beat.";
public override IconUsage? Icon => FontAwesome.Solid.YinYang;
public override ModType Type => ModType.Conversion;
public void ApplyToBeatmap(IBeatmap beatmap)
{
var maniaBeatmap = (ManiaBeatmap)beatmap;
var newObjects = new List<ManiaHitObject>();
foreach (var column in maniaBeatmap.HitObjects.GroupBy(h => h.Column))
{
var newColumnObjects = new List<ManiaHitObject>();
var locations = column.OfType<Note>().Select(n => (startTime: n.StartTime, samples: n.Samples))
.Concat(column.OfType<HoldNote>().SelectMany(h => new[]
{
(startTime: h.StartTime, samples: h.GetNodeSamples(0)),
(startTime: h.EndTime, samples: h.GetNodeSamples(1))
}))
.OrderBy(h => h.startTime).ToList();
for (int i = 0; i < locations.Count - 1; i++)
{
// Full duration of the hold note.
double duration = locations[i + 1].startTime - locations[i].startTime;
// Beat length at the end of the hold note.
double beatLength = beatmap.ControlPointInfo.TimingPointAt(locations[i + 1].startTime).BeatLength;
// Decrease the duration by at most a 1/4 beat to ensure there's no instantaneous notes.
duration = Math.Max(duration / 2, duration - beatLength / 4);
newColumnObjects.Add(new HoldNote
{
Column = column.Key,
StartTime = locations[i].startTime,
Duration = duration,
Samples = locations[i].samples,
NodeSamples = new List<IList<HitSampleInfo>>
{
locations[i].samples,
locations[i + 1].samples
}
});
}
newObjects.AddRange(newColumnObjects);
}
maniaBeatmap.HitObjects = newObjects.OrderBy(h => h.StartTime).ToList();
// No breaks
maniaBeatmap.Breaks.Clear();
}
}
}
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
private readonly Container<DrawableHoldNoteTail> tailContainer;
private readonly Container<DrawableHoldNoteTick> tickContainer;
private readonly Drawable bodyPiece;
private readonly SkinnableDrawable bodyPiece;
/// <summary>
/// Time at which the user started holding this hold note. Null if the user is not holding this hold note.
@@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
RelativeSizeAxes = Axes.X;
AddRangeInternal(new[]
AddRangeInternal(new Drawable[]
{
bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece
{
@@ -135,6 +135,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
// Samples are played by the head/tail notes.
}
public override void OnKilled()
{
base.OnKilled();
(bodyPiece.Drawable as IHoldNoteBody)?.Recycle();
}
protected override void Update()
{
base.Update();
@@ -19,24 +19,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
/// <summary>
/// Represents length-wise portion of a hold note.
/// </summary>
public class DefaultBodyPiece : CompositeDrawable
public class DefaultBodyPiece : CompositeDrawable, IHoldNoteBody
{
protected readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
private readonly LayoutValue subtractionCache = new LayoutValue(Invalidation.DrawSize);
private readonly IBindable<bool> isHitting = new Bindable<bool>();
protected readonly IBindable<bool> IsHitting = new Bindable<bool>();
protected Drawable Background { get; private set; }
protected BufferedContainer Foreground { get; private set; }
private BufferedContainer subtractionContainer;
private Container subtractionLayer;
private Container foregroundContainer;
public DefaultBodyPiece()
{
Blending = BlendingParameters.Additive;
AddLayout(subtractionCache);
}
[BackgroundDependencyLoader(true)]
@@ -45,7 +38,54 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
InternalChildren = new[]
{
Background = new Box { RelativeSizeAxes = Axes.Both },
Foreground = new BufferedContainer
foregroundContainer = new Container { RelativeSizeAxes = Axes.Both }
};
if (drawableObject != null)
{
var holdNote = (DrawableHoldNote)drawableObject;
AccentColour.BindTo(drawableObject.AccentColour);
IsHitting.BindTo(holdNote.IsHitting);
}
AccentColour.BindValueChanged(onAccentChanged, true);
Recycle();
}
public void Recycle() => foregroundContainer.Child = CreateForeground();
protected virtual Drawable CreateForeground() => new ForegroundPiece
{
AccentColour = { BindTarget = AccentColour },
IsHitting = { BindTarget = IsHitting }
};
private void onAccentChanged(ValueChangedEvent<Color4> accent) => Background.Colour = accent.NewValue.Opacity(0.7f);
private class ForegroundPiece : CompositeDrawable
{
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
public readonly IBindable<bool> IsHitting = new Bindable<bool>();
private readonly LayoutValue subtractionCache = new LayoutValue(Invalidation.DrawSize);
private BufferedContainer foregroundBuffer;
private BufferedContainer subtractionBuffer;
private Container subtractionLayer;
public ForegroundPiece()
{
RelativeSizeAxes = Axes.Both;
AddLayout(subtractionCache);
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = foregroundBuffer = new BufferedContainer
{
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
@@ -53,7 +93,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
Children = new Drawable[]
{
new Box { RelativeSizeAxes = Axes.Both },
subtractionContainer = new BufferedContainer
subtractionBuffer = new BufferedContainer
{
RelativeSizeAxes = Axes.Both,
// This is needed because we're blending with another object
@@ -77,60 +117,51 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
}
}
}
}
};
if (drawableObject != null)
{
var holdNote = (DrawableHoldNote)drawableObject;
AccentColour.BindTo(drawableObject.AccentColour);
isHitting.BindTo(holdNote.IsHitting);
}
AccentColour.BindValueChanged(onAccentChanged, true);
isHitting.BindValueChanged(_ => onAccentChanged(new ValueChangedEvent<Color4>(AccentColour.Value, AccentColour.Value)), true);
}
private void onAccentChanged(ValueChangedEvent<Color4> accent)
{
Foreground.Colour = accent.NewValue.Opacity(0.5f);
Background.Colour = accent.NewValue.Opacity(0.7f);
const float animation_length = 50;
Foreground.ClearTransforms(false, nameof(Foreground.Colour));
if (isHitting.Value)
{
// wait for the next sync point
double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
using (Foreground.BeginDelayedSequence(synchronisedOffset))
Foreground.FadeColour(accent.NewValue.Lighten(0.2f), animation_length).Then().FadeColour(Foreground.Colour, animation_length).Loop();
}
subtractionCache.Invalidate();
}
protected override void Update()
{
base.Update();
if (!subtractionCache.IsValid)
{
subtractionLayer.Width = 5;
subtractionLayer.Height = Math.Max(0, DrawHeight - DrawWidth);
subtractionLayer.EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.White,
Type = EdgeEffectType.Glow,
Radius = DrawWidth
};
Foreground.ForceRedraw();
subtractionContainer.ForceRedraw();
AccentColour.BindValueChanged(onAccentChanged, true);
IsHitting.BindValueChanged(_ => onAccentChanged(new ValueChangedEvent<Color4>(AccentColour.Value, AccentColour.Value)), true);
}
subtractionCache.Validate();
private void onAccentChanged(ValueChangedEvent<Color4> accent)
{
foregroundBuffer.Colour = accent.NewValue.Opacity(0.5f);
const float animation_length = 50;
foregroundBuffer.ClearTransforms(false, nameof(foregroundBuffer.Colour));
if (IsHitting.Value)
{
// wait for the next sync point
double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
using (foregroundBuffer.BeginDelayedSequence(synchronisedOffset))
foregroundBuffer.FadeColour(accent.NewValue.Lighten(0.2f), animation_length).Then().FadeColour(foregroundBuffer.Colour, animation_length).Loop();
}
subtractionCache.Invalidate();
}
protected override void Update()
{
base.Update();
if (!subtractionCache.IsValid)
{
subtractionLayer.Width = 5;
subtractionLayer.Height = Math.Max(0, DrawHeight - DrawWidth);
subtractionLayer.EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.White,
Type = EdgeEffectType.Glow,
Radius = DrawWidth
};
foregroundBuffer.ForceRedraw();
subtractionBuffer.ForceRedraw();
subtractionCache.Validate();
}
}
}
}
@@ -0,0 +1,16 @@
// 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.Rulesets.Mania.Objects.Drawables.Pieces
{
/// <summary>
/// Interface for mania hold note bodies.
/// </summary>
public interface IHoldNoteBody
{
/// <summary>
/// Recycles the contents of this <see cref="IHoldNoteBody"/> to free used resources.
/// </summary>
void Recycle();
}
}
+3 -3
View File
@@ -102,14 +102,14 @@ namespace osu.Game.Rulesets.Mania.Objects
{
StartTime = StartTime,
Column = Column,
Samples = getNodeSamples(0),
Samples = GetNodeSamples(0),
});
AddNested(Tail = new TailNote
{
StartTime = EndTime,
Column = Column,
Samples = getNodeSamples((NodeSamples?.Count - 1) ?? 1),
Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1),
});
}
@@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mania.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
private IList<HitSampleInfo> getNodeSamples(int nodeIndex) =>
public IList<HitSampleInfo> GetNodeSamples(int nodeIndex) =>
nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples;
}
}
@@ -7,6 +7,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
@@ -66,6 +67,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
public void Animate(JudgementResult result)
{
if (result.Judgement is HoldNoteTickJudgement)
return;
(explosion as IFramedAnimation)?.GotoFrame(0);
explosion?.FadeInFromZero(80)
@@ -31,12 +31,12 @@ namespace osu.Game.Rulesets.Mania.UI
/// <summary>
/// The minimum time range. This occurs at a <see cref="relativeTimeRange"/> of 40.
/// </summary>
public const double MIN_TIME_RANGE = 150;
public const double MIN_TIME_RANGE = 340;
/// <summary>
/// The maximum time range. This occurs at a <see cref="relativeTimeRange"/> of 1.
/// </summary>
public const double MAX_TIME_RANGE = 6000;
public const double MAX_TIME_RANGE = 13720;
protected new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield;
@@ -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;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModSpunOut : OsuModTestScene
{
protected override bool AllowFail => true;
[Test]
public void TestSpinnerAutoCompleted() => CreateModTest(new ModTestData
{
Mod = new OsuModSpunOut(),
Autoplay = false,
Beatmap = singleSpinnerBeatmap,
PassCondition = () => Player.ChildrenOfType<DrawableSpinner>().Single().Progress >= 1
});
[TestCase(null)]
[TestCase(typeof(OsuModDoubleTime))]
[TestCase(typeof(OsuModHalfTime))]
public void TestSpinRateUnaffectedByMods(Type additionalModType)
{
var mods = new List<Mod> { new OsuModSpunOut() };
if (additionalModType != null)
mods.Add((Mod)Activator.CreateInstance(additionalModType));
CreateModTest(new ModTestData
{
Mods = mods,
Autoplay = false,
Beatmap = singleSpinnerBeatmap,
PassCondition = () => Precision.AlmostEquals(Player.ChildrenOfType<SpinnerSpmCounter>().Single().SpinsPerMinute, 286, 1)
});
}
private Beatmap singleSpinnerBeatmap => new Beatmap
{
HitObjects = new List<HitObject>
{
new Spinner
{
Position = new Vector2(256, 192),
StartTime = 500,
Duration = 2000
}
}
};
}
}
+38 -12
View File
@@ -17,32 +17,58 @@ namespace osu.Game.Rulesets.Osu.Tests
{
private int depthIndex;
public TestSceneSpinner()
private TestDrawableSpinner drawableSpinner;
[TestCase(false)]
[TestCase(true)]
public void TestVariousSpinners(bool autoplay)
{
AddStep("Miss Big", () => SetContents(() => testSingle(2)));
AddStep("Miss Medium", () => SetContents(() => testSingle(5)));
AddStep("Miss Small", () => SetContents(() => testSingle(7)));
AddStep("Hit Big", () => SetContents(() => testSingle(2, true)));
AddStep("Hit Medium", () => SetContents(() => testSingle(5, true)));
AddStep("Hit Small", () => SetContents(() => testSingle(7, true)));
string term = autoplay ? "Hit" : "Miss";
AddStep($"{term} Big", () => SetContents(() => testSingle(2, autoplay)));
AddStep($"{term} Medium", () => SetContents(() => testSingle(5, autoplay)));
AddStep($"{term} Small", () => SetContents(() => testSingle(7, autoplay)));
}
private Drawable testSingle(float circleSize, bool auto = false)
[TestCase(false)]
[TestCase(true)]
public void TestLongSpinner(bool autoplay)
{
var spinner = new Spinner { StartTime = Time.Current + 2000, EndTime = Time.Current + 5000 };
AddStep("Very short spinner", () => SetContents(() => testSingle(5, autoplay, 2000)));
AddUntilStep("Wait for completion", () => drawableSpinner.Result.HasResult);
AddUntilStep("Check correct progress", () => drawableSpinner.Progress == (autoplay ? 1 : 0));
}
[TestCase(false)]
[TestCase(true)]
public void TestSuperShortSpinner(bool autoplay)
{
AddStep("Very short spinner", () => SetContents(() => testSingle(5, autoplay, 200)));
AddUntilStep("Wait for completion", () => drawableSpinner.Result.HasResult);
AddUntilStep("Short spinner implicitly completes", () => drawableSpinner.Progress == 1);
}
private Drawable testSingle(float circleSize, bool auto = false, double length = 3000)
{
const double delay = 2000;
var spinner = new Spinner
{
StartTime = Time.Current + delay,
EndTime = Time.Current + delay + length
};
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });
var drawable = new TestDrawableSpinner(spinner, auto)
drawableSpinner = new TestDrawableSpinner(spinner, auto)
{
Anchor = Anchor.Centre,
Depth = depthIndex++
};
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawable });
mod.ApplyToDrawableHitObjects(new[] { drawableSpinner });
return drawable;
return drawableSpinner;
}
private class TestDrawableSpinner : DrawableSpinner
@@ -1,11 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Timing;
@@ -60,58 +62,67 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestSpinnerRewindingRotation()
{
double trackerRotationTolerance = 0;
addSeekStep(5000);
AddStep("calculate rotation tolerance", () =>
{
trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f);
});
AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100));
AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, 0, 100));
AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, 0, 100));
addSeekStep(0);
AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100));
AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, 0, 100));
AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, trackerRotationTolerance));
AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, 0, 100));
}
[Test]
public void TestSpinnerMiddleRewindingRotation()
{
double finalAbsoluteDiscRotation = 0, finalRelativeDiscRotation = 0, finalSpinnerSymbolRotation = 0;
double finalCumulativeTrackerRotation = 0;
double finalTrackerRotation = 0, trackerRotationTolerance = 0;
double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
addSeekStep(5000);
AddStep("retrieve disc relative rotation", () => finalRelativeDiscRotation = drawableSpinner.RotationTracker.Rotation);
AddStep("retrieve disc absolute rotation", () => finalAbsoluteDiscRotation = drawableSpinner.RotationTracker.CumulativeRotation);
AddStep("retrieve spinner symbol rotation", () => finalSpinnerSymbolRotation = spinnerSymbol.Rotation);
AddStep("retrieve disc rotation", () =>
{
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
trackerRotationTolerance = Math.Abs(finalTrackerRotation * 0.05f);
});
AddStep("retrieve spinner symbol rotation", () =>
{
finalSpinnerSymbolRotation = spinnerSymbol.Rotation;
spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f);
});
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.RotationTracker.RateAdjustedRotation);
addSeekStep(2500);
AddUntilStep("disc rotation rewound",
AddAssert("disc rotation rewound",
// we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalRelativeDiscRotation / 2, 100));
AddUntilStep("symbol rotation rewound",
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, 100));
// due to the exponential damping applied we're allowing a larger margin of error of about 10%
// (5% relative to the final rotation value, but we're half-way through the spin).
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation / 2, trackerRotationTolerance));
AddAssert("symbol rotation rewound",
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation rewound",
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100));
addSeekStep(5000);
AddAssert("is disc rotation almost same",
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalRelativeDiscRotation, 100));
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance));
AddAssert("is symbol rotation almost same",
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, 100));
AddAssert("is disc rotation absolute almost same",
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, finalAbsoluteDiscRotation, 100));
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation almost same",
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, finalCumulativeTrackerRotation, 100));
}
[Test]
public void TestRotationDirection([Values(true, false)] bool clockwise)
{
if (clockwise)
{
AddStep("flip replay", () =>
{
var drawableRuleset = this.ChildrenOfType<DrawableOsuRuleset>().Single();
var score = drawableRuleset.ReplayScore;
var scoreWithFlippedReplay = new Score
{
ScoreInfo = score.ScoreInfo,
Replay = flipReplay(score.Replay)
};
drawableRuleset.SetReplayScore(scoreWithFlippedReplay);
});
}
transformReplay(flip);
addSeekStep(5000);
@@ -119,7 +130,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("spinner symbol direction correct", () => clockwise ? spinnerSymbol.Rotation > 0 : spinnerSymbol.Rotation < 0);
}
private Replay flipReplay(Replay scoreReplay) => new Replay
private Replay flip(Replay scoreReplay) => new Replay
{
Frames = scoreReplay
.Frames
@@ -142,7 +153,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
return totalScore == (int)(drawableSpinner.RotationTracker.CumulativeRotation / 360) * SpinnerTick.SCORE_PER_TICK;
return totalScore == (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360) * SpinnerTick.SCORE_PER_TICK;
});
addSeekStep(0);
@@ -174,6 +185,49 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
}
[TestCase(0.5)]
[TestCase(2.0)]
public void TestSpinUnaffectedByClockRate(double rate)
{
double expectedProgress = 0;
double expectedSpm = 0;
addSeekStep(1000);
AddStep("retrieve spinner state", () =>
{
expectedProgress = drawableSpinner.Progress;
expectedSpm = drawableSpinner.SpmCounter.SpinsPerMinute;
});
addSeekStep(0);
AddStep("adjust track rate", () => track.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate)));
// autoplay replay frames use track time;
// if a spin takes 1000ms in track time and we're playing with a 2x rate adjustment, the spin will take 500ms of *real* time.
// therefore we need to apply the rate adjustment to the replay itself to change from track time to real time,
// as real time is what we care about for spinners
// (so we're making the spin take 1000ms in real time *always*, regardless of the track clock's rate).
transformReplay(replay => applyRateAdjustment(replay, rate));
addSeekStep(1000);
AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpmCounter.SpinsPerMinute, 2.0));
}
private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
{
Frames = scoreReplay
.Frames
.Cast<OsuReplayFrame>()
.Select(replayFrame =>
{
var adjustedTime = replayFrame.Time * rate;
return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray());
})
.Cast<ReplayFrame>()
.ToList()
};
private void addSeekStep(double time)
{
AddStep($"seek to {time}", () => track.Seek(time));
@@ -181,6 +235,18 @@ namespace osu.Game.Rulesets.Osu.Tests
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
private void transformReplay(Func<Replay, Replay> replayTransformation) => AddStep("set replay", () =>
{
var drawableRuleset = this.ChildrenOfType<DrawableOsuRuleset>().Single();
var score = drawableRuleset.ReplayScore;
var transformedScore = new Score
{
ScoreInfo = score.ScoreInfo,
Replay = replayTransformation.Invoke(score.Replay)
};
drawableRuleset.SetReplayScore(transformedScore);
});
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
{
HitObjects = new List<HitObject>
@@ -1,59 +0,0 @@
// 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.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class TestSceneSpinnerSpunOut : OsuTestScene
{
[SetUp]
public void SetUp() => Schedule(() =>
{
SelectedMods.Value = new[] { new OsuModSpunOut() };
});
[Test]
public void TestSpunOut()
{
DrawableSpinner spinner = null;
AddStep("create spinner", () => spinner = createSpinner());
AddUntilStep("wait for end", () => Time.Current > spinner.LifetimeEnd);
AddAssert("spinner is completed", () => spinner.Progress >= 1);
}
private DrawableSpinner createSpinner()
{
var spinner = new Spinner
{
StartTime = Time.Current + 500,
EndTime = Time.Current + 2500
};
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableSpinner = new DrawableSpinner(spinner)
{
Anchor = Anchor.Centre
};
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawableSpinner });
Add(drawableSpinner);
return drawableSpinner;
}
}
}
@@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
+10 -1
View File
@@ -41,7 +41,16 @@ namespace osu.Game.Rulesets.Osu.Mods
var spinner = (DrawableSpinner)drawable;
spinner.RotationTracker.Tracking = true;
spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f));
// early-return if we were paused to avoid division-by-zero in the subsequent calculations.
if (Precision.AlmostEquals(spinner.Clock.Rate, 0))
return;
// because the spinner is under the gameplay clock, it is affected by rate adjustments on the track;
// for that reason using ElapsedFrameTime directly leads to fewer SPM with Half Time and more SPM with Double Time.
// for spinners we want the real (wall clock) elapsed time; to achieve that, unapply the clock rate locally here.
var rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate;
spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * 0.03f));
}
}
}
@@ -14,8 +14,8 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Scoring;
using osuTK;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -11,6 +12,7 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Skinning;
@@ -30,6 +32,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>();
private bool spinnerFrequencyModulate;
public DrawableSpinner(Spinner s)
: base(s)
{
@@ -81,8 +85,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
private SkinnableSound spinningSample;
private const float minimum_volume = 0.0001f;
private const float spinning_sample_initial_frequency = 1.0f;
private const float spinning_sample_modulated_base_frequency = 0.5f;
protected override void LoadSamples()
{
@@ -100,8 +104,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
AddInternal(spinningSample = new SkinnableSound(clone)
{
Volume = { Value = minimum_volume },
Volume = { Value = 0 },
Looping = true,
Frequency = { Value = spinning_sample_initial_frequency }
});
}
}
@@ -118,7 +123,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
else
{
spinningSample?.VolumeTo(minimum_volume, 200).Finally(_ => spinningSample.Stop());
spinningSample?.VolumeTo(0, 200).Finally(_ => spinningSample.Stop());
}
}
@@ -172,10 +177,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
positionBindable.BindTo(HitObject.PositionBindable);
}
protected override void ApplySkin(ISkinSource skin, bool allowFallback)
{
base.ApplySkin(skin, allowFallback);
spinnerFrequencyModulate = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.SpinnerFrequencyModulate)?.Value ?? true;
}
/// <summary>
/// The completion progress of this spinner from 0..1 (clamped).
/// </summary>
public float Progress => Math.Clamp(RotationTracker.CumulativeRotation / 360 / Spinner.SpinsRequired, 0, 1);
public float Progress
{
get
{
if (Spinner.SpinsRequired == 0)
// some spinners are so short they can't require an integer spin count.
// these become implicitly hit.
return 1;
return Math.Clamp(RotationTracker.RateAdjustedRotation / 360 / Spinner.SpinsRequired, 0, 1);
}
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
@@ -210,9 +232,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (HandleUserInput)
RotationTracker.Tracking = !Result.HasResult && (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
if (spinningSample != null)
// todo: implement SpinnerFrequencyModulate
spinningSample.Frequency.Value = 0.5f + Progress;
if (spinningSample != null && spinnerFrequencyModulate)
spinningSample.Frequency.Value = spinning_sample_modulated_base_frequency + Progress;
}
protected override void UpdateAfterChildren()
@@ -221,7 +242,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (!SpmCounter.IsPresent && RotationTracker.Tracking)
SpmCounter.FadeIn(HitObject.TimeFadeIn);
SpmCounter.SetRotation(RotationTracker.CumulativeRotation);
SpmCounter.SetRotation(RotationTracker.RateAdjustedRotation);
updateBonusScore();
}
@@ -233,7 +254,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (ticks.Count == 0)
return;
int spins = (int)(RotationTracker.CumulativeRotation / 360);
int spins = (int)(RotationTracker.RateAdjustedRotation / 360);
if (spins < wholeSpins)
{
@@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
get
{
int rotations = (int)(drawableSpinner.RotationTracker.CumulativeRotation / 360);
int rotations = (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360);
if (wholeRotationCount == rotations) return false;
@@ -31,17 +31,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
public readonly BindableBool Complete = new BindableBool();
/// <summary>
/// The total rotation performed on the spinner disc, disregarding the spin direction.
/// The total rotation performed on the spinner disc, disregarding the spin direction,
/// adjusted for the track's playback rate.
/// </summary>
/// <remarks>
/// <para>
/// This value is always non-negative and is monotonically increasing with time
/// (i.e. will only increase if time is passing forward, but can decrease during rewind).
/// </para>
/// <para>
/// The rotation from each frame is multiplied by the clock's current playback rate.
/// The reason this is done is to ensure that spinners give the same score and require the same number of spins
/// regardless of whether speed-modifying mods are applied.
/// </para>
/// </remarks>
/// <example>
/// If the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise,
/// Assuming no speed-modifying mods are active,
/// if the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise,
/// this property will return the value of 720 (as opposed to 0 for <see cref="Drawable.Rotation"/>).
/// If Double Time is active instead (with a speed multiplier of 1.5x),
/// in the same scenario the property will return 720 * 1.5 = 1080.
/// </example>
public float CumulativeRotation { get; private set; }
public float RateAdjustedRotation { get; private set; }
/// <summary>
/// Whether the spinning is spinning at a reasonable speed to be considered visually spinning.
@@ -113,7 +124,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
currentRotation += angle;
CumulativeRotation += Math.Abs(angle) * Math.Sign(Clock.ElapsedFrameTime);
// rate has to be applied each frame, because it's not guaranteed to be constant throughout playback
// (see: ModTimeRamp)
RateAdjustedRotation += (float)(Math.Abs(angle) * Clock.Rate);
}
}
}
+1 -2
View File
@@ -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 osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
@@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Objects
double minimumRotationsPerSecond = stable_matching_fudge * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5);
SpinsRequired = (int)Math.Max(1, (secondsDuration * minimumRotationsPerSecond));
SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond);
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration);
}
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement
{
protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK;
protected override int NumericResultFor(HitResult result) => result == MaxResult ? SCORE_PER_TICK : 0;
protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2;
}
+1 -1
View File
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public override bool AffectsCombo => false;
protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK;
protected override int NumericResultFor(HitResult result) => result == MaxResult ? SCORE_PER_TICK : 0;
protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0;
}
@@ -14,6 +14,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Rulesets.Osu.Skinning
{
@@ -28,53 +29,67 @@ namespace osu.Game.Rulesets.Osu.Skinning
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
}
private Container<Sprite> circleSprites;
private Sprite hitCircleSprite;
private Sprite hitCircleOverlay;
private SkinnableSpriteText hitCircleText;
private readonly IBindable<ArmedState> state = new Bindable<ArmedState>();
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
[Resolved]
private ISkinSource skin { get; set; }
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject, ISkinSource skin)
private void load(DrawableHitObject drawableObject)
{
OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject;
Sprite hitCircleSprite;
SkinnableSpriteText hitCircleText;
InternalChildren = new Drawable[]
{
hitCircleSprite = new Sprite
circleSprites = new Container<Sprite>
{
Texture = getTextureWithFallback(string.Empty),
Colour = drawableObject.AccentColour.Value,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Children = new[]
{
hitCircleSprite = new Sprite
{
Texture = getTextureWithFallback(string.Empty),
Colour = drawableObject.AccentColour.Value,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
hitCircleOverlay = new Sprite
{
Texture = getTextureWithFallback("overlay"),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
},
hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
{
Font = OsuFont.Numeric.With(size: 40),
UseFullGlyphHeight = false,
}, confineMode: ConfineMode.NoScaling),
new Sprite
}, confineMode: ConfineMode.NoScaling)
{
Texture = getTextureWithFallback("overlay"),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
},
};
bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
if (!overlayAboveNumber)
ChangeInternalChildDepth(hitCircleText, -float.MaxValue);
if (overlayAboveNumber)
AddInternal(hitCircleOverlay.CreateProxy());
state.BindTo(drawableObject.State);
state.BindValueChanged(updateState, true);
accentColour.BindTo(drawableObject.AccentColour);
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true);
indexInCurrentCombo.BindTo(osuObject.IndexInCurrentComboBindable);
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
Texture getTextureWithFallback(string name)
{
@@ -87,6 +102,15 @@ namespace osu.Game.Rulesets.Osu.Skinning
}
}
protected override void LoadComplete()
{
base.LoadComplete();
state.BindValueChanged(updateState, true);
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true);
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
}
private void updateState(ValueChangedEvent<ArmedState> state)
{
const double legacy_fade_duration = 240;
@@ -94,8 +118,21 @@ namespace osu.Game.Rulesets.Osu.Skinning
switch (state.NewValue)
{
case ArmedState.Hit:
this.FadeOut(legacy_fade_duration, Easing.Out);
this.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
circleSprites.FadeOut(legacy_fade_duration, Easing.Out);
circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
var legacyVersion = skin.GetConfig<LegacySetting, decimal>(LegacySetting.Version)?.Value;
if (legacyVersion >= 2.0m)
// legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
else
{
// old skins scale and fade it normally along other pieces.
hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
}
break;
}
}
@@ -79,8 +79,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
var spinner = (Spinner)drawableSpinner.HitObject;
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
this.FadeInFromZero(spinner.TimePreempt / 2);
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
this.FadeInFromZero(spinner.TimeFadeIn / 2);
fixedMiddle.FadeColour(Color4.White);
using (BeginAbsoluteSequence(spinner.StartTime, true))
@@ -85,8 +85,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
var spinner = drawableSpinner.HitObject;
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
this.FadeInFromZero(spinner.TimePreempt / 2);
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
this.FadeInFromZero(spinner.TimeFadeIn / 2);
}
protected override void Update()
@@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
CursorExpand,
CursorRotate,
HitCircleOverlayAboveNumber,
HitCircleOverlayAboveNumer // Some old skins will have this typo
HitCircleOverlayAboveNumer, // Some old skins will have this typo
SpinnerFrequencyModulate
}
}
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.UI
protected override PassThroughInputManager CreateInputManager() => new OsuInputManager(Ruleset.RulesetInfo);
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer();
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { AlignWithStoryboard = true };
protected override ResumeOverlay CreateResumeOverlay() => new OsuResumeOverlay();
+12 -4
View File
@@ -23,7 +23,8 @@ namespace osu.Game.Rulesets.Osu.UI
{
public class OsuPlayfield : Playfield
{
private readonly ApproachCircleProxyContainer approachCircles;
private readonly ProxyContainer approachCircles;
private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
private readonly FollowPointRenderer followPoints;
private readonly OrderedHitPolicy hitPolicy;
@@ -38,6 +39,10 @@ namespace osu.Game.Rulesets.Osu.UI
{
InternalChildren = new Drawable[]
{
spinnerProxies = new ProxyContainer
{
RelativeSizeAxes = Axes.Both
},
followPoints = new FollowPointRenderer
{
RelativeSizeAxes = Axes.Both,
@@ -54,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.UI
{
Child = HitObjectContainer,
},
approachCircles = new ApproachCircleProxyContainer
approachCircles = new ProxyContainer
{
RelativeSizeAxes = Axes.Both,
Depth = -1,
@@ -76,6 +81,9 @@ namespace osu.Game.Rulesets.Osu.UI
h.OnNewResult += onNewResult;
h.OnLoadComplete += d =>
{
if (d is DrawableSpinner)
spinnerProxies.Add(d.CreateProxy());
if (d is IDrawableHitObjectWithProxiedApproach c)
approachCircles.Add(c.ProxiedLayer.CreateProxy());
};
@@ -113,9 +121,9 @@ namespace osu.Game.Rulesets.Osu.UI
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos);
private class ApproachCircleProxyContainer : LifetimeManagementContainer
private class ProxyContainer : LifetimeManagementContainer
{
public void Add(Drawable approachCircleProxy) => AddInternal(approachCircleProxy);
public void Add(Drawable proxy) => AddInternal(proxy);
}
private class DrawableJudgementPool : DrawablePool<DrawableOsuJudgement>
@@ -11,10 +11,19 @@ namespace osu.Game.Rulesets.Osu.UI
public class OsuPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{
protected override Container<Drawable> Content => content;
private readonly Container content;
private readonly ScalingContainer content;
private const float playfield_size_adjust = 0.8f;
/// <summary>
/// When true, an offset is applied to allow alignment with historical storyboards displayed in the same parent space.
/// This will shift the playfield downwards slightly.
/// </summary>
public bool AlignWithStoryboard
{
set => content.PlayfieldShift = value;
}
public OsuPlayfieldAdjustmentContainer()
{
Anchor = Anchor.Centre;
@@ -39,6 +48,8 @@ namespace osu.Game.Rulesets.Osu.UI
/// </summary>
private class ScalingContainer : Container
{
internal bool PlayfieldShift { get; set; }
protected override void Update()
{
base.Update();
@@ -55,6 +66,7 @@ namespace osu.Game.Rulesets.Osu.UI
// Scale = 819.2 / 512
// Scale = 1.6
Scale = new Vector2(Parent.ChildSize.X / OsuPlayfield.BASE_SIZE.X);
Position = new Vector2(0, (PlayfieldShift ? 8f : 0f) * Scale.X);
// Size = 0.625
Size = Vector2.Divide(Vector2.One, Scale);
}
@@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
@@ -6,9 +6,9 @@ using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Skinning;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Tests.Gameplay
{
@@ -211,7 +211,7 @@ namespace osu.Game.Tests.Gameplay
}
/// <summary>
/// Tests that when a custom sample bank is used, but <see cref="GlobalSkinConfiguration.LayeredHitSounds"/> is disabled,
/// Tests that when a custom sample bank is used, but <see cref="LegacySetting.LayeredHitSounds"/> is disabled,
/// only the additional sound will be looked up.
/// </summary>
[Test]
@@ -230,7 +230,7 @@ namespace osu.Game.Tests.Gameplay
}
/// <summary>
/// Tests that when a normal sample bank is used and <see cref="GlobalSkinConfiguration.LayeredHitSounds"/> is disabled,
/// Tests that when a normal sample bank is used and <see cref="LegacySetting.LayeredHitSounds"/> is disabled,
/// the normal sound will be looked up anyway.
/// </summary>
[Test]
@@ -247,6 +247,6 @@ namespace osu.Game.Tests.Gameplay
}
private void disableLayeredHitSounds()
=> AddStep("set LayeredHitSounds to false", () => Skin.Configuration.ConfigDictionary[GlobalSkinConfiguration.LayeredHitSounds.ToString()] = "0");
=> AddStep("set LayeredHitSounds to false", () => Skin.Configuration.ConfigDictionary[LegacySetting.LayeredHitSounds.ToString()] = "0");
}
}
@@ -0,0 +1,41 @@
// 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.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Gameplay
{
[HeadlessTest]
public class TestSceneScoreProcessor : OsuTestScene
{
[Test]
public void TestNoScoreIncreaseFromMiss()
{
var beatmap = new Beatmap<TestHitObject> { HitObjects = { new TestHitObject() } };
var scoreProcessor = new ScoreProcessor();
scoreProcessor.ApplyBeatmap(beatmap);
// Apply a miss judgement
scoreProcessor.ApplyResult(new JudgementResult(new TestHitObject(), new TestJudgement()) { Type = HitResult.Miss });
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(0.0));
}
private class TestHitObject : HitObject
{
public override Judgement CreateJudgement() => new TestJudgement();
}
private class TestJudgement : Judgement
{
protected override int NumericResultFor(HitResult result) => 100;
}
}
}
@@ -272,7 +272,21 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("Overlay is closed", () => pauseOverlay.State.Value == Visibility.Hidden);
}
[Test]
public void TestSelectionResetOnVisibilityChange()
{
showOverlay();
AddStep("Select last button", () => InputManager.Key(Key.Up));
hideOverlay();
showOverlay();
AddAssert("No button selected",
() => pauseOverlay.Buttons.All(button => !button.Selected.Value));
}
private void showOverlay() => AddStep("Show overlay", () => pauseOverlay.Show());
private void hideOverlay() => AddStep("Hide overlay", () => pauseOverlay.Hide());
private DialogButton getButton(int index) => pauseOverlay.Buttons.Skip(index).First();
@@ -46,25 +46,36 @@ namespace osu.Game.Tests.Visual.Gameplay
/// </summary>
/// <param name="interactive">If the test player should behave like the production one.</param>
/// <param name="beforeLoadAction">An action to run before player load but after bindable leases are returned.</param>
/// <param name="afterLoadAction">An action to run after container load.</param>
public void ResetPlayer(bool interactive, Action beforeLoadAction = null, Action afterLoadAction = null)
public void ResetPlayer(bool interactive, Action beforeLoadAction = null)
{
player = null;
audioManager.Volume.SetDefault();
InputManager.Clear();
container = new TestPlayerLoaderContainer(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive)));
beforeLoadAction?.Invoke();
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
foreach (var mod in SelectedMods.Value.OfType<IApplicableToTrack>())
mod.ApplyToTrack(Beatmap.Value.Track);
InputManager.Child = container = new TestPlayerLoaderContainer(
loader = new TestPlayerLoader(() =>
{
afterLoadAction?.Invoke();
return player = new TestPlayer(interactive, interactive);
}));
InputManager.Child = container;
}
[Test]
public void TestEarlyExitBeforePlayerConstruction()
{
AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() }));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddStep("exit loader", () => loader.Exit());
AddUntilStep("wait for not current", () => !loader.IsCurrentScreen());
AddAssert("player did not load", () => player == null);
AddUntilStep("player disposed", () => loader.DisposalTask == null);
AddAssert("mod rate still applied", () => Beatmap.Value.Track.Rate != 1);
}
/// <summary>
@@ -73,11 +84,12 @@ namespace osu.Game.Tests.Visual.Gameplay
/// speed adjustments were undone too late, causing cross-screen pollution.
/// </summary>
[Test]
public void TestEarlyExit()
public void TestEarlyExitAfterPlayerConstruction()
{
AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() }));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1);
AddUntilStep("wait for non-null player", () => player != null);
AddStep("exit loader", () => loader.Exit());
AddUntilStep("wait for not current", () => !loader.IsCurrentScreen());
AddAssert("player did not load", () => !player.IsLoaded);
@@ -94,7 +106,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for load ready", () =>
{
moveMouse();
return player.LoadState == LoadState.Ready;
return player?.LoadState == LoadState.Ready;
});
AddRepeatStep("move mouse", moveMouse, 20);
@@ -195,19 +207,19 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestMutedNotificationMasterVolume()
{
addVolumeSteps("master volume", () => audioManager.Volume.Value = 0, null, () => audioManager.Volume.IsDefault);
addVolumeSteps("master volume", () => audioManager.Volume.Value = 0, () => audioManager.Volume.IsDefault);
}
[Test]
public void TestMutedNotificationTrackVolume()
{
addVolumeSteps("music volume", () => audioManager.VolumeTrack.Value = 0, null, () => audioManager.VolumeTrack.IsDefault);
addVolumeSteps("music volume", () => audioManager.VolumeTrack.Value = 0, () => audioManager.VolumeTrack.IsDefault);
}
[Test]
public void TestMutedNotificationMuteButton()
{
addVolumeSteps("mute button", null, () => container.VolumeOverlay.IsMuted.Value = true, () => !container.VolumeOverlay.IsMuted.Value);
addVolumeSteps("mute button", () => container.VolumeOverlay.IsMuted.Value = true, () => !container.VolumeOverlay.IsMuted.Value);
}
/// <remarks>
@@ -215,14 +227,13 @@ namespace osu.Game.Tests.Visual.Gameplay
/// </remarks>
/// <param name="volumeName">What part of the volume system is checked</param>
/// <param name="beforeLoad">The action to be invoked to set the volume before loading</param>
/// <param name="afterLoad">The action to be invoked to set the volume after loading</param>
/// <param name="assert">The function to be invoked and checked</param>
private void addVolumeSteps(string volumeName, Action beforeLoad, Action afterLoad, Func<bool> assert)
private void addVolumeSteps(string volumeName, Action beforeLoad, Func<bool> assert)
{
AddStep("reset notification lock", () => sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce).Value = false);
AddStep("load player", () => ResetPlayer(false, beforeLoad, afterLoad));
AddUntilStep("wait for player", () => player.LoadState == LoadState.Ready);
AddStep("load player", () => ResetPlayer(false, beforeLoad));
AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready);
AddAssert("check for notification", () => container.NotificationOverlay.UnreadCount.Value == 1);
AddStep("click notification", () =>
@@ -20,7 +20,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
TextSize = 40,
Margin = new MarginPadding(20),
};
Add(score);
@@ -30,7 +29,6 @@ namespace osu.Game.Tests.Visual.Gameplay
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
Margin = new MarginPadding(10),
TextSize = 40,
};
Add(comboCounter);
@@ -4,8 +4,10 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets;
using osuTK.Input;
@@ -15,7 +17,7 @@ namespace osu.Game.Tests.Visual.Menus
[TestFixture]
public class TestSceneToolbar : OsuManualInputManagerTestScene
{
private Toolbar toolbar;
private TestToolbar toolbar;
[Resolved]
private RulesetStore rulesets { get; set; }
@@ -23,7 +25,7 @@ namespace osu.Game.Tests.Visual.Menus
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = toolbar = new Toolbar { State = { Value = Visibility.Visible } };
Child = toolbar = new TestToolbar { State = { Value = Visibility.Visible } };
});
[Test]
@@ -72,5 +74,24 @@ namespace osu.Game.Tests.Visual.Menus
AddUntilStep("ruleset switched", () => rulesetSelector.Current.Value.Equals(expected));
}
}
[TestCase(OverlayActivation.All)]
[TestCase(OverlayActivation.Disabled)]
public void TestRespectsOverlayActivation(OverlayActivation mode)
{
AddStep($"set activation mode to {mode}", () => toolbar.OverlayActivationMode.Value = mode);
AddStep("hide toolbar", () => toolbar.Hide());
AddStep("try to show toolbar", () => toolbar.Show());
if (mode == OverlayActivation.Disabled)
AddAssert("toolbar still hidden", () => toolbar.State.Value == Visibility.Hidden);
else
AddAssert("toolbar is visible", () => toolbar.State.Value == Visibility.Visible);
}
public class TestToolbar : Toolbar
{
public new Bindable<OverlayActivation> OverlayActivationMode => base.OverlayActivationMode;
}
}
}
@@ -16,7 +16,9 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Multi.Components;
using osu.Game.Screens.Select;
@@ -145,6 +147,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("new item has id 2", () => Room.Playlist.Last().ID == 2);
}
/// <summary>
/// Tests that the same <see cref="Mod"/> instances are not shared between two playlist items.
/// </summary>
[Test]
public void TestNewItemHasNewModInstances()
{
AddStep("set dt mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() });
AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem());
AddStep("change mod rate", () => ((OsuModDoubleTime)SelectedMods.Value[0]).SpeedChange.Value = 2);
AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem());
AddAssert("item 1 has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)Room.Playlist.First().RequiredMods[0]).SpeedChange.Value));
AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)Room.Playlist.Last().RequiredMods[0]).SpeedChange.Value));
}
private class TestMatchSongSelect : MatchSongSelect
{
public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails;
@@ -0,0 +1,61 @@
// 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.Graphics.Containers;
using osu.Framework.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Framework.Allocation;
using osu.Game.Overlays;
using System;
using osu.Game.Overlays.Dashboard.Home.News;
using osuTK;
using System.Collections.Generic;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneHomeNewsPanel : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Purple);
public TestSceneHomeNewsPanel()
{
Add(new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Y,
Width = 500,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
Children = new Drawable[]
{
new FeaturedNewsItemPanel(new APINewsPost
{
Title = "This post has an image which starts with \"/\" and has many authors!",
Preview = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
FirstImage = "/help/wiki/shared/news/banners/monthly-beatmapping-contest.png",
PublishedAt = DateTimeOffset.Now,
Slug = "2020-07-16-summer-theme-park-2020-voting-open"
}),
new NewsItemGroupPanel(new List<APINewsPost>
{
new APINewsPost
{
Title = "Title 1",
Slug = "2020-07-16-summer-theme-park-2020-voting-open",
PublishedAt = DateTimeOffset.Now,
},
new APINewsPost
{
Title = "Title of this post is Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
Slug = "2020-07-16-summer-theme-park-2020-voting-open",
PublishedAt = DateTimeOffset.Now,
}
}),
new ShowMoreNewsPanel()
}
});
}
}
}
@@ -31,15 +31,16 @@ namespace osu.Game.Tests.Visual.Online
{
new NewsCard(new APINewsPost
{
Title = "This post has an image which starts with \"/\" and has many authors!",
Title = "This post has an image which starts with \"/\" and has many authors! (clickable)",
Preview = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
Author = "someone, someone1, someone2, someone3, someone4",
FirstImage = "/help/wiki/shared/news/banners/monthly-beatmapping-contest.png",
PublishedAt = DateTimeOffset.Now
PublishedAt = DateTimeOffset.Now,
Slug = "2020-07-16-summer-theme-park-2020-voting-open"
}),
new NewsCard(new APINewsPost
{
Title = "This post has a full-url image! (HTML entity: &amp;)",
Title = "This post has a full-url image! (HTML entity: &amp;) (non-clickable)",
Preview = "boom (HTML entity: &amp;)",
Author = "user (HTML entity: &amp;)",
FirstImage = "https://assets.ppy.sh/artists/88/header.jpg",
@@ -0,0 +1,53 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Overlays.News;
using osu.Framework.Graphics;
using osu.Game.Overlays;
using osu.Framework.Allocation;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneNewsHeader : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
private TestHeader header;
[SetUp]
public void Setup() => Schedule(() =>
{
Child = header = new TestHeader
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
};
});
[Test]
public void TestControl()
{
AddAssert("Front page selected", () => header.Current.Value == "frontpage");
AddAssert("1 tab total", () => header.TabCount == 1);
AddStep("Set article 1", () => header.SetArticle("1"));
AddAssert("Article 1 selected", () => header.Current.Value == "1");
AddAssert("2 tabs total", () => header.TabCount == 2);
AddStep("Set article 2", () => header.SetArticle("2"));
AddAssert("Article 2 selected", () => header.Current.Value == "2");
AddAssert("2 tabs total", () => header.TabCount == 2);
AddStep("Set front page", () => header.SetFrontPage());
AddAssert("Front page selected", () => header.Current.Value == "frontpage");
AddAssert("1 tab total", () => header.TabCount == 1);
}
private class TestHeader : NewsHeader
{
public int TabCount => TabControl.Items.Count;
}
}
}
@@ -2,65 +2,64 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using NUnit.Framework;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.News;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneNewsOverlay : OsuTestScene
{
private TestNewsOverlay news;
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
protected override void LoadComplete()
private NewsOverlay news;
[SetUp]
public void SetUp() => Schedule(() => Child = news = new NewsOverlay());
[Test]
public void TestRequest()
{
base.LoadComplete();
Add(news = new TestNewsOverlay());
AddStep(@"Show", news.Show);
AddStep(@"Hide", news.Hide);
AddStep(@"Show front page", () => news.ShowFrontPage());
AddStep(@"Custom article", () => news.Current.Value = "Test Article 101");
AddStep(@"Article covers", () => news.LoadAndShowContent(new NewsCoverTest()));
setUpNewsResponse(responseExample);
AddStep("Show", () => news.Show());
AddStep("Show article", () => news.ShowArticle("article"));
}
private class TestNewsOverlay : NewsOverlay
{
public new void LoadAndShowContent(NewsContent content) => base.LoadAndShowContent(content);
}
private class NewsCoverTest : NewsContent
{
public NewsCoverTest()
private void setUpNewsResponse(GetNewsResponse r)
=> AddStep("set up response", () =>
{
Spacing = new osuTK.Vector2(0, 10);
var article = new NewsArticleCover.ArticleInfo
dummyAPI.HandleRequest = request =>
{
Author = "Ephemeral",
CoverUrl = "https://assets.ppy.sh/artists/58/header.jpg",
Time = new DateTime(2019, 12, 4),
Title = "New Featured Artist: Kurokotei"
};
if (!(request is GetNewsRequest getNewsRequest))
return;
Children = new Drawable[]
{
new NewsArticleCover(article)
{
Height = 200
},
new NewsArticleCover(article)
{
Height = 120
},
new NewsArticleCover(article)
{
RelativeSizeAxes = Axes.None,
Size = new osuTK.Vector2(400, 200),
}
getNewsRequest.TriggerSuccess(r);
};
});
private GetNewsResponse responseExample => new GetNewsResponse
{
NewsPosts = new[]
{
new APINewsPost
{
Title = "This post has an image which starts with \"/\" and has many authors!",
Preview = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
Author = "someone, someone1, someone2, someone3, someone4",
FirstImage = "/help/wiki/shared/news/banners/monthly-beatmapping-contest.png",
PublishedAt = DateTimeOffset.Now
},
new APINewsPost
{
Title = "This post has a full-url image! (HTML entity: &amp;)",
Preview = "boom (HTML entity: &amp;)",
Author = "user (HTML entity: &amp;)",
FirstImage = "https://assets.ppy.sh/artists/88/header.jpg",
PublishedAt = DateTimeOffset.Now
}
}
}
};
}
}
@@ -64,5 +64,77 @@ namespace osu.Game.Tests.Visual.Settings
}, 0, true);
});
}
[Test]
public void TestClearButtonOnBindings()
{
KeyBindingRow backBindingRow = null;
AddStep("click back binding row", () =>
{
backBindingRow = panel.ChildrenOfType<KeyBindingRow>().ElementAt(10);
InputManager.MoveMouseTo(backBindingRow);
InputManager.Click(MouseButton.Left);
});
clickClearButton();
AddAssert("first binding cleared", () => string.IsNullOrEmpty(backBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().First().Text.Text));
AddStep("click second binding", () =>
{
var target = backBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().ElementAt(1);
InputManager.MoveMouseTo(target);
InputManager.Click(MouseButton.Left);
});
clickClearButton();
AddAssert("second binding cleared", () => string.IsNullOrEmpty(backBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().ElementAt(1).Text.Text));
void clickClearButton()
{
AddStep("click clear button", () =>
{
var clearButton = backBindingRow.ChildrenOfType<KeyBindingRow.ClearButton>().Single();
InputManager.MoveMouseTo(clearButton);
InputManager.Click(MouseButton.Left);
});
}
}
[Test]
public void TestClickRowSelectsFirstBinding()
{
KeyBindingRow backBindingRow = null;
AddStep("click back binding row", () =>
{
backBindingRow = panel.ChildrenOfType<KeyBindingRow>().ElementAt(10);
InputManager.MoveMouseTo(backBindingRow);
InputManager.Click(MouseButton.Left);
});
AddAssert("first binding selected", () => backBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().First().IsBinding);
AddStep("click second binding", () =>
{
var target = backBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().ElementAt(1);
InputManager.MoveMouseTo(target);
InputManager.Click(MouseButton.Left);
});
AddStep("click back binding row", () =>
{
backBindingRow = panel.ChildrenOfType<KeyBindingRow>().ElementAt(10);
InputManager.MoveMouseTo(backBindingRow);
InputManager.Click(MouseButton.Left);
});
AddAssert("first binding selected", () => backBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().First().IsBinding);
}
}
}
@@ -44,6 +44,7 @@ namespace osu.Game.Tests.Visual
typeof(NotificationOverlay),
typeof(BeatmapListingOverlay),
typeof(DashboardOverlay),
typeof(NewsOverlay),
typeof(ChannelManager),
typeof(ChatOverlay),
typeof(SettingsOverlay),
@@ -0,0 +1,150 @@
// 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.Graphics.Containers;
using osu.Framework.Graphics;
using osu.Game.Overlays.Dashboard.Home;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Framework.Allocation;
using osu.Game.Users;
using System;
using osu.Framework.Graphics.Shapes;
using System.Collections.Generic;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneDashboardBeatmapListing : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
private readonly Container content;
public TestSceneDashboardBeatmapListing()
{
Add(content = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Y,
Width = 300,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 10 },
Child = new DashboardBeatmapListing(new_beatmaps, popular_beatmaps)
}
}
});
}
protected override void LoadComplete()
{
base.LoadComplete();
AddStep("Set width to 500", () => content.ResizeWidthTo(500, 500));
AddStep("Set width to 300", () => content.ResizeWidthTo(300, 500));
}
private static readonly List<BeatmapSetInfo> new_beatmaps = new List<BeatmapSetInfo>
{
new BeatmapSetInfo
{
Metadata = new BeatmapMetadata
{
Title = "Very Long Title (TV size) [TATOE]",
Artist = "This artist has a really long name how is this possible",
Author = new User
{
Username = "author",
Id = 100
}
},
OnlineInfo = new BeatmapSetOnlineInfo
{
Covers = new BeatmapSetOnlineCovers
{
Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608",
},
Ranked = DateTimeOffset.Now
}
},
new BeatmapSetInfo
{
Metadata = new BeatmapMetadata
{
Title = "Very Long Title (TV size) [TATOE]",
Artist = "This artist has a really long name how is this possible",
Author = new User
{
Username = "author",
Id = 100
}
},
OnlineInfo = new BeatmapSetOnlineInfo
{
Covers = new BeatmapSetOnlineCovers
{
Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608",
},
Ranked = DateTimeOffset.MinValue
}
}
};
private static readonly List<BeatmapSetInfo> popular_beatmaps = new List<BeatmapSetInfo>
{
new BeatmapSetInfo
{
Metadata = new BeatmapMetadata
{
Title = "Title",
Artist = "Artist",
Author = new User
{
Username = "author",
Id = 100
}
},
OnlineInfo = new BeatmapSetOnlineInfo
{
Covers = new BeatmapSetOnlineCovers
{
Cover = "https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg?1595295586",
},
FavouriteCount = 100
}
},
new BeatmapSetInfo
{
Metadata = new BeatmapMetadata
{
Title = "Title 2",
Artist = "Artist 2",
Author = new User
{
Username = "someone",
Id = 100
}
},
OnlineInfo = new BeatmapSetOnlineInfo
{
Covers = new BeatmapSetOnlineCovers
{
Cover = "https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg?1595295586",
},
FavouriteCount = 10
}
}
};
}
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -263,7 +264,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private void moveLogoFacade()
{
if (logoFacade?.Transforms.Count == 0 && transferContainer?.Transforms.Count == 0)
if (!(logoFacade?.Transforms).Any() && !(transferContainer?.Transforms).Any())
{
Random random = new Random();
trackingContainer.Delay(500).MoveTo(new Vector2(random.Next(0, (int)logo.Parent.DrawWidth), random.Next(0, (int)logo.Parent.DrawHeight)), 300);
@@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
@@ -15,6 +16,7 @@ using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Tests.Visual.UserInterface
@@ -75,6 +77,24 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("Customisation closed", () => modSelect.ModSettingsContainer.Alpha == 0);
}
[Test]
public void TestModSettingsUnboundWhenCopied()
{
OsuModDoubleTime original = null;
OsuModDoubleTime copy = null;
AddStep("create mods", () =>
{
original = new OsuModDoubleTime();
copy = (OsuModDoubleTime)original.CreateCopy();
});
AddStep("change property", () => original.SpeedChange.Value = 2);
AddAssert("original has new value", () => Precision.AlmostEquals(2.0, original.SpeedChange.Value));
AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, copy.SpeedChange.Value));
}
private void createModSelect()
{
AddStep("create mod select", () =>
+1 -1
View File
@@ -3,7 +3,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
</ItemGroup>
@@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Tournament.IPC;
using osu.Game.Tournament.Models;
@@ -127,21 +128,29 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
private class MatchScoreCounter : ScoreCounter
{
private OsuSpriteText displayedSpriteText;
public MatchScoreCounter()
{
Margin = new MarginPadding { Top = bar_height, Horizontal = 10 };
Winning = false;
DisplayedCountSpriteText.Spacing = new Vector2(-6);
}
public bool Winning
{
set => DisplayedCountSpriteText.Font = value
set => updateFont(value);
}
protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s =>
{
displayedSpriteText = s;
displayedSpriteText.Spacing = new Vector2(-6);
updateFont(false);
});
private void updateFont(bool winning)
=> displayedSpriteText.Font = winning
? OsuFont.Torus.With(weight: FontWeight.Bold, size: 50, fixedWidth: true)
: OsuFont.Torus.With(weight: FontWeight.Regular, size: 40, fixedWidth: true);
}
}
}
}
@@ -81,7 +81,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
{
}
private class SettingsRoundDropdown : LadderSettingsDropdown<TournamentRound>
private class SettingsRoundDropdown : SettingsDropdown<TournamentRound>
{
public SettingsRoundDropdown(BindableList<TournamentRound> rounds)
{
@@ -1,26 +0,0 @@
// 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.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
namespace osu.Game.Tournament.Screens.Ladder.Components
{
public class LadderSettingsDropdown<T> : SettingsDropdown<T>
{
protected override OsuDropdown<T> CreateDropdown() => new DropdownControl();
private new class DropdownControl : SettingsDropdown<T>.DropdownControl
{
protected override DropdownMenu CreateMenu() => new Menu();
private new class Menu : OsuDropdownMenu
{
public Menu()
{
MaxHeight = 200;
}
}
}
}
}
@@ -6,11 +6,12 @@ using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Overlays.Settings;
using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Screens.Ladder.Components
{
public class SettingsTeamDropdown : LadderSettingsDropdown<TournamentTeam>
public class SettingsTeamDropdown : SettingsDropdown<TournamentTeam>
{
public SettingsTeamDropdown(BindableList<TournamentTeam> teams)
{
@@ -67,19 +67,18 @@ namespace osu.Game.Beatmaps.Drawables
if (beatmapSet != null)
{
BeatmapSetCover cover;
Add(displayedCover = new DelayedLoadWrapper(
cover = new BeatmapSetCover(beatmapSet, coverType)
Add(displayedCover = new DelayedLoadUnloadWrapper(() =>
{
var cover = new BeatmapSetCover(beatmapSet, coverType)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
})
);
cover.OnLoadComplete += d => d.FadeInFromZero(400, Easing.Out);
};
cover.OnLoadComplete += d => d.FadeInFromZero(400, Easing.Out);
return cover;
}));
}
}
}
+9
View File
@@ -109,6 +109,15 @@ namespace osu.Game.Beatmaps
// Convert
IBeatmap converted = converter.Convert();
// Apply conversion mods to the result
foreach (var mod in mods.OfType<IApplicableAfterBeatmapConversion>())
{
if (cancellationSource.IsCancellationRequested)
throw new BeatmapLoadTimeoutException(BeatmapInfo);
mod.ApplyToBeatmap(converted);
}
// Apply difficulty mods
if (mods.Any(m => m is IApplicableToDifficulty))
{
@@ -20,7 +20,7 @@ namespace osu.Game.Graphics.Backgrounds
}
[BackgroundDependencyLoader]
private void load(TextureStore textures)
private void load(LargeTextureStore textures)
{
Sprite.Texture = Beatmap?.Background ?? textures.Get(fallbackTextureName);
}
@@ -74,9 +74,9 @@ namespace osu.Game.Graphics.UserInterface
protected override Color4 SelectionColour => new Color4(249, 90, 255, 255);
protected override void OnTextAdded(string added)
protected override void OnUserTextAdded(string added)
{
base.OnTextAdded(added);
base.OnUserTextAdded(added);
if (added.Any(char.IsUpper) && AllowUniqueCharacterSamples)
capsTextAddedSample?.Play();
@@ -84,9 +84,9 @@ namespace osu.Game.Graphics.UserInterface
textAddedSamples[RNG.Next(0, 3)]?.Play();
}
protected override void OnTextRemoved(string removed)
protected override void OnUserTextRemoved(string removed)
{
base.OnTextRemoved(removed);
base.OnUserTextRemoved(removed);
textRemovedSample?.Play();
}
@@ -3,6 +3,8 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Utils;
namespace osu.Game.Graphics.UserInterface
@@ -23,12 +25,11 @@ namespace osu.Game.Graphics.UserInterface
public PercentageCounter()
{
DisplayedCountSpriteText.Font = DisplayedCountSpriteText.Font.With(fixedWidth: true);
Current.Value = DisplayedCount = 1.0f;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours) => AccentColour = colours.BlueLighter;
private void load(OsuColour colours) => Colour = colours.BlueLighter;
protected override string FormatCount(double count) => count.FormatAccuracy();
@@ -37,6 +38,9 @@ namespace osu.Game.Graphics.UserInterface
return Math.Abs(currentValue - newValue) * RollingDuration * 100.0f;
}
protected override OsuSpriteText CreateSpriteText()
=> base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f, fixedWidth: true));
public override void Increment(double amount)
{
Current.Value += amount;
@@ -7,12 +7,12 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Sprites;
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface
{
public abstract class RollingCounter<T> : Container, IHasAccentColour
public abstract class RollingCounter<T> : Container
where T : struct, IEquatable<T>
{
/// <summary>
@@ -20,7 +20,7 @@ namespace osu.Game.Graphics.UserInterface
/// </summary>
public Bindable<T> Current = new Bindable<T>();
protected SpriteText DisplayedCountSpriteText;
private SpriteText displayedCountSpriteText;
/// <summary>
/// If true, the roll-up duration will be proportional to change in value.
@@ -46,57 +46,39 @@ namespace osu.Game.Graphics.UserInterface
public virtual T DisplayedCount
{
get => displayedCount;
set
{
if (EqualityComparer<T>.Default.Equals(displayedCount, value))
return;
displayedCount = value;
DisplayedCountSpriteText.Text = FormatCount(value);
if (displayedCountSpriteText != null)
displayedCountSpriteText.Text = FormatCount(value);
}
}
public abstract void Increment(T amount);
public float TextSize
{
get => DisplayedCountSpriteText.Font.Size;
set => DisplayedCountSpriteText.Font = DisplayedCountSpriteText.Font.With(size: value);
}
public Color4 AccentColour
{
get => DisplayedCountSpriteText.Colour;
set => DisplayedCountSpriteText.Colour = value;
}
/// <summary>
/// Skeleton of a numeric counter which value rolls over time.
/// </summary>
protected RollingCounter()
{
Children = new Drawable[]
{
DisplayedCountSpriteText = new OsuSpriteText { Font = OsuFont.Numeric }
};
TextSize = 40;
AutoSizeAxes = Axes.Both;
DisplayedCount = Current.Value;
Current.ValueChanged += val =>
{
if (IsLoaded) TransformCount(displayedCount, val.NewValue);
if (IsLoaded)
TransformCount(DisplayedCount, val.NewValue);
};
}
protected override void LoadComplete()
[BackgroundDependencyLoader]
private void load()
{
base.LoadComplete();
DisplayedCountSpriteText.Text = FormatCount(Current.Value);
displayedCountSpriteText = CreateSpriteText();
displayedCountSpriteText.Text = FormatCount(DisplayedCount);
Child = displayedCountSpriteText;
}
/// <summary>
@@ -167,5 +149,10 @@ namespace osu.Game.Graphics.UserInterface
this.TransformTo(nameof(DisplayedCount), newValue, rollingTotalDuration, RollingEasing);
}
protected virtual OsuSpriteText CreateSpriteText() => new OsuSpriteText
{
Font = OsuFont.Numeric.With(size: 40f),
};
}
}
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Graphics.UserInterface
{
@@ -24,12 +25,11 @@ namespace osu.Game.Graphics.UserInterface
/// <param name="leading">How many leading zeroes the counter will have.</param>
public ScoreCounter(uint leading = 0)
{
DisplayedCountSpriteText.Font = DisplayedCountSpriteText.Font.With(fixedWidth: true);
LeadingZeroes = leading;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours) => AccentColour = colours.BlueLighter;
private void load(OsuColour colours) => Colour = colours.BlueLighter;
protected override double GetProportionalDuration(double currentValue, double newValue)
{
@@ -49,6 +49,9 @@ namespace osu.Game.Graphics.UserInterface
return ((long)count).ToString(format);
}
protected override OsuSpriteText CreateSpriteText()
=> base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true));
public override void Increment(double amount)
{
Current.Value += amount;
@@ -3,6 +3,8 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Graphics.UserInterface
{
@@ -19,7 +21,7 @@ namespace osu.Game.Graphics.UserInterface
}
[BackgroundDependencyLoader]
private void load(OsuColour colours) => AccentColour = colours.BlueLighter;
private void load(OsuColour colours) => Colour = colours.BlueLighter;
protected override string FormatCount(int count)
{
@@ -35,5 +37,8 @@ namespace osu.Game.Graphics.UserInterface
{
Current.Value += amount;
}
protected override OsuSpriteText CreateSpriteText()
=> base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f));
}
}
@@ -53,6 +53,7 @@ namespace osu.Game.Input.Bindings
public IEnumerable<KeyBinding> InGameKeyBindings => new[]
{
new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene),
new KeyBinding(InputKey.ExtraMouseButton2, GlobalAction.SkipCutscene),
new KeyBinding(InputKey.Tilde, GlobalAction.QuickRetry),
new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit),
new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed),
@@ -0,0 +1,27 @@
// 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.IO.Network;
using osu.Game.Extensions;
namespace osu.Game.Online.API.Requests
{
public class GetNewsRequest : APIRequest<GetNewsResponse>
{
private readonly Cursor cursor;
public GetNewsRequest(Cursor cursor = null)
{
this.cursor = cursor;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.AddCursor(cursor);
return req;
}
protected override string Target => "news";
}
}
@@ -0,0 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using Newtonsoft.Json;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
public class GetNewsResponse : ResponseWithCursor
{
[JsonProperty("news_posts")]
public IEnumerable<APINewsPost> NewsPosts;
}
}
@@ -82,20 +82,10 @@ namespace osu.Game.Online.Leaderboards
Children = new Drawable[]
{
new Container
new RankLabel(rank)
{
RelativeSizeAxes = Axes.Y,
Width = rank_width,
Children = new[]
{
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 20, italics: true),
Text = rank == null ? "-" : rank.Value.ToMetric(decimals: rank < 100000 ? 1 : 0),
},
},
},
content = new Container
{
@@ -356,6 +346,25 @@ namespace osu.Game.Online.Leaderboards
}
}
private class RankLabel : Container, IHasTooltip
{
public RankLabel(int? rank)
{
if (rank >= 1000)
TooltipText = $"#{rank:N0}";
Child = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 20, italics: true),
Text = rank == null ? "-" : rank.Value.ToMetric(decimals: rank < 100000 ? 1 : 0),
};
}
public string TooltipText { get; }
}
public class LeaderboardScoreStatistic
{
public IconUsage Icon;
+18 -34
View File
@@ -67,12 +67,12 @@ namespace osu.Game
[NotNull]
private readonly NotificationOverlay notifications = new NotificationOverlay();
private NowPlayingOverlay nowPlaying;
private BeatmapListingOverlay beatmapListing;
private DashboardOverlay dashboard;
private NewsOverlay news;
private UserProfileOverlay userProfile;
private BeatmapSetOverlay beatmapSetOverlay;
@@ -632,6 +632,7 @@ namespace osu.Game
// overlay elements
loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true);
loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true);
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true);
loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true);
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
@@ -647,7 +648,7 @@ namespace osu.Game
Origin = Anchor.TopRight,
}, rightFloatingOverlayContent.Add, true);
loadComponentSingleFile(nowPlaying = new NowPlayingOverlay
loadComponentSingleFile(new NowPlayingOverlay
{
GetToolbarHeight = () => ToolbarOffset,
Anchor = Anchor.TopRight,
@@ -682,14 +683,13 @@ namespace osu.Game
{
overlay.State.ValueChanged += state =>
{
if (state.NewValue == Visibility.Hidden) return;
informationalOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
if (state.NewValue != Visibility.Hidden)
showOverlayAboveOthers(overlay, informationalOverlays);
};
}
// ensure only one of these overlays are open at once.
var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, dashboard, beatmapListing, changelogOverlay, rankingsOverlay };
var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, news, dashboard, beatmapListing, changelogOverlay, rankingsOverlay };
foreach (var overlay in singleDisplayOverlays)
{
@@ -698,9 +698,8 @@ namespace osu.Game
// informational overlays should be dismissed on a show or hide of a full overlay.
informationalOverlays.ForEach(o => o.Hide());
if (state.NewValue == Visibility.Hidden) return;
singleDisplayOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
if (state.NewValue != Visibility.Hidden)
showOverlayAboveOthers(overlay, singleDisplayOverlays);
};
}
@@ -725,6 +724,15 @@ namespace osu.Game
notifications.State.ValueChanged += _ => updateScreenOffset();
}
private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays)
{
otherOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
// show above others if not visible at all, else leave at current depth.
if (!overlay.IsPresent)
overlayContent.ChangeChildDepth(overlay, (float)-Clock.CurrentTime);
}
public class GameIdleTracker : IdleTracker
{
private InputManager inputManager;
@@ -859,18 +867,6 @@ namespace osu.Game
switch (action)
{
case GlobalAction.ToggleNowPlaying:
nowPlaying.ToggleVisibility();
return true;
case GlobalAction.ToggleChat:
chatOverlay.ToggleVisibility();
return true;
case GlobalAction.ToggleSocial:
dashboard.ToggleVisibility();
return true;
case GlobalAction.ResetInputSettings:
var sensitivity = frameworkConfig.GetBindable<double>(FrameworkSetting.CursorSensitivity);
@@ -886,18 +882,6 @@ namespace osu.Game
Toolbar.ToggleVisibility();
return true;
case GlobalAction.ToggleSettings:
Settings.ToggleVisibility();
return true;
case GlobalAction.ToggleDirect:
beatmapListing.ToggleVisibility();
return true;
case GlobalAction.ToggleNotifications:
notifications.ToggleVisibility();
return true;
case GlobalAction.ToggleGameplayMouseButtons:
LocalConfig.Set(OsuSetting.MouseDisableButtons, !LocalConfig.Get<bool>(OsuSetting.MouseDisableButtons));
return true;
+12 -2
View File
@@ -36,6 +36,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osuTK.Input;
using RuntimeInfo = osu.Framework.RuntimeInfo;
namespace osu.Game
{
@@ -134,8 +135,17 @@ namespace osu.Game
[BackgroundDependencyLoader]
private void load()
{
using (var str = File.OpenRead(typeof(OsuGameBase).Assembly.Location))
VersionHash = str.ComputeMD5Hash();
try
{
using (var str = File.OpenRead(typeof(OsuGameBase).Assembly.Location))
VersionHash = str.ComputeMD5Hash();
}
catch
{
// special case for android builds, which can't read DLLs from a packed apk.
// should eventually be handled in a better way.
VersionHash = $"{Version}-{RuntimeInfo.OS}".ComputeMD5Hash();
}
Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly));
@@ -0,0 +1,43 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osuTK;
namespace osu.Game.Overlays.Dashboard.Home
{
public class DashboardBeatmapListing : CompositeDrawable
{
private readonly List<BeatmapSetInfo> newBeatmaps;
private readonly List<BeatmapSetInfo> popularBeatmaps;
public DashboardBeatmapListing(List<BeatmapSetInfo> newBeatmaps, List<BeatmapSetInfo> popularBeatmaps)
{
this.newBeatmaps = newBeatmaps;
this.popularBeatmaps = popularBeatmaps;
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new FillFlowContainer<DrawableBeatmapList>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10),
Children = new DrawableBeatmapList[]
{
new DrawableNewBeatmapList(newBeatmaps),
new DrawablePopularBeatmapList(popularBeatmaps)
}
};
}
}
}
@@ -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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Overlays.Dashboard.Home
{
public abstract class DashboardBeatmapPanel : OsuClickableContainer
{
[Resolved]
protected OverlayColourProvider ColourProvider { get; private set; }
[Resolved(canBeNull: true)]
private BeatmapSetOverlay beatmapOverlay { get; set; }
protected readonly BeatmapSetInfo SetInfo;
private Box hoverBackground;
private SpriteIcon chevron;
protected DashboardBeatmapPanel(BeatmapSetInfo setInfo)
{
SetInfo = setInfo;
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
Height = 60;
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = -10 },
Child = hoverBackground = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background3,
Alpha = 0
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 70),
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
},
RowDimensions = new[]
{
new Dimension()
},
Content = new[]
{
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 6,
Child = new UpdateableBeatmapSetCover
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
BeatmapSet = SetInfo
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = 10 },
Child = new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new OsuSpriteText
{
RelativeSizeAxes = Axes.X,
Truncate = true,
Font = OsuFont.GetFont(weight: FontWeight.Regular),
Text = SetInfo.Metadata.Title
},
new OsuSpriteText
{
RelativeSizeAxes = Axes.X,
Truncate = true,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular),
Text = SetInfo.Metadata.Artist
},
new LinkFlowContainer(f => f.Font = OsuFont.GetFont(size: 10, weight: FontWeight.Regular))
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Spacing = new Vector2(3),
Margin = new MarginPadding { Top = 2 }
}.With(c =>
{
c.AddText("by");
c.AddUserLink(SetInfo.Metadata.Author);
c.AddArbitraryDrawable(CreateInfo());
})
}
}
},
chevron = new SpriteIcon
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Size = new Vector2(16),
Icon = FontAwesome.Solid.ChevronRight,
Colour = ColourProvider.Foreground1
}
}
}
}
}
};
Action = () =>
{
if (SetInfo.OnlineBeatmapSetID.HasValue)
beatmapOverlay?.FetchAndShowBeatmapSet(SetInfo.OnlineBeatmapSetID.Value);
};
}
protected abstract Drawable CreateInfo();
protected override bool OnHover(HoverEvent e)
{
base.OnHover(e);
hoverBackground.FadeIn(200, Easing.OutQuint);
chevron.FadeColour(ColourProvider.Light1, 200, Easing.OutQuint);
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
hoverBackground.FadeOut(200, Easing.OutQuint);
chevron.FadeColour(ColourProvider.Foreground1, 200, Easing.OutQuint);
}
}
}
@@ -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 osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
namespace osu.Game.Overlays.Dashboard.Home
{
public class DashboardNewBeatmapPanel : DashboardBeatmapPanel
{
public DashboardNewBeatmapPanel(BeatmapSetInfo setInfo)
: base(setInfo)
{
}
protected override Drawable CreateInfo() => new DrawableDate(SetInfo.OnlineInfo.Ranked ?? DateTimeOffset.Now, 10, false)
{
Colour = ColourProvider.Foreground1
};
}
}
@@ -0,0 +1,42 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Overlays.Dashboard.Home
{
public class DashboardPopularBeatmapPanel : DashboardBeatmapPanel
{
public DashboardPopularBeatmapPanel(BeatmapSetInfo setInfo)
: base(setInfo)
{
}
protected override Drawable CreateInfo() => new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(3, 0),
Colour = ColourProvider.Foreground1,
Children = new Drawable[]
{
new SpriteIcon
{
Size = new Vector2(10),
Icon = FontAwesome.Solid.Heart
},
new OsuSpriteText
{
Font = OsuFont.GetFont(size: 10, weight: FontWeight.Regular),
Text = SetInfo.OnlineInfo.FavouriteCount.ToString()
}
}
};
}
}
@@ -0,0 +1,56 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Overlays.Dashboard.Home
{
public abstract class DrawableBeatmapList : CompositeDrawable
{
private readonly List<BeatmapSetInfo> beatmaps;
protected DrawableBeatmapList(List<BeatmapSetInfo> beatmaps)
{
this.beatmaps = beatmaps;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
FillFlowContainer flow;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = flow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10),
Children = new Drawable[]
{
new OsuSpriteText
{
Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold),
Colour = colourProvider.Light1,
Text = Title
}
}
};
flow.AddRange(beatmaps.Select(CreateBeatmapPanel));
}
protected abstract string Title { get; }
protected abstract DashboardBeatmapPanel CreateBeatmapPanel(BeatmapSetInfo setInfo);
}
}
@@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
namespace osu.Game.Overlays.Dashboard.Home
{
public class DrawableNewBeatmapList : DrawableBeatmapList
{
public DrawableNewBeatmapList(List<BeatmapSetInfo> beatmaps)
: base(beatmaps)
{
}
protected override DashboardBeatmapPanel CreateBeatmapPanel(BeatmapSetInfo setInfo) => new DashboardNewBeatmapPanel(setInfo);
protected override string Title => "New Ranked Beatmaps";
}
}
@@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
namespace osu.Game.Overlays.Dashboard.Home
{
public class DrawablePopularBeatmapList : DrawableBeatmapList
{
public DrawablePopularBeatmapList(List<BeatmapSetInfo> beatmaps)
: base(beatmaps)
{
}
protected override DashboardBeatmapPanel CreateBeatmapPanel(BeatmapSetInfo setInfo) => new DashboardPopularBeatmapPanel(setInfo);
protected override string Title => "Popular Beatmaps";
}
}
@@ -0,0 +1,55 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Dashboard.Home
{
public class HomePanel : Container
{
protected override Container<Drawable> Content => content;
private readonly Container content;
private readonly Box background;
public HomePanel()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Masking = true;
EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.Black.Opacity(0.25f),
Type = EdgeEffectType.Shadow,
Radius = 3,
Offset = new Vector2(0, 1)
};
AddRangeInternal(new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
});
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
background.Colour = colourProvider.Background4;
}
}
}
@@ -0,0 +1,195 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Platform;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.News;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Dashboard.Home.News
{
public class FeaturedNewsItemPanel : HomePanel
{
private readonly APINewsPost post;
public FeaturedNewsItemPanel(APINewsPost post)
{
this.post = post;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Children = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new ClickableNewsBackground(post),
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, size: 60),
new Dimension(GridSizeMode.Absolute, size: 20),
new Dimension()
},
Content = new[]
{
new Drawable[]
{
new Date(post.PublishedAt),
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Vertical = 10 },
Child = new Box
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopRight,
Width = 1,
RelativeSizeAxes = Axes.Y,
Colour = colourProvider.Light1
}
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = 5, Bottom = 10 },
Padding = new MarginPadding { Right = 10 },
Spacing = new Vector2(0, 10),
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new NewsTitleLink(post),
new TextFlowContainer(f =>
{
f.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular);
})
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Text = post.Preview
}
}
}
}
}
}
}
}
};
}
private class ClickableNewsBackground : OsuHoverContainer
{
private readonly APINewsPost post;
public ClickableNewsBackground(APINewsPost post)
{
this.post = post;
RelativeSizeAxes = Axes.X;
Height = 130;
}
[BackgroundDependencyLoader]
private void load(GameHost host)
{
NewsPostBackground bg;
Child = new DelayedLoadWrapper(bg = new NewsPostBackground(post.FirstImage)
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0
})
{
RelativeSizeAxes = Axes.Both
};
bg.OnLoadComplete += d => d.FadeIn(250, Easing.In);
TooltipText = "view in browser";
Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug);
HoverColour = Color4.White;
}
}
private class Date : CompositeDrawable, IHasCustomTooltip
{
public ITooltip GetCustomTooltip() => new DateTooltip();
public object TooltipContent => date;
private readonly DateTimeOffset date;
public Date(DateTimeOffset date)
{
this.date = date;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
AutoSizeAxes = Axes.Both;
Anchor = Anchor.TopRight;
Origin = Anchor.TopRight;
Margin = new MarginPadding { Top = 10 };
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Font = OsuFont.GetFont(weight: FontWeight.Bold), // using Bold since there is no 800 weight alternative
Colour = colourProvider.Light1,
Text = $"{date:dd}"
},
new TextFlowContainer(f =>
{
f.Font = OsuFont.GetFont(size: 11, weight: FontWeight.Regular);
f.Colour = colourProvider.Light1;
})
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Text = $"{date:MMM yyyy}"
}
}
};
}
}
}
}
@@ -0,0 +1,115 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Overlays.Dashboard.Home.News
{
public class NewsGroupItem : CompositeDrawable
{
private readonly APINewsPost post;
public NewsGroupItem(APINewsPost post)
{
this.post = post;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, size: 60),
new Dimension(GridSizeMode.Absolute, size: 20),
new Dimension()
},
Content = new[]
{
new Drawable[]
{
new Date(post.PublishedAt),
new Box
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopRight,
Width = 1,
RelativeSizeAxes = Axes.Y,
Colour = colourProvider.Light1
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Padding = new MarginPadding { Right = 10 },
Child = new NewsTitleLink(post)
}
}
}
};
}
private class Date : CompositeDrawable, IHasCustomTooltip
{
public ITooltip GetCustomTooltip() => new DateTooltip();
public object TooltipContent => date;
private readonly DateTimeOffset date;
public Date(DateTimeOffset date)
{
this.date = date;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
TextFlowContainer textFlow;
AutoSizeAxes = Axes.Both;
Anchor = Anchor.TopRight;
Origin = Anchor.TopRight;
InternalChild = textFlow = new TextFlowContainer(t =>
{
t.Colour = colourProvider.Light1;
})
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Vertical = 5 }
};
textFlow.AddText($"{date:dd}", t =>
{
t.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold);
});
textFlow.AddText($"{date: MMM}", t =>
{
t.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Regular);
});
}
}
}
}
@@ -0,0 +1,36 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Overlays.Dashboard.Home.News
{
public class NewsItemGroupPanel : HomePanel
{
private readonly List<APINewsPost> posts;
public NewsItemGroupPanel(List<APINewsPost> posts)
{
this.posts = posts;
}
[BackgroundDependencyLoader]
private void load()
{
Content.Padding = new MarginPadding { Vertical = 5 };
Child = new FillFlowContainer<NewsGroupItem>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = posts.Select(p => new NewsGroupItem(p)).ToArray()
};
}
}
}
@@ -0,0 +1,45 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Platform;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Overlays.Dashboard.Home.News
{
public class NewsTitleLink : OsuHoverContainer
{
private readonly APINewsPost post;
public NewsTitleLink(APINewsPost post)
{
this.post = post;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
}
[BackgroundDependencyLoader]
private void load(GameHost host, OverlayColourProvider colourProvider)
{
Child = new TextFlowContainer(t =>
{
t.Font = OsuFont.GetFont(weight: FontWeight.Bold);
})
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Text = post.Title
};
HoverColour = colourProvider.Light1;
TooltipText = "view in browser";
Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug);
}
}
}
@@ -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.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
namespace osu.Game.Overlays.Dashboard.Home.News
{
public class ShowMoreNewsPanel : OsuHoverContainer
{
protected override IEnumerable<Drawable> EffectTargets => new[] { text };
[Resolved(canBeNull: true)]
private NewsOverlay overlay { get; set; }
private OsuSpriteText text;
public ShowMoreNewsPanel()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Child = new HomePanel
{
Child = text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Vertical = 20 },
Text = "see more"
}
};
IdleColour = colourProvider.Light1;
HoverColour = Color4.White;
Action = () =>
{
overlay?.ShowFrontPage();
};
}
}
}
+3 -5
View File
@@ -19,7 +19,6 @@ namespace osu.Game.Overlays
{
private CancellationTokenSource cancellationToken;
private Box background;
private Container content;
private DashboardOverlayHeader header;
private LoadingLayer loading;
@@ -35,9 +34,10 @@ namespace osu.Game.Overlays
{
Children = new Drawable[]
{
background = new Box
new Box
{
RelativeSizeAxes = Axes.Both
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background5
},
scrollFlow = new OverlayScrollContainer
{
@@ -66,8 +66,6 @@ namespace osu.Game.Overlays
},
loading = new LoadingLayer(content),
};
background.Colour = ColourProvider.Background5;
}
protected override void LoadComplete()
+13 -10
View File
@@ -48,8 +48,7 @@ namespace osu.Game.Overlays.KeyBinding
public bool FilteringActive { get; set; }
private OsuSpriteText text;
private Drawable pressAKey;
private FillFlowContainer cancelAndClearButtons;
private FillFlowContainer<KeyButton> buttons;
public IEnumerable<string> FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend((string)text.Text);
@@ -80,7 +79,7 @@ namespace osu.Game.Overlays.KeyBinding
Hollow = true,
};
Children = new[]
Children = new Drawable[]
{
new Box
{
@@ -99,7 +98,7 @@ namespace osu.Game.Overlays.KeyBinding
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight
},
pressAKey = new FillFlowContainer
cancelAndClearButtons = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding(padding) { Top = height + padding * 2 },
@@ -187,7 +186,8 @@ namespace osu.Game.Overlays.KeyBinding
if (bindTarget.IsHovered)
finalise();
else
// prevent updating bind target before clear button's action
else if (!cancelAndClearButtons.Any(b => b.IsHovered))
updateBindTarget();
}
@@ -298,8 +298,8 @@ namespace osu.Game.Overlays.KeyBinding
if (HasFocus)
GetContainingInputManager().ChangeFocus(null);
pressAKey.FadeOut(300, Easing.OutQuint);
pressAKey.BypassAutoSizeAxes |= Axes.Y;
cancelAndClearButtons.FadeOut(300, Easing.OutQuint);
cancelAndClearButtons.BypassAutoSizeAxes |= Axes.Y;
}
protected override void OnFocus(FocusEvent e)
@@ -307,8 +307,8 @@ namespace osu.Game.Overlays.KeyBinding
AutoSizeDuration = 500;
AutoSizeEasing = Easing.OutQuint;
pressAKey.FadeIn(300, Easing.OutQuint);
pressAKey.BypassAutoSizeAxes &= ~Axes.Y;
cancelAndClearButtons.FadeIn(300, Easing.OutQuint);
cancelAndClearButtons.BypassAutoSizeAxes &= ~Axes.Y;
updateBindTarget();
base.OnFocus(e);
@@ -320,6 +320,9 @@ namespace osu.Game.Overlays.KeyBinding
base.OnFocusLost(e);
}
/// <summary>
/// Updates the bind target to the currently hovered key button or the first if clicked anywhere else.
/// </summary>
private void updateBindTarget()
{
if (bindTarget != null) bindTarget.IsBinding = false;
@@ -354,7 +357,7 @@ namespace osu.Game.Overlays.KeyBinding
}
}
private class KeyButton : Container
public class KeyButton : Container
{
public readonly Framework.Input.Bindings.KeyBinding KeyBinding;
@@ -0,0 +1,114 @@
// 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 System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osuTK;
namespace osu.Game.Overlays.News.Displays
{
public class FrontPageDisplay : CompositeDrawable
{
[Resolved]
private IAPIProvider api { get; set; }
private FillFlowContainer content;
private ShowMoreButton showMore;
private GetNewsRequest request;
private Cursor lastCursor;
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding
{
Vertical = 20,
Left = 30,
Right = 50
};
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10),
Children = new Drawable[]
{
content = new FillFlowContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10)
},
showMore = new ShowMoreButton
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Margin = new MarginPadding
{
Top = 15
},
Action = performFetch,
Alpha = 0
}
}
};
performFetch();
}
private void performFetch()
{
request?.Cancel();
request = new GetNewsRequest(lastCursor);
request.Success += response => Schedule(() => onSuccess(response));
api.PerformAsync(request);
}
private CancellationTokenSource cancellationToken;
private void onSuccess(GetNewsResponse response)
{
cancellationToken?.Cancel();
lastCursor = response.Cursor;
var flow = new FillFlowContainer<NewsCard>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10),
Children = response.NewsPosts.Select(p => new NewsCard(p)).ToList()
};
LoadComponentAsync(flow, loaded =>
{
content.Add(loaded);
showMore.IsLoading = false;
showMore.Show();
}, (cancellationToken = new CancellationTokenSource()).Token);
}
protected override void Dispose(bool isDisposing)
{
request?.Cancel();
cancellationToken?.Cancel();
base.Dispose(isDisposing);
}
}
}
-174
View File
@@ -1,174 +0,0 @@
// 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.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
namespace osu.Game.Overlays.News
{
public class NewsArticleCover : Container
{
private const int hover_duration = 300;
private readonly Box gradient;
public NewsArticleCover(ArticleInfo info)
{
RelativeSizeAxes = Axes.X;
Masking = true;
CornerRadius = 4;
NewsBackground bg;
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(OsuColour.Gray(0.2f), OsuColour.Gray(0.1f))
},
new DelayedLoadWrapper(bg = new NewsBackground(info.CoverUrl)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
Alpha = 0
})
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
},
gradient = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.1f), Color4.Black.Opacity(0.7f)),
Alpha = 0
},
new DateContainer(info.Time)
{
Margin = new MarginPadding
{
Right = 20,
Top = 20,
}
},
new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding
{
Left = 25,
Bottom = 50,
},
Font = OsuFont.GetFont(Typeface.Torus, 24, FontWeight.Bold),
Text = info.Title,
},
new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding
{
Left = 25,
Bottom = 30,
},
Font = OsuFont.GetFont(Typeface.Torus, 16, FontWeight.Bold),
Text = "by " + info.Author
}
};
bg.OnLoadComplete += d => d.FadeIn(250, Easing.In);
}
protected override bool OnHover(HoverEvent e)
{
gradient.FadeIn(hover_duration, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
gradient.FadeOut(hover_duration, Easing.OutQuint);
}
[LongRunningLoad]
private class NewsBackground : Sprite
{
private readonly string url;
public NewsBackground(string coverUrl)
{
url = coverUrl ?? "Headers/news";
}
[BackgroundDependencyLoader]
private void load(LargeTextureStore store)
{
Texture = store.Get(url);
}
}
private class DateContainer : Container, IHasTooltip
{
private readonly DateTime date;
public DateContainer(DateTime date)
{
this.date = date;
Anchor = Anchor.TopRight;
Origin = Anchor.TopRight;
Masking = true;
CornerRadius = 4;
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(0.5f),
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(Typeface.Torus, 12, FontWeight.Bold, false, false),
Text = date.ToString("d MMM yyy").ToUpper(),
Margin = new MarginPadding
{
Vertical = 4,
Horizontal = 8,
}
}
};
}
public string TooltipText => date.ToString("dddd dd MMMM yyyy hh:mm:ss UTCz").ToUpper();
}
// fake API data struct to use for now as a skeleton for data, as there is no API struct for news article info for now
public class ArticleInfo
{
public string Title { get; set; }
public string CoverUrl { get; set; }
public DateTime Time { get; set; }
public string Author { get; set; }
}
}
}
+25 -58
View File
@@ -2,26 +2,25 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
using osu.Framework.Platform;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Overlays.News
{
public class NewsCard : CompositeDrawable
public class NewsCard : OsuHoverContainer
{
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
protected override IEnumerable<Drawable> EffectTargets => new[] { background };
private readonly APINewsPost post;
@@ -31,24 +30,28 @@ namespace osu.Game.Overlays.News
public NewsCard(APINewsPost post)
{
this.post = post;
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Masking = true;
CornerRadius = 6;
}
NewsBackground bg;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, GameHost host)
{
if (post.Slug != null)
{
TooltipText = "view in browser";
Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug);
}
InternalChildren = new Drawable[]
NewsPostBackground bg;
AddRange(new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4
RelativeSizeAxes = Axes.Both
},
new FillFlowContainer
{
@@ -65,7 +68,7 @@ namespace osu.Game.Overlays.News
CornerRadius = 6,
Children = new Drawable[]
{
new DelayedLoadWrapper(bg = new NewsBackground(post.FirstImage)
new DelayedLoadWrapper(bg = new NewsPostBackground(post.FirstImage)
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
@@ -104,9 +107,11 @@ namespace osu.Game.Overlays.News
}
}
}
},
new HoverClickSounds()
};
}
});
IdleColour = colourProvider.Background4;
HoverColour = colourProvider.Background3;
bg.OnLoadComplete += d => d.FadeIn(250, Easing.In);
@@ -116,46 +121,6 @@ namespace osu.Game.Overlays.News
main.AddText(post.Author, t => t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold));
}
protected override bool OnHover(HoverEvent e)
{
background.FadeColour(colourProvider.Background3, 200, Easing.OutQuint);
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
background.FadeColour(colourProvider.Background4, 200, Easing.OutQuint);
base.OnHoverLost(e);
}
[LongRunningLoad]
private class NewsBackground : Sprite
{
private readonly string sourceUrl;
public NewsBackground(string sourceUrl)
{
this.sourceUrl = sourceUrl;
}
[BackgroundDependencyLoader]
private void load(LargeTextureStore store)
{
Texture = store.Get(createUrl(sourceUrl));
}
private string createUrl(string source)
{
if (string.IsNullOrEmpty(source))
return "Headers/news";
if (source.StartsWith('/'))
return "https://osu.ppy.sh" + source;
return source;
}
}
private class DateContainer : CircularContainer, IHasCustomTooltip
{
public ITooltip GetCustomTooltip() => new DateTooltip();
@@ -193,6 +158,8 @@ namespace osu.Game.Overlays.News
}
};
}
protected override bool OnClick(ClickEvent e) => true; // Protects the NewsCard from clicks while hovering DateContainer
}
}
}
-19
View File
@@ -1,19 +0,0 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Overlays.News
{
public abstract class NewsContent : FillFlowContainer
{
protected NewsContent()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical;
Padding = new MarginPadding { Bottom = 100, Top = 20, Horizontal = 50 };
}
}
}
+11 -7
View File
@@ -1,9 +1,9 @@
// 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.Bindables;
using osu.Framework.Graphics;
using System;
namespace osu.Game.Overlays.News
{
@@ -11,24 +11,28 @@ namespace osu.Game.Overlays.News
{
private const string front_page_string = "frontpage";
public readonly Bindable<string> Post = new Bindable<string>(null);
public Action ShowFrontPage;
private readonly Bindable<string> article = new Bindable<string>(null);
public NewsHeader()
{
TabControl.AddItem(front_page_string);
Current.ValueChanged += e =>
Current.BindValueChanged(e =>
{
if (e.NewValue == front_page_string)
ShowFrontPage?.Invoke();
};
});
Post.ValueChanged += showPost;
article.BindValueChanged(onArticleChanged, true);
}
private void showPost(ValueChangedEvent<string> e)
public void SetFrontPage() => article.Value = null;
public void SetArticle(string slug) => article.Value = slug;
private void onArticleChanged(ValueChangedEvent<string> e)
{
if (e.OldValue != null)
TabControl.RemoveItem(e.OldValue);
@@ -0,0 +1,37 @@
// 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.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
namespace osu.Game.Overlays.News
{
[LongRunningLoad]
public class NewsPostBackground : Sprite
{
private readonly string sourceUrl;
public NewsPostBackground(string sourceUrl)
{
this.sourceUrl = sourceUrl;
}
[BackgroundDependencyLoader]
private void load(LargeTextureStore store)
{
Texture = store.Get(createUrl(sourceUrl));
}
private string createUrl(string source)
{
if (string.IsNullOrEmpty(source))
return "Headers/news";
if (source.StartsWith('/'))
return "https://osu.ppy.sh" + source;
return source;
}
}
}
+75 -22
View File
@@ -7,18 +7,20 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.News;
using osu.Game.Overlays.News.Displays;
namespace osu.Game.Overlays
{
public class NewsOverlay : FullscreenOverlay
{
private readonly Bindable<string> article = new Bindable<string>(null);
private Container content;
private LoadingLayer loading;
private NewsHeader header;
private Container<NewsContent> content;
public readonly Bindable<string> Current = new Bindable<string>(null);
private OverlayScrollContainer scrollFlow;
public NewsOverlay()
: base(OverlayColourScheme.Purple)
@@ -26,18 +28,19 @@ namespace osu.Game.Overlays
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
private void load()
{
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.PurpleDarkAlternative
Colour = ColourProvider.Background5,
},
new OverlayScrollContainer
scrollFlow = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
@@ -49,7 +52,7 @@ namespace osu.Game.Overlays
{
ShowFrontPage = ShowFrontPage
},
content = new Container<NewsContent>
content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@@ -57,31 +60,81 @@ namespace osu.Game.Overlays
},
},
},
loading = new LoadingLayer(content),
};
header.Post.BindTo(Current);
Current.TriggerChange();
}
private CancellationTokenSource loadContentCancellation;
protected void LoadAndShowContent(NewsContent newContent)
protected override void LoadComplete()
{
content.FadeTo(0.2f, 300, Easing.OutQuint);
base.LoadComplete();
loadContentCancellation?.Cancel();
// should not be run until first pop-in to avoid requesting data before user views.
article.BindValueChanged(onArticleChanged);
}
LoadComponentAsync(newContent, c =>
private bool displayUpdateRequired = true;
protected override void PopIn()
{
base.PopIn();
if (displayUpdateRequired)
{
content.Child = c;
content.FadeIn(300, Easing.OutQuint);
}, (loadContentCancellation = new CancellationTokenSource()).Token);
article.TriggerChange();
displayUpdateRequired = false;
}
}
protected override void PopOutComplete()
{
base.PopOutComplete();
displayUpdateRequired = true;
}
public void ShowFrontPage()
{
Current.Value = null;
article.Value = null;
Show();
}
public void ShowArticle(string slug)
{
article.Value = slug;
Show();
}
private CancellationTokenSource cancellationToken;
private void onArticleChanged(ValueChangedEvent<string> e)
{
cancellationToken?.Cancel();
loading.Show();
if (e.NewValue == null)
{
header.SetFrontPage();
LoadDisplay(new FrontPageDisplay());
return;
}
header.SetArticle(e.NewValue);
LoadDisplay(Empty());
}
protected void LoadDisplay(Drawable display)
{
scrollFlow.ScrollToStart();
LoadComponentAsync(display, loaded =>
{
content.Child = loaded;
loading.Hide();
}, (cancellationToken = new CancellationTokenSource()).Token);
}
protected override void Dispose(bool isDisposing)
{
cancellationToken?.Cancel();
base.Dispose(isDisposing);
}
}
}
@@ -116,8 +116,6 @@ namespace osu.Game.Overlays.Settings.Sections
private class SkinDropdownControl : DropdownControl
{
protected override string GenerateItemText(SkinInfo item) => item.ToString();
protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 200);
}
}

Some files were not shown because too many files have changed in this diff Show More