1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-16 05:37:19 +08:00

Merge branch 'master' into mania-fix-hold-note-rewind

This commit is contained in:
Dean Herbert 2023-05-03 15:04:59 +09:00
commit 4013cdcfa8
298 changed files with 4251 additions and 1671 deletions

View File

@ -13,17 +13,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
# FIXME: Tools won't run in .NET 6.0 unless you install 3.1.x LTS side by side.
# https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e
- name: Install .NET 3.1.x LTS
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: "3.1.x"
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: "6.0.x"
@ -77,10 +77,10 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: "6.0.x"
@ -94,7 +94,7 @@ jobs:
# Attempt to upload results even if test fails.
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
- name: Upload Test Results
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: ${{ always() }}
with:
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
@ -106,10 +106,10 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: "6.0.x"
@ -125,10 +125,10 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: "6.0.x"

View File

@ -48,8 +48,8 @@ jobs:
CONTINUE="no"
fi
echo "::set-output name=continue::${CONTINUE}"
echo "::set-output name=matrix::${MATRIX_JSON}"
echo "continue=${CONTINUE}" >> $GITHUB_OUTPUT
echo "matrix=${MATRIX_JSON}" >> $GITHUB_OUTPUT
diffcalc:
name: Run
runs-on: self-hosted
@ -80,34 +80,34 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "::set-output name=branchname::$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.ref' | sed 's/\"//g')"
echo "::set-output name=repo::$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.repo.full_name' | sed 's/\"//g')"
echo "branchname=$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.ref' | sed 's/\"//g')" >> $GITHUB_OUTPUT
echo "repo=$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.repo.full_name' | sed 's/\"//g')" >> $GITHUB_OUTPUT
# Checkout osu
- name: Checkout osu (master)
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
path: 'master/osu'
- name: Checkout osu (pr)
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
path: 'pr/osu'
repository: ${{ steps.upstreambranch.outputs.repo }}
ref: ${{ steps.upstreambranch.outputs.branchname }}
- name: Checkout osu-difficulty-calculator (master)
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: ppy/osu-difficulty-calculator
path: 'master/osu-difficulty-calculator'
- name: Checkout osu-difficulty-calculator (pr)
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: ppy/osu-difficulty-calculator
path: 'pr/osu-difficulty-calculator'
- name: Install .NET 5.0.x
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: "5.0.x"

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0

View File

@ -1,7 +1,7 @@
<!-- Contains required properties for osu!framework projects. -->
<Project>
<PropertyGroup Label="C#">
<LangVersion>9.0</LangVersion>
<LangVersion>10.0</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
</PropertyGroup>

View File

@ -105,7 +105,7 @@ When it comes to contributing to the project, the two main things you can do to
If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web).
For those interested, we love to reward quality contributions via [bounties](https://docs.google.com/spreadsheets/d/1jNXfj_S3Pb5PErA-czDdC9DUu4IgUbe1Lt8E7CYUJuE/view?&rm=minimal#gid=523803337), paid out via PayPal or osu!supporter tags. Don't hesitate to [request a bounty](https://docs.google.com/forms/d/e/1FAIpQLSet_8iFAgPMG526pBZ2Kic6HSh7XPM3fE8xPcnWNkMzINDdYg/viewform) for your work on this project.
We love to reward quality contributions. If you have made a large contribution, or are a regular contributor, you are welcome to [submit an expense via opencollective](https://opencollective.com/ppy/expenses/new). If you have any questions, feel free to [reach out to peppy](mailto:pe@ppy.sh) before doing so.
## Licence

View File

@ -11,7 +11,7 @@
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.228.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.418.0" />
</ItemGroup>
<ItemGroup>
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Versioning;
using System.Threading.Tasks;
@ -139,7 +138,17 @@ namespace osu.Desktop
desktopWindow.CursorState |= CursorState.Hidden;
desktopWindow.Title = Name;
desktopWindow.DragDrop += f => fileDrop(new[] { f });
desktopWindow.DragDrop += f =>
{
// on macOS, URL associations are handled via SDL_DROPFILE events.
if (f.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal))
{
HandleLink(f);
return;
}
fileDrop(new[] { f });
};
}
protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo();
@ -151,10 +160,6 @@ namespace osu.Desktop
{
lock (importableFiles)
{
string firstExtension = Path.GetExtension(filePaths.First());
if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return;
importableFiles.AddRange(filePaths);
Logger.Log($"Adding {filePaths.Length} files for import");

View File

@ -1,17 +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.
#nullable disable
using Foundation;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Catch.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameAppDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}

View File

@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using UIKit;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Catch.Tests.iOS
{
@ -11,7 +10,7 @@ namespace osu.Game.Rulesets.Catch.Tests.iOS
{
public static void Main(string[] args)
{
UIApplication.Main(args, null, typeof(AppDelegate));
GameApplication.Main(new OsuTestBrowser());
}
}
}

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
AddAssert("default slider velocity", () => lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
AddAssert("default slider velocity", () => lastObject.SliderVelocityBindable.IsDefault);
}
[Test]
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addPlacementSteps(times, positions);
addPathCheckStep(times, positions);
AddAssert("slider velocity changed", () => !lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
AddAssert("slider velocity changed", () => !lastObject.SliderVelocityBindable.IsDefault);
}
[Test]

View File

@ -108,11 +108,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
double[] times = { 100, 300 };
float[] positions = { 200, 300 };
addBlueprintStep(times, positions);
AddAssert("default slider velocity", () => hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
AddAssert("default slider velocity", () => hitObject.SliderVelocityBindable.IsDefault);
addDragStartStep(times[1], positions[1]);
AddMouseMoveStep(times[1], 400);
AddAssert("slider velocity changed", () => !hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityBindable.IsDefault);
}
[Test]

View File

@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
var xPositionData = obj as IHasXPosition;
var yPositionData = obj as IHasYPosition;
var comboData = obj as IHasCombo;
var sliderVelocityData = obj as IHasSliderVelocity;
switch (obj)
{
@ -41,7 +42,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1
}.Yield();
case IHasDuration endTime:

View File

@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void UpdateHitObjectFromPath(JuiceStream hitObject)
{
// The SV setting may need to be changed for the current path.
var svBindable = hitObject.DifficultyControlPoint.SliderVelocityBindable;
var svBindable = hitObject.SliderVelocityBindable;
double svToVelocityFactor = hitObject.Velocity / svBindable.Value;
double requiredVelocity = path.ComputeRequiredVelocity();

View File

@ -48,7 +48,6 @@ namespace osu.Game.Rulesets.Catch.Edit
private void load()
{
// todo: enable distance spacing once catch supports applying it to its existing distance snap grid implementation.
RightSideToolboxContainer.Alpha = 0;
DistanceSpacingMultiplier.Disabled = true;
LayerBelowRuleset.Add(new PlayfieldBorder

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModDaycore : ModDaycore
{
public override double ScoreMultiplier => 0.3;
}
}

View File

@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 1,
MinValue = 0,
MaxValue = 10,
ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.CircleSize,
@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 1,
MinValue = 0,
MaxValue = 10,
ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.ApproachRate,

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModDoubleTime : ModDoubleTime
{
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
}
}

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModHalfTime : ModHalfTime
{
public override double ScoreMultiplier => 0.3;
}
}

View File

@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModNightcore : ModNightcore<CatchHitObject>
{
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
}
}

View File

@ -22,11 +22,11 @@ namespace osu.Game.Rulesets.Catch.Objects
public override Judgement CreateJudgement() => new CatchBananaJudgement();
private static readonly List<HitSampleInfo> samples = new List<HitSampleInfo> { new BananaHitSampleInfo() };
private static readonly IList<HitSampleInfo> default_banana_samples = new List<HitSampleInfo> { new BananaHitSampleInfo() }.AsReadOnly();
public Banana()
{
Samples = samples;
Samples = default_banana_samples;
}
// override any external colour changes with banananana
@ -47,13 +47,13 @@ namespace osu.Game.Rulesets.Catch.Objects
}
}
private class BananaHitSampleInfo : HitSampleInfo, IEquatable<BananaHitSampleInfo>
public class BananaHitSampleInfo : HitSampleInfo, IEquatable<BananaHitSampleInfo>
{
private static readonly string[] lookup_names = { "Gameplay/metronomelow", "Gameplay/catch-banana" };
public override IEnumerable<string> LookupNames => lookup_names;
public BananaHitSampleInfo(int volume = 0)
public BananaHitSampleInfo(int volume = 100)
: base(string.Empty, volume: volume)
{
}

View File

@ -1,7 +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.Collections.Generic;
using System.Threading;
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
@ -39,6 +41,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
StartTime = time,
BananaIndex = i,
Samples = new List<HitSampleInfo> { new Banana.BananaHitSampleInfo(GetSampleInfo().Volume) }
});
time += spacing;

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@ -16,7 +17,7 @@ using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects
{
public class JuiceStream : CatchHitObject, IHasPathWithRepeats
public class JuiceStream : CatchHitObject, IHasPathWithRepeats, IHasSliderVelocity
{
/// <summary>
/// Positional distance that results in a duration of one second, before any speed adjustments.
@ -27,6 +28,19 @@ namespace osu.Game.Rulesets.Catch.Objects
public int RepeatCount { get; set; }
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
{
Precision = 0.01,
MinValue = 0.1,
MaxValue = 10
};
public double SliderVelocity
{
get => SliderVelocityBindable.Value;
set => SliderVelocityBindable.Value = value;
}
[JsonIgnore]
private double velocityFactor;
@ -34,10 +48,10 @@ namespace osu.Game.Rulesets.Catch.Objects
private double tickDistanceFactor;
[JsonIgnore]
public double Velocity => velocityFactor * DifficultyControlPoint.SliderVelocity;
public double Velocity => velocityFactor * SliderVelocity;
[JsonIgnore]
public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity;
public double TickDistance => tickDistanceFactor * SliderVelocity;
/// <summary>
/// The length of one span of this <see cref="JuiceStream"/>.

View File

@ -136,6 +136,7 @@ namespace osu.Game.Rulesets.Catch.UI
Origin = Anchor.TopCentre;
Size = new Vector2(BASE_SIZE);
if (difficulty != null)
Scale = calculateScale(difficulty);
@ -333,8 +334,11 @@ namespace osu.Game.Rulesets.Catch.UI
base.Update();
var scaleFromDirection = new Vector2((int)VisualDirection, 1);
body.Scale = scaleFromDirection;
caughtObjectContainer.Scale = hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
// Inverse of catcher scale is applied here, as catcher gets scaled by circle size and so do the incoming fruit.
caughtObjectContainer.Scale = (1 / Scale.X) * (flipCatcherPlate ? scaleFromDirection : Vector2.One);
hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
// Correct overshooting.
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
@ -414,10 +418,13 @@ namespace osu.Game.Rulesets.Catch.UI
private void clearPlate(DroppedObjectAnimation animation)
{
var droppedObjects = caughtObjectContainer.Children.Select(getDroppedObject).ToArray();
var caughtObjects = caughtObjectContainer.Children.ToArray();
caughtObjectContainer.Clear(false);
// Use the already returned PoolableDrawables for new objects
var droppedObjects = caughtObjects.Select(getDroppedObject).ToArray();
droppedObjectTarget.AddRange(droppedObjects);
foreach (var droppedObject in droppedObjects)
@ -426,10 +433,10 @@ namespace osu.Game.Rulesets.Catch.UI
private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation)
{
var droppedObject = getDroppedObject(caughtObject);
caughtObjectContainer.Remove(caughtObject, false);
var droppedObject = getDroppedObject(caughtObject);
droppedObjectTarget.Add(droppedObject);
applyDropAnimation(droppedObject, animation);
@ -452,6 +459,8 @@ namespace osu.Game.Rulesets.Catch.UI
break;
}
// Define lifetime start for dropped objects to be disposed correctly when rewinding replay
d.LifetimeStart = Clock.CurrentTime;
d.Expire();
}

View File

@ -1,17 +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.
#nullable disable
using Foundation;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Mania.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameAppDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}

View File

@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using UIKit;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Mania.Tests.iOS
{
@ -11,7 +10,7 @@ namespace osu.Game.Rulesets.Mania.Tests.iOS
{
public static void Main(string[] args)
{
UIApplication.Main(args, null, typeof(AppDelegate));
GameApplication.Main(new OsuTestBrowser());
}
}
}

View File

@ -3,6 +3,9 @@
#nullable disable
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
@ -10,5 +13,19 @@ namespace osu.Game.Rulesets.Mania.Tests
public partial class TestSceneManiaPlayer : PlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("change direction to down", () => changeDirectionTo(ManiaScrollingDirection.Down));
AddStep("change direction to up", () => changeDirectionTo(ManiaScrollingDirection.Up));
}
private void changeDirectionTo(ManiaScrollingDirection direction)
{
var rulesetConfig = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(new ManiaRuleset()).AsNonNull();
rulesetConfig.SetValue(ManiaRulesetSetting.ScrollDirection, direction);
}
}
}

View File

@ -14,7 +14,6 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
@ -49,15 +48,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
Debug.Assert(distanceData != null);
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
DifficultyControlPoint difficultyPoint = hitObject.DifficultyControlPoint;
double beatLength;
#pragma warning disable 618
if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint)
#pragma warning restore 618
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
if (hitObject.LegacyBpmMultiplier.HasValue)
beatLength = timingPoint.BeatLength * hitObject.LegacyBpmMultiplier.Value;
else if (hitObject is IHasSliderVelocity hasSliderVelocity)
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
else
beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity;
beatLength = timingPoint.BeatLength;
SpanCount = repeatsData?.SpanCount() ?? 1;
StartTime = (int)Math.Round(hitObject.StartTime);

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModDaycore : ModDaycore
{
public override double ScoreMultiplier => 0.5;
}
}

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModDoubleTime : ModDoubleTime
{
public override double ScoreMultiplier => 1;
}
}

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModHalfTime : ModHalfTime
{
public override double ScoreMultiplier => 0.5;
}
}

View File

@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModNightcore : ModNightcore<ManiaHitObject>
{
public override double ScoreMultiplier => 1;
}
}

View File

@ -236,6 +236,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
};
// Position and resize the body to lie half-way under the head and the tail notes.
// The rationale for this is account for heads/tails with corner radius.
bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2;
bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2;
@ -354,13 +355,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
// Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being.
if (HitObject.SampleControlPoint == null)
{
throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
}
slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
slidingSample.Samples = HitObject.CreateSlidingSamples().Cast<ISampleInfo>().ToArray();
}
public override void StopAllSamples()

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail;
protected DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject;
protected internal DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject;
public DrawableHoldNoteTail()
: this(null)

View File

@ -10,7 +10,7 @@
["Gameplay/soft-hitnormal"],
["Gameplay/drum-hitnormal"]
],
"Samples": ["Gameplay/-hitnormal"]
"Samples": ["Gameplay/normal-hitnormal"]
}, {
"StartTime": 1875.0,
"EndTime": 2750.0,
@ -19,7 +19,7 @@
["Gameplay/soft-hitnormal"],
["Gameplay/drum-hitnormal"]
],
"Samples": ["Gameplay/-hitnormal"]
"Samples": ["Gameplay/normal-hitnormal"]
}]
}, {
"StartTime": 3750.0,

View File

@ -43,9 +43,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
largeFaint = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Height = ArgonNotePiece.NOTE_ACCENT_RATIO,
Masking = true,
CornerRadius = ArgonNotePiece.CORNER_RADIUS,
Blending = BlendingParameters.Additive,
@ -80,11 +79,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
if (direction.NewValue == ScrollingDirection.Up)
{
Anchor = Anchor.TopCentre;
largeFaint.Anchor = Anchor.TopCentre;
largeFaint.Origin = Anchor.TopCentre;
Y = ArgonNotePiece.NOTE_HEIGHT / 2;
}
else
{
Anchor = Anchor.BottomCentre;
largeFaint.Anchor = Anchor.BottomCentre;
largeFaint.Origin = Anchor.BottomCentre;
Y = -ArgonNotePiece.NOTE_HEIGHT / 2;
}
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
private void load(IScrollingInfo scrollingInfo)
{
RelativeSizeAxes = Axes.X;
Height = ArgonNotePiece.NOTE_HEIGHT;
Height = ArgonNotePiece.NOTE_HEIGHT * ArgonNotePiece.NOTE_ACCENT_RATIO;
Masking = true;
CornerRadius = ArgonNotePiece.CORNER_RADIUS;

View File

@ -20,10 +20,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
public partial class ArgonHoldBodyPiece : CompositeDrawable, IHoldNoteBody
{
protected readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
protected readonly IBindable<bool> IsHitting = new Bindable<bool>();
private Drawable background = null!;
private Box foreground = null!;
private ArgonHoldNoteHittingLayer hittingLayer = null!;
public ArgonHoldBodyPiece()
{
@ -32,7 +31,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
// Without this, the width of the body will be slightly larger than the head/tail.
Masking = true;
CornerRadius = ArgonNotePiece.CORNER_RADIUS;
Blending = BlendingParameters.Additive;
}
[BackgroundDependencyLoader(true)]
@ -41,12 +39,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
InternalChildren = new[]
{
background = new Box { RelativeSizeAxes = Axes.Both },
foreground = new Box
{
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Alpha = 0,
},
hittingLayer = new ArgonHoldNoteHittingLayer()
};
if (drawableObject != null)
@ -54,44 +47,19 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
var holdNote = (DrawableHoldNote)drawableObject;
AccentColour.BindTo(holdNote.AccentColour);
IsHitting.BindTo(holdNote.IsHitting);
hittingLayer.AccentColour.BindTo(holdNote.AccentColour);
((IBindable<bool>)hittingLayer.IsHitting).BindTo(holdNote.IsHitting);
}
AccentColour.BindValueChanged(colour =>
{
background.Colour = colour.NewValue.Darken(1.2f);
foreground.Colour = colour.NewValue.Opacity(0.2f);
background.Colour = colour.NewValue.Darken(0.6f);
}, true);
IsHitting.BindValueChanged(hitting =>
{
const float animation_length = 50;
foreground.ClearTransforms();
if (hitting.NewValue)
{
// wait for the next sync point
double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
using (foreground.BeginDelayedSequence(synchronisedOffset))
{
foreground.FadeTo(1, animation_length).Then()
.FadeTo(0.5f, animation_length)
.Loop();
}
}
else
{
foreground.FadeOut(animation_length);
}
});
}
public void Recycle()
{
foreground.ClearTransforms();
foreground.Alpha = 0;
hittingLayer.Recycle();
}
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
internal partial class ArgonHoldNoteHeadPiece : ArgonNotePiece
{
protected override Drawable CreateIcon() => new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = 2,
Size = new Vector2(20, 5),
};
}
}

View File

@ -0,0 +1,64 @@
// 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.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osuTK.Graphics;
using Box = osu.Framework.Graphics.Shapes.Box;
namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
public partial class ArgonHoldNoteHittingLayer : Box
{
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
public readonly Bindable<bool> IsHitting = new Bindable<bool>();
public ArgonHoldNoteHittingLayer()
{
RelativeSizeAxes = Axes.Both;
Blending = BlendingParameters.Additive;
Alpha = 0;
}
protected override void LoadComplete()
{
base.LoadComplete();
AccentColour.BindValueChanged(colour =>
{
Colour = colour.NewValue.Lighten(0.2f).Opacity(0.3f);
}, true);
IsHitting.BindValueChanged(hitting =>
{
const float animation_length = 80;
ClearTransforms();
if (hitting.NewValue)
{
// wait for the next sync point
double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
using (BeginDelayedSequence(synchronisedOffset))
{
this.FadeTo(1, animation_length, Easing.OutSine).Then()
.FadeTo(0.5f, animation_length, Easing.InSine)
.Loop();
}
}
else
{
this.FadeOut(animation_length);
}
}, true);
}
public void Recycle()
{
ClearTransforms();
Alpha = 0;
}
}
}

View File

@ -5,8 +5,10 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
@ -16,47 +18,68 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
internal partial class ArgonHoldNoteTailPiece : CompositeDrawable
{
[Resolved]
private DrawableHitObject? drawableObject { get; set; }
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
private readonly Box shadeBackground;
private readonly Box shadeForeground;
private readonly Box foreground;
private readonly ArgonHoldNoteHittingLayer hittingLayer;
private readonly Box foregroundAdditive;
public ArgonHoldNoteTailPiece()
{
RelativeSizeAxes = Axes.X;
Height = ArgonNotePiece.NOTE_HEIGHT;
CornerRadius = ArgonNotePiece.CORNER_RADIUS;
Masking = true;
InternalChildren = new Drawable[]
{
shadeBackground = new Box
{
RelativeSizeAxes = Axes.Both,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Height = ArgonNotePiece.NOTE_ACCENT_RATIO,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.X,
Height = ArgonNotePiece.NOTE_HEIGHT,
CornerRadius = ArgonNotePiece.CORNER_RADIUS,
Masking = true,
Children = new Drawable[]
{
shadeForeground = new Box
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0), Colour4.Black),
// Avoid ugly single pixel overlap.
Height = 0.9f,
},
},
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Height = ArgonNotePiece.NOTE_ACCENT_RATIO,
CornerRadius = ArgonNotePiece.CORNER_RADIUS,
Masking = true,
Children = new Drawable[]
{
foreground = new Box
{
RelativeSizeAxes = Axes.Both,
},
hittingLayer = new ArgonHoldNoteHittingLayer(),
foregroundAdditive = new Box
{
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Height = 0.5f,
},
},
},
}
},
};
}
[BackgroundDependencyLoader(true)]
private void load(IScrollingInfo scrollingInfo, DrawableHitObject? drawableObject)
private void load(IScrollingInfo scrollingInfo)
{
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
@ -65,9 +88,24 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
accentColour.BindTo(drawableObject.AccentColour);
accentColour.BindValueChanged(onAccentChanged, true);
drawableObject.HitObjectApplied += hitObjectApplied;
}
}
private void hitObjectApplied(DrawableHitObject drawableHitObject)
{
var holdNoteTail = (DrawableHoldNoteTail)drawableHitObject;
hittingLayer.Recycle();
hittingLayer.AccentColour.UnbindBindings();
hittingLayer.AccentColour.BindTo(holdNoteTail.HoldNote.AccentColour);
hittingLayer.IsHitting.UnbindBindings();
((IBindable<bool>)hittingLayer.IsHitting).BindTo(holdNoteTail.HoldNote.IsHitting);
}
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
{
Scale = new Vector2(1, direction.NewValue == ScrollingDirection.Up ? -1 : 1);
@ -75,8 +113,20 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
private void onAccentChanged(ValueChangedEvent<Color4> accent)
{
shadeBackground.Colour = accent.NewValue.Darken(1.7f);
shadeForeground.Colour = accent.NewValue.Darken(1.1f);
foreground.Colour = accent.NewValue.Darken(0.6f); // matches body
foregroundAdditive.Colour = ColourInfo.GradientVertical(
accent.NewValue.Opacity(0.4f),
accent.NewValue.Opacity(0)
);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableObject != null)
drawableObject.HitObjectApplied -= hitObjectApplied;
}
}
}

View File

@ -26,7 +26,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
private readonly Box colouredBox;
private readonly Box shadow;
public ArgonNotePiece()
{
@ -36,11 +35,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
CornerRadius = CORNER_RADIUS;
Masking = true;
InternalChildren = new Drawable[]
InternalChildren = new[]
{
shadow = new Box
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0), Colour4.Black)
},
new Container
{
@ -65,18 +65,22 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
RelativeSizeAxes = Axes.X,
Height = CORNER_RADIUS * 2,
},
new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = 4,
Icon = FontAwesome.Solid.AngleDown,
Size = new Vector2(20),
Scale = new Vector2(1, 0.7f)
}
CreateIcon(),
};
}
protected virtual Drawable CreateIcon() => new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = 4,
// TODO: replace with a non-squashed version.
// The 0.7f height scale should be removed.
Icon = FontAwesome.Solid.AngleDown,
Size = new Vector2(20),
Scale = new Vector2(1, 0.7f)
};
[BackgroundDependencyLoader(true)]
private void load(IScrollingInfo scrollingInfo, DrawableHitObject? drawableObject)
{
@ -105,8 +109,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
accent.NewValue.Lighten(0.1f),
accent.NewValue
);
shadow.Colour = accent.NewValue.Darken(0.5f);
}
}
}

View File

@ -50,6 +50,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
return new ArgonHoldNoteTailPiece();
case ManiaSkinComponents.HoldNoteHead:
return new ArgonHoldNoteHeadPiece();
case ManiaSkinComponents.Note:
return new ArgonNotePiece();
@ -69,12 +71,23 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
return base.GetDrawableComponent(lookup);
}
private static readonly Color4 colour_special_column = new Color4(169, 106, 255, 255);
private const int total_colours = 6;
private static readonly Color4 colour_yellow = new Color4(255, 197, 40, 255);
private static readonly Color4 colour_orange = new Color4(252, 109, 1, 255);
private static readonly Color4 colour_pink = new Color4(213, 35, 90, 255);
private static readonly Color4 colour_purple = new Color4(203, 60, 236, 255);
private static readonly Color4 colour_cyan = new Color4(72, 198, 255, 255);
private static readonly Color4 colour_green = new Color4(100, 192, 92, 255);
public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
{
if (lookup is ManiaSkinConfigurationLookup maniaLookup)
{
int column = maniaLookup.ColumnIndex ?? 0;
var stage = beatmap.GetStageForColumnIndex(column);
int columnIndex = maniaLookup.ColumnIndex ?? 0;
var stage = beatmap.GetStageForColumnIndex(columnIndex);
switch (maniaLookup.Lookup)
{
@ -87,53 +100,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case LegacyManiaSkinConfigurationLookups.ColumnWidth:
return SkinUtils.As<TValue>(new Bindable<float>(
stage.IsSpecialColumn(column) ? 120 : 60
stage.IsSpecialColumn(columnIndex) ? 120 : 60
));
case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:
Color4 colour;
const int total_colours = 7;
if (stage.IsSpecialColumn(column))
colour = new Color4(159, 101, 255, 255);
else
{
switch (column % total_colours)
{
case 0:
colour = new Color4(240, 216, 0, 255);
break;
case 1:
colour = new Color4(240, 101, 0, 255);
break;
case 2:
colour = new Color4(240, 0, 130, 255);
break;
case 3:
colour = new Color4(192, 0, 240, 255);
break;
case 4:
colour = new Color4(0, 96, 240, 255);
break;
case 5:
colour = new Color4(0, 226, 240, 255);
break;
case 6:
colour = new Color4(0, 240, 96, 255);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
var colour = getColourForLayout(columnIndex, stage);
return SkinUtils.As<TValue>(new Bindable<Color4>(colour));
}
@ -141,5 +113,203 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
return base.GetConfig<TLookup, TValue>(lookup);
}
private Color4 getColourForLayout(int columnIndex, StageDefinition stage)
{
// Account for cases like dual-stage (assume that all stages have the same column count for now).
columnIndex %= stage.Columns;
// For now, these are defined per column count as per https://user-images.githubusercontent.com/50823728/218038463-b450f46c-ef21-4551-b133-f866be59970c.png
// See https://github.com/ppy/osu/discussions/21996 for discussion.
switch (stage.Columns)
{
case 1:
return colour_yellow;
case 2:
switch (columnIndex)
{
case 0: return colour_green;
case 1: return colour_cyan;
default: throw new ArgumentOutOfRangeException();
}
case 3:
switch (columnIndex)
{
case 0: return colour_pink;
case 1: return colour_orange;
case 2: return colour_yellow;
default: throw new ArgumentOutOfRangeException();
}
case 4:
switch (columnIndex)
{
case 0: return colour_yellow;
case 1: return colour_orange;
case 2: return colour_pink;
case 3: return colour_purple;
default: throw new ArgumentOutOfRangeException();
}
case 5:
switch (columnIndex)
{
case 0: return colour_pink;
case 1: return colour_orange;
case 2: return colour_yellow;
case 3: return colour_green;
case 4: return colour_cyan;
default: throw new ArgumentOutOfRangeException();
}
case 6:
switch (columnIndex)
{
case 0: return colour_pink;
case 1: return colour_orange;
case 2: return colour_yellow;
case 3: return colour_cyan;
case 4: return colour_purple;
case 5: return colour_pink;
default: throw new ArgumentOutOfRangeException();
}
case 7:
switch (columnIndex)
{
case 0: return colour_pink;
case 1: return colour_cyan;
case 2: return colour_pink;
case 3: return colour_special_column;
case 4: return colour_green;
case 5: return colour_cyan;
case 6: return colour_green;
default: throw new ArgumentOutOfRangeException();
}
case 8:
switch (columnIndex)
{
case 0: return colour_purple;
case 1: return colour_pink;
case 2: return colour_orange;
case 3: return colour_yellow;
case 4: return colour_yellow;
case 5: return colour_orange;
case 6: return colour_pink;
case 7: return colour_purple;
default: throw new ArgumentOutOfRangeException();
}
case 9:
switch (columnIndex)
{
case 0: return colour_purple;
case 1: return colour_pink;
case 2: return colour_orange;
case 3: return colour_yellow;
case 4: return colour_special_column;
case 5: return colour_yellow;
case 6: return colour_orange;
case 7: return colour_pink;
case 8: return colour_purple;
default: throw new ArgumentOutOfRangeException();
}
case 10:
switch (columnIndex)
{
case 0: return colour_purple;
case 1: return colour_pink;
case 2: return colour_orange;
case 3: return colour_yellow;
case 4: return colour_cyan;
case 5: return colour_green;
case 6: return colour_yellow;
case 7: return colour_orange;
case 8: return colour_pink;
case 9: return colour_purple;
default: throw new ArgumentOutOfRangeException();
}
}
// fallback for unhandled scenarios
if (stage.IsSpecialColumn(columnIndex))
return colour_special_column;
switch (columnIndex % total_colours)
{
case 0: return colour_yellow;
case 1: return colour_orange;
case 2: return colour_pink;
case 3: return colour_purple;
case 4: return colour_cyan;
case 5: return colour_green;
default: throw new ArgumentOutOfRangeException();
}
}
}
}

View File

@ -1,17 +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.
#nullable disable
using Foundation;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Osu.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameAppDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}

View File

@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using UIKit;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Osu.Tests.iOS
{
@ -11,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Tests.iOS
{
public static void Main(string[] args)
{
UIApplication.Main(args, null, typeof(AppDelegate));
GameApplication.Main(new OsuTestBrowser());
}
}
}

View File

@ -138,8 +138,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples)
&& mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples)
&& mergedSlider.Samples.SequenceEqual(slider1.Samples)
&& mergedSlider.SampleControlPoint.IsRedundant(slider1.SampleControlPoint);
&& mergedSlider.Samples.SequenceEqual(slider1.Samples);
});
AddAssert("slider end is at same completion for last slider", () =>

View File

@ -181,10 +181,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
if (slider is null) return;
slider.SampleControlPoint.SampleBank = "soft";
slider.SampleControlPoint.SampleVolume = 70;
sample = new HitSampleInfo("hitwhistle");
slider.Samples.Add(sample);
sample = new HitSampleInfo("hitwhistle", "soft", volume: 70);
slider.Samples.Add(sample.With());
});
AddStep("select added slider", () =>
@ -207,9 +205,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("sliders have hitsounds", hasHitsounds);
bool hasHitsounds() => sample is not null &&
EditorBeatmap.HitObjects.All(o => o.SampleControlPoint.SampleBank == "soft" &&
o.SampleControlPoint.SampleVolume == 70 &&
o.Samples.Contains(sample));
EditorBeatmap.HitObjects.All(o => o.Samples.Contains(sample));
}
private bool sliderCreatedFor(Slider s, double startTime, double endTime, params (Vector2 pos, PathType? pathType)[] expectedControlPoints)

View File

@ -199,8 +199,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
Precision.AlmostEquals(circle.StartTime, time, 1)
&& Precision.AlmostEquals(circle.Position, position, 0.01f)
&& circle.NewCombo == startsNewCombo
&& circle.Samples.SequenceEqual(slider.HeadCircle.Samples)
&& circle.SampleControlPoint.IsRedundant(slider.SampleControlPoint);
&& circle.Samples.SequenceEqual(slider.HeadCircle.Samples);
}
private bool sliderRestored(Slider slider)

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModBubbles : OsuModTestScene
{
[Test]
public void TestOsuModBubbles() => CreateModTest(new ModTestData
{
Mod = new OsuModBubbles(),
Autoplay = true,
PassCondition = () => true
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -18,6 +19,7 @@ using osu.Framework.Testing.Input;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play;
@ -40,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable background;
private readonly Bindable<bool> ripples = new Bindable<bool>();
public TestSceneGameplayCursor()
{
var ruleset = new OsuRuleset();
@ -57,6 +61,8 @@ namespace osu.Game.Rulesets.Osu.Tests
});
});
AddToggleStep("ripples", v => ripples.Value = v);
AddSliderStep("circle size", 0f, 10f, 0f, val =>
{
config.SetValue(OsuSetting.AutoCursorSize, true);
@ -67,6 +73,13 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("test cursor container", () => loadContent(false));
}
[BackgroundDependencyLoader]
private void load()
{
var rulesetConfig = (OsuRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
rulesetConfig.BindWith(OsuRulesetSetting.ShowCursorRipples, ripples);
}
[TestCase(1, 1)]
[TestCase(5, 1)]
[TestCase(10, 1)]

View File

@ -439,7 +439,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public TestSlider()
{
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
SliderVelocity = 0.1f;
DefaultsApplied += _ =>
{

View File

@ -21,7 +21,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Graphics;
@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Osu.Tests
[Resolved]
private OsuConfigManager config { get; set; } = null!;
private TestActionKeyCounter leftKeyCounter = null!;
private DefaultKeyCounter leftKeyCounter = null!;
private TestActionKeyCounter rightKeyCounter = null!;
private DefaultKeyCounter rightKeyCounter = null!;
private OsuInputManager osuInputManager = null!;
@ -59,14 +59,14 @@ namespace osu.Game.Rulesets.Osu.Tests
Origin = Anchor.Centre,
Children = new Drawable[]
{
leftKeyCounter = new TestActionKeyCounter(OsuAction.LeftButton)
leftKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.LeftButton))
{
Anchor = Anchor.Centre,
Origin = Anchor.CentreRight,
Depth = float.MinValue,
X = -100,
},
rightKeyCounter = new TestActionKeyCounter(OsuAction.RightButton)
rightKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.RightButton))
{
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
@ -150,6 +150,42 @@ namespace osu.Game.Rulesets.Osu.Tests
assertKeyCounter(1, 1);
}
[Test]
public void TestPositionalTrackingAfterLongDistanceTravelled()
{
// When a single touch has already travelled enough distance on screen, it should remain as the positional
// tracking touch until released (unless a direct touch occurs).
beginTouch(TouchSource.Touch1);
assertKeyCounter(1, 0);
checkPressed(OsuAction.LeftButton);
checkPosition(TouchSource.Touch1);
// cover some distance
beginTouch(TouchSource.Touch1, new Vector2(0));
beginTouch(TouchSource.Touch1, new Vector2(9999));
beginTouch(TouchSource.Touch1, new Vector2(0));
beginTouch(TouchSource.Touch1, new Vector2(9999));
beginTouch(TouchSource.Touch1);
beginTouch(TouchSource.Touch2);
assertKeyCounter(1, 1);
checkNotPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
// in this case, touch 2 should not become the positional tracking touch.
checkPosition(TouchSource.Touch1);
// even if the second touch moves on the screen, the original tracking touch is retained.
beginTouch(TouchSource.Touch2, new Vector2(0));
beginTouch(TouchSource.Touch2, new Vector2(9999));
beginTouch(TouchSource.Touch2, new Vector2(0));
beginTouch(TouchSource.Touch2, new Vector2(9999));
checkPosition(TouchSource.Touch1);
}
[Test]
public void TestPositionalInputUpdatesOnlyFromMostRecentTouch()
{
@ -562,8 +598,8 @@ namespace osu.Game.Rulesets.Osu.Tests
private void assertKeyCounter(int left, int right)
{
AddAssert($"The left key was pressed {left} times", () => leftKeyCounter.CountPresses, () => Is.EqualTo(left));
AddAssert($"The right key was pressed {right} times", () => rightKeyCounter.CountPresses, () => Is.EqualTo(right));
AddAssert($"The left key was pressed {left} times", () => leftKeyCounter.CountPresses.Value, () => Is.EqualTo(left));
AddAssert($"The right key was pressed {right} times", () => rightKeyCounter.CountPresses.Value, () => Is.EqualTo(right));
}
private void releaseAllTouches()
@ -579,11 +615,11 @@ namespace osu.Game.Rulesets.Osu.Tests
private void checkNotPressed(OsuAction action) => AddAssert($"Not pressing {action}", () => !osuInputManager.PressedActions.Contains(action));
private void checkPressed(OsuAction action) => AddAssert($"Is pressing {action}", () => osuInputManager.PressedActions.Contains(action));
public partial class TestActionKeyCounter : KeyCounter, IKeyBindingHandler<OsuAction>
public partial class TestActionKeyCounterTrigger : InputTrigger, IKeyBindingHandler<OsuAction>
{
public OsuAction Action { get; }
public TestActionKeyCounter(OsuAction action)
public TestActionKeyCounterTrigger(OsuAction action)
: base(action.ToString())
{
Action = action;
@ -593,8 +629,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
if (e.Action == Action)
{
IsLit = true;
Increment();
Activate();
}
return false;
@ -602,7 +637,8 @@ namespace osu.Game.Rulesets.Osu.Tests
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
if (e.Action == Action) IsLit = false;
if (e.Action == Action)
Deactivate();
}
}

View File

@ -7,7 +7,6 @@ using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
@ -47,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider_start,
Position = new Vector2(0, 0),
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = velocity },
SliderVelocity = velocity,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,

View File

@ -8,7 +8,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
@ -350,7 +349,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider_start,
Position = new Vector2(0, 0),
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f },
SliderVelocity = 0.1f,
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,

View File

@ -399,7 +399,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public TestSlider()
{
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
SliderVelocity = 0.1f;
DefaultsApplied += _ =>
{

View File

@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{
var positionData = original as IHasPosition;
var comboData = original as IHasCombo;
var sliderVelocityData = original as IHasSliderVelocity;
var generateTicksData = original as IHasGenerateTicks;
switch (original)
{
@ -47,7 +49,9 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
GenerateTicks = generateTicksData?.GenerateTicks ?? true,
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1,
}.Yield();
case IHasDuration endTimeData:

View File

@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
SetDefault(OsuRulesetSetting.SnakingInSliders, true);
SetDefault(OsuRulesetSetting.SnakingOutSliders, true);
SetDefault(OsuRulesetSetting.ShowCursorTrail, true);
SetDefault(OsuRulesetSetting.ShowCursorRipples, false);
SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None);
}
}
@ -31,6 +32,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
SnakingInSliders,
SnakingOutSliders,
ShowCursorTrail,
ShowCursorRipples,
PlayfieldBorderStyle,
}
}

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
protected override bool AlwaysShowWhenSelected => true;
protected override bool ShouldBeAlive => base.ShouldBeAlive
|| (ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION);
|| (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION);
protected OsuSelectionBlueprint(T hitObject)
: base(hitObject)

View File

@ -10,7 +10,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@ -83,11 +82,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
case SliderPlacementState.Initial:
BeginPlacement();
var nearestDifficultyPoint = editorBeatmap.HitObjects
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime)?
.DifficultyControlPoint?.DeepClone() as DifficultyControlPoint;
double? nearestSliderVelocity = (editorBeatmap.HitObjects
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocity;
HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint();
HitObject.SliderVelocity = nearestSliderVelocity ?? 1;
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
// Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation.

View File

@ -14,7 +14,6 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
@ -311,17 +310,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
var splitControlPoints = controlPoints.Take(index + 1).ToList();
controlPoints.RemoveRange(0, index);
// Turn the control points which were split off into a new slider.
var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone();
var difficultyPoint = (DifficultyControlPoint)HitObject.DifficultyControlPoint.DeepClone();
var newSlider = new Slider
{
StartTime = HitObject.StartTime,
Position = HitObject.Position + splitControlPoints[0].Position,
NewCombo = HitObject.NewCombo,
SampleControlPoint = samplePoint,
DifficultyControlPoint = difficultyPoint,
LegacyLastTickOffset = HitObject.LegacyLastTickOffset,
Samples = HitObject.Samples.Select(s => s.With()).ToList(),
RepeatCount = HitObject.RepeatCount,
@ -378,15 +371,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition);
var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone();
samplePoint.Time = time;
editorBeatmap.Add(new HitCircle
{
StartTime = time,
Position = position,
NewCombo = i == 0 && HitObject.NewCombo,
SampleControlPoint = samplePoint,
Samples = HitObject.HeadCircle.Samples.Select(s => s.With()).ToList()
});

View File

@ -13,8 +13,8 @@ using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
@ -62,7 +62,12 @@ namespace osu.Game.Rulesets.Osu.Edit
private void load()
{
// Give a bit of breathing room around the playfield content.
PlayfieldContentContainer.Padding = new MarginPadding(10);
PlayfieldContentContainer.Padding = new MarginPadding
{
Vertical = 10,
Left = TOOLBOX_CONTRACTED_SIZE_LEFT + 10,
Right = TOOLBOX_CONTRACTED_SIZE_RIGHT + 10,
};
LayerBelowRuleset.AddRange(new Drawable[]
{

View File

@ -362,7 +362,6 @@ namespace osu.Game.Rulesets.Osu.Edit
StartTime = firstHitObject.StartTime,
Position = firstHitObject.Position,
NewCombo = firstHitObject.NewCombo,
SampleControlPoint = firstHitObject.SampleControlPoint,
Samples = firstHitObject.Samples,
};

View File

@ -1,6 +1,7 @@
// 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.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@ -10,6 +11,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModBarrelRoll : ModBarrelRoll<OsuHitObject>, IApplicableToDrawableHitObject
{
public override Type[] IncompatibleMods => new[] { typeof(OsuModBubbles) };
public void ApplyToDrawableHitObject(DrawableHitObject d)
{
d.OnUpdate += _ =>

View File

@ -0,0 +1,214 @@
// 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.Diagnostics;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
public partial class OsuModBubbles : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToDrawableHitObject, IApplicableToScoreProcessor
{
public override string Name => "Bubbles";
public override string Acronym => "BU";
public override LocalisableString Description => "Don't let their popping distract you!";
public override double ScoreMultiplier => 1;
public override ModType Type => ModType.Fun;
// Compatibility with these seems potentially feasible in the future, blocked for now because they don't work as one would expect
public override Type[] IncompatibleMods => new[] { typeof(OsuModBarrelRoll), typeof(OsuModMagnetised), typeof(OsuModRepel) };
private PlayfieldAdjustmentContainer bubbleContainer = null!;
private readonly Bindable<int> currentCombo = new BindableInt();
private float maxSize;
private float bubbleSize;
private double bubbleFade;
private readonly DrawablePool<BubbleDrawable> bubblePool = new DrawablePool<BubbleDrawable>(100);
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
{
currentCombo.BindTo(scoreProcessor.Combo);
currentCombo.BindValueChanged(combo =>
maxSize = Math.Min(1.75f, (float)(1.25 + 0.005 * combo.NewValue)), true);
}
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
// Multiplying by 2 results in an initial size that is too large, hence 1.90 has been chosen
// Also avoids the HitObject bleeding around the edges of the bubble drawable at minimum size
bubbleSize = (float)(drawableRuleset.Beatmap.HitObjects.OfType<HitCircle>().First().Radius * 1.90f);
bubbleFade = drawableRuleset.Beatmap.HitObjects.OfType<HitCircle>().First().TimePreempt * 2;
// We want to hide the judgements since they are obscured by the BubbleDrawable (due to layering)
drawableRuleset.Playfield.DisplayJudgements.Value = false;
bubbleContainer = drawableRuleset.CreatePlayfieldAdjustmentContainer();
drawableRuleset.Overlays.Add(bubbleContainer);
}
public void ApplyToDrawableHitObject(DrawableHitObject drawableObject)
{
drawableObject.OnNewResult += (drawable, _) =>
{
if (drawable is not DrawableOsuHitObject drawableOsuHitObject) return;
switch (drawableOsuHitObject.HitObject)
{
case Slider:
case SpinnerTick:
break;
default:
addBubble();
break;
}
void addBubble()
{
BubbleDrawable bubble = bubblePool.Get();
bubble.DrawableOsuHitObject = drawableOsuHitObject;
bubble.InitialSize = new Vector2(bubbleSize);
bubble.FadeTime = bubbleFade;
bubble.MaxSize = maxSize;
bubbleContainer.Add(bubble);
}
};
drawableObject.OnRevertResult += (drawable, _) =>
{
if (drawable.HitObject is SpinnerTick or Slider) return;
BubbleDrawable? lastBubble = bubbleContainer.OfType<BubbleDrawable>().LastOrDefault();
lastBubble?.ClearTransforms();
lastBubble?.Expire(true);
};
}
#region Pooled Bubble drawable
private partial class BubbleDrawable : PoolableDrawable
{
public DrawableOsuHitObject? DrawableOsuHitObject { get; set; }
public Vector2 InitialSize { get; set; }
public float MaxSize { get; set; }
public double FadeTime { get; set; }
private readonly Box colourBox;
private readonly CircularContainer content;
public BubbleDrawable()
{
Origin = Anchor.Centre;
InternalChild = content = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
MaskingSmoothness = 2,
BorderThickness = 0,
BorderColour = Colour4.White,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = 3,
Colour = Colour4.Black.Opacity(0.05f),
},
Child = colourBox = new Box { RelativeSizeAxes = Axes.Both, }
};
}
protected override void PrepareForUse()
{
Debug.Assert(DrawableOsuHitObject.IsNotNull());
Colour = DrawableOsuHitObject.IsHit ? Colour4.White : Colour4.Black;
Scale = new Vector2(1);
Position = getPosition(DrawableOsuHitObject);
Size = InitialSize;
//We want to fade to a darker colour to avoid colours such as white hiding the "ripple" effect.
ColourInfo colourDarker = DrawableOsuHitObject.AccentColour.Value.Darken(0.1f);
// The absolute length of the bubble's animation, can be used in fractions for animations of partial length
double duration = 1700 + Math.Pow(FadeTime, 1.07f);
// Main bubble scaling based on combo
this.FadeTo(1)
.ScaleTo(MaxSize, duration * 0.8f)
.Then()
// Pop at the end of the bubbles life time
.ScaleTo(MaxSize * 1.5f, duration * 0.2f, Easing.OutQuint)
.FadeOut(duration * 0.2f, Easing.OutCirc).Expire();
if (!DrawableOsuHitObject.IsHit) return;
content.BorderThickness = InitialSize.X / 3.5f;
content.BorderColour = Colour4.White;
colourBox.FadeColour(colourDarker);
content.TransformTo(nameof(BorderColour), colourDarker, duration * 0.3f, Easing.OutQuint);
// Ripple effect utilises the border to reduce drawable count
content.TransformTo(nameof(BorderThickness), 2f, duration * 0.3f, Easing.OutQuint)
.Then()
// Avoids transparency overlap issues during the bubble "pop"
.TransformTo(nameof(BorderThickness), 0f);
}
private Vector2 getPosition(DrawableOsuHitObject drawableObject)
{
switch (drawableObject)
{
// SliderHeads are derived from HitCircles,
// so we must handle them before to avoid them using the wrong positioning logic
case DrawableSliderHead:
return drawableObject.HitObject.Position;
// Using hitobject position will cause issues with HitCircle placement due to stack leniency.
case DrawableHitCircle:
return drawableObject.Position;
default:
return drawableObject.HitObject.Position;
}
}
}
#endregion
}
}

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModDaycore : ModDaycore
{
public override double ScoreMultiplier => 0.3;
}
}

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModDoubleTime : ModDoubleTime
{
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModHalfTime : ModHalfTime
{
public override double ScoreMultiplier => 0.3;
}
}

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "No need to chase the circles your cursor is a magnet!";
public override double ScoreMultiplier => 0.5;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles) };
[SettingSource("Attraction strength", "How strong the pull is.", 0)]
public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f)

View File

@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModNightcore : ModNightcore<OsuHitObject>
{
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "Hit objects run away!";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModBubbles) };
[SettingSource("Repulsion strength", "How strong the repulsion is.", 0)]
public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f)

View File

@ -133,14 +133,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
// Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being.
if (HitObject.SampleControlPoint == null)
{
throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
}
Samples.Samples = HitObject.TailSamples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
Samples.Samples = HitObject.TailSamples.Cast<ISampleInfo>().ToArray();
slidingSample.Samples = HitObject.CreateSlidingSamples().Cast<ISampleInfo>().ToArray();
}
public override void StopAllSamples()

View File

@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.LoadSamples();
spinningSample.Samples = HitObject.CreateSpinningSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
spinningSample.Samples = HitObject.CreateSpinningSamples().Cast<ISampleInfo>().ToArray();
spinningSample.Frequency.Value = spinning_sample_initial_frequency;
}

View File

@ -10,18 +10,18 @@ using osu.Game.Rulesets.Objects;
using System.Linq;
using System.Threading;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
public class Slider : OsuHitObject, IHasPathWithRepeats
public class Slider : OsuHitObject, IHasPathWithRepeats, IHasSliderVelocity, IHasGenerateTicks
{
public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
@ -134,6 +134,21 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary>
public bool OnlyJudgeNestedObjects = true;
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
{
Precision = 0.01,
MinValue = 0.1,
MaxValue = 10
};
public double SliderVelocity
{
get => SliderVelocityBindable.Value;
set => SliderVelocityBindable.Value = value;
}
public bool GenerateTicks { get; set; } = true;
[JsonIgnore]
public SliderHeadCircle HeadCircle { get; protected set; }
@ -151,15 +166,11 @@ namespace osu.Game.Rulesets.Osu.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
#pragma warning disable 618
var legacyDifficultyPoint = DifficultyControlPoint as LegacyBeatmapDecoder.LegacyDifficultyControlPoint;
#pragma warning restore 618
double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
bool generateTicks = legacyDifficultyPoint?.GenerateTicks ?? true;
double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = generateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity;
TickDistance = GenerateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)

View File

@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Objects
AddNested(i < SpinsRequired
? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration }
: new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration });
: new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration, Samples = new[] { GetSampleInfo("spinnerbonus") } });
}
}
@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects
return new[]
{
SampleControlPoint.ApplyTo(referenceSample).With("spinnerspin")
referenceSample.With("spinnerspin")
};
}
}

View File

@ -3,7 +3,6 @@
#nullable disable
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
@ -11,11 +10,6 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public class SpinnerBonusTick : SpinnerTick
{
public SpinnerBonusTick()
{
Samples.Add(new HitSampleInfo("spinnerbonus"));
}
public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement();
public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement

View File

@ -203,7 +203,8 @@ namespace osu.Game.Rulesets.Osu
new OsuModNoScope(),
new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
new ModAdaptiveSpeed(),
new OsuModFreezeFrame()
new OsuModFreezeFrame(),
new OsuModBubbles()
};
case ModType.System:

View File

@ -10,6 +10,7 @@ namespace osu.Game.Rulesets.Osu
Cursor,
CursorTrail,
CursorParticles,
CursorRipple,
SliderScorePoint,
ReverseArrow,
HitCircleText,

View File

@ -98,7 +98,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
{
this.FadeOut(duration, Easing.OutQuint);
// intentionally pile on an extra FadeOut to make it happen much faster
this.FadeOut(duration / 4, Easing.OutQuint);
icon.ScaleTo(defaultIconScale * icon_scale, duration, Easing.OutQuint);
}
}

View File

@ -100,6 +100,28 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null;
case OsuSkinComponents.CursorRipple:
if (GetTexture("cursor-ripple") != null)
{
var ripple = this.GetAnimation("cursor-ripple", false, false);
// In stable this element was scaled down to 50% and opacity 20%, but this makes the elements WAY too big and inflexible.
// If anyone complains about these not being applied, this can be uncommented.
//
// But if no one complains I'd rather fix this in lazer. Wiki documentation doesn't mention size,
// so we might be okay.
//
// if (ripple != null)
// {
// ripple.Scale = new Vector2(0.5f);
// ripple.Alpha = 0.2f;
// }
return ripple;
}
return null;
case OsuSkinComponents.CursorParticles:
if (GetTexture("star2") != null)
return new LegacyCursorParticles();

View File

@ -252,13 +252,14 @@ namespace osu.Game.Rulesets.Osu.Skinning
renderer.SetBlend(BlendingParameters.Additive);
renderer.PushLocalMatrix(DrawInfo.Matrix);
TextureShader.Bind();
BindTextureShader(renderer);
texture.Bind();
for (int i = 0; i < points.Count; i++)
drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex);
TextureShader.Unbind();
UnbindTextureShader(renderer);
renderer.PopLocalMatrix();
}

View File

@ -0,0 +1,105 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.UI.Cursor
{
public partial class CursorRippleVisualiser : CompositeDrawable, IKeyBindingHandler<OsuAction>
{
private readonly Bindable<bool> showRipples = new Bindable<bool>(true);
private readonly DrawablePool<CursorRipple> ripplePool = new DrawablePool<CursorRipple>(20);
public CursorRippleVisualiser()
{
RelativeSizeAxes = Axes.Both;
}
public Vector2 CursorScale { get; set; } = Vector2.One;
[BackgroundDependencyLoader(true)]
private void load(OsuRulesetConfigManager? rulesetConfig)
{
rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorRipples, showRipples);
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
{
if (showRipples.Value)
{
AddInternal(ripplePool.Get(r =>
{
r.Position = e.MousePosition;
r.Scale = CursorScale;
}));
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
}
private partial class CursorRipple : PoolableDrawable
{
private Drawable ripple = null!;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
Origin = Anchor.Centre;
InternalChild = ripple = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorRipple), _ => new DefaultCursorRipple())
{
Blending = BlendingParameters.Additive,
};
}
protected override void PrepareForUse()
{
base.PrepareForUse();
ClearTransforms(true);
ripple.ScaleTo(0.1f)
.ScaleTo(1, 700, Easing.Out);
this
.FadeOutFromOne(700)
.Expire(true);
}
}
public partial class DefaultCursorRipple : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new RingPiece(3)
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2),
Alpha = 0.1f,
}
};
}
}
}
}

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Shaders.Types;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.Input.Events;
@ -255,15 +256,23 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Source.parts.CopyTo(parts, 0);
}
private IUniformBuffer<CursorTrailParameters> cursorTrailParameters;
public override void Draw(IRenderer renderer)
{
base.Draw(renderer);
vertexBatch ??= renderer.CreateQuadBatch<TexturedTrailVertex>(max_sprites, 1);
cursorTrailParameters ??= renderer.CreateUniformBuffer<CursorTrailParameters>();
cursorTrailParameters.Data = cursorTrailParameters.Data with
{
FadeClock = time,
FadeExponent = fadeExponent
};
shader.Bind();
shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time);
shader.GetUniform<float>("g_FadeExponent").UpdateValue(ref fadeExponent);
shader.BindUniformBlock("m_CursorTrailParameters", cursorTrailParameters);
texture.Bind();
@ -323,6 +332,15 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
base.Dispose(isDisposing);
vertexBatch?.Dispose();
cursorTrailParameters?.Dispose();
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private record struct CursorTrailParameters
{
public UniformFloat FadeClock;
public UniformFloat FadeExponent;
private readonly UniformPadding8 pad1;
}
}

View File

@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private Bindable<float> userCursorScale;
private Bindable<bool> autoCursorScale;
private readonly CursorRippleVisualiser rippleVisualiser;
public OsuCursorContainer()
{
InternalChild = fadeContainer = new Container
@ -48,6 +50,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Children = new[]
{
cursorTrail = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling),
rippleVisualiser = new CursorRippleVisualiser(),
new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorParticles), confineMode: ConfineMode.NoScaling),
}
};
@ -82,6 +85,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
var newScale = new Vector2(e.NewValue);
ActiveCursor.Scale = newScale;
rippleVisualiser.CursorScale = newScale;
cursorTrail.Scale = newScale;
}, true);

View File

@ -43,6 +43,11 @@ namespace osu.Game.Rulesets.Osu.UI
LabelText = RulesetSettingsStrings.CursorTrail,
Current = config.GetBindable<bool>(OsuRulesetSetting.ShowCursorTrail)
},
new SettingsCheckbox
{
LabelText = RulesetSettingsStrings.CursorRipples,
Current = config.GetBindable<bool>(OsuRulesetSetting.ShowCursorRipples)
},
new SettingsEnumDropdown<PlayfieldBorderStyle>
{
LabelText = RulesetSettingsStrings.PlayfieldBorderStyle,

View File

@ -22,6 +22,13 @@ namespace osu.Game.Rulesets.Osu.UI
/// </summary>
private readonly List<TrackedTouch> trackedTouches = new List<TrackedTouch>();
/// <summary>
/// The distance (in local pixels) that a touch must move before being considered a permanent tracking touch.
/// After this distance is covered, any extra touches on the screen will be considered as button inputs, unless
/// a new touch directly interacts with a hit circle.
/// </summary>
private const float distance_before_position_tracking_lock_in = 100;
private TrackedTouch? positionTrackingTouch;
private readonly OsuInputManager osuInputManager;
@ -97,26 +104,32 @@ namespace osu.Game.Rulesets.Osu.UI
return;
}
// ..or if the current position tracking touch was not a direct touch (this one is debatable and may be change in the future, but it's the simplest way to handle)
if (!positionTrackingTouch.DirectTouch)
// ..or if the current position tracking touch was not a direct touch (and didn't travel across the screen too far).
if (!positionTrackingTouch.DirectTouch && positionTrackingTouch.DistanceTravelled < distance_before_position_tracking_lock_in)
{
positionTrackingTouch = newTouch;
return;
}
// In the case the new touch was not used for position tracking, we should also check the previous position tracking touch.
// If it was a direct touch and still has its action pressed, that action should be released.
// If it still has its action pressed, that action should be released.
//
// This is done to allow tracking with the initial touch while still having both Left/Right actions available for alternating with two more touches.
if (positionTrackingTouch.DirectTouch && positionTrackingTouch.Action is OsuAction directTouchAction)
if (positionTrackingTouch.Action is OsuAction touchAction)
{
osuInputManager.KeyBindingContainer.TriggerReleased(directTouchAction);
osuInputManager.KeyBindingContainer.TriggerReleased(touchAction);
positionTrackingTouch.Action = null;
}
}
private void handleTouchMovement(TouchEvent touchEvent)
{
if (touchEvent is TouchMoveEvent moveEvent)
{
var trackedTouch = trackedTouches.Single(t => t.Source == touchEvent.Touch.Source);
trackedTouch.DistanceTravelled += moveEvent.Delta.Length;
}
// Movement should only be tracked for the most recent touch.
if (touchEvent.Touch.Source != positionTrackingTouch?.Source)
return;
@ -148,8 +161,16 @@ namespace osu.Game.Rulesets.Osu.UI
public OsuAction? Action;
/// <summary>
/// Whether the touch was on a hit circle receptor.
/// </summary>
public readonly bool DirectTouch;
/// <summary>
/// The total distance on screen travelled by this touch (in local pixels).
/// </summary>
public float DistanceTravelled;
public TrackedTouch(TouchSource source, OsuAction? action, bool directTouch)
{
Source = source;

View File

@ -4,6 +4,7 @@
<OutputType>Library</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Description>click the circles. to the beat.</Description>
<LangVersion>10</LangVersion>
</PropertyGroup>
<PropertyGroup Label="Nuget">

View File

@ -1,17 +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.
#nullable disable
using Foundation;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Taiko.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameAppDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}

View File

@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using UIKit;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Taiko.Tests.iOS
{
@ -11,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.iOS
{
public static void Main(string[] args)
{
UIApplication.Main(args, null, typeof(AppDelegate));
GameApplication.Main(new OsuTestBrowser());
}
}
}

View File

@ -64,7 +64,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
foreach (HitObject hitObject in original.HitObjects)
{
double nextScrollSpeed = hitObject.DifficultyControlPoint.SliderVelocity;
if (hitObject is not IHasSliderVelocity hasSliderVelocity) continue;
double nextScrollSpeed = hasSliderVelocity.SliderVelocity;
EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime);
if (!Precision.AlmostEquals(lastScrollSpeed, nextScrollSpeed, acceptableDifference: currentEffectPoint.ScrollSpeedBindable.Precision))
@ -131,7 +133,8 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
StartTime = obj.StartTime,
Samples = obj.Samples,
Duration = taikoDuration,
TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4
TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4,
SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1
};
}
@ -177,15 +180,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
DifficultyControlPoint difficultyPoint = obj.DifficultyControlPoint;
double beatLength;
#pragma warning disable 618
if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint)
#pragma warning restore 618
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
if (obj.LegacyBpmMultiplier.HasValue)
beatLength = timingPoint.BeatLength * obj.LegacyBpmMultiplier.Value;
else if (obj is IHasSliderVelocity hasSliderVelocity)
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
else
beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity;
beatLength = timingPoint.BeatLength;
double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Taiko.Objects;
@ -35,20 +33,11 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
protected override bool OnMouseDown(MouseDownEvent e)
{
switch (e.Button)
{
case MouseButton.Left:
HitObject.Type = HitType.Centre;
EndPlacement(true);
return true;
if (e.Button != MouseButton.Left)
return false;
case MouseButton.Right:
HitObject.Type = HitType.Rim;
EndPlacement(true);
return true;
}
return false;
EndPlacement(true);
return true;
}
public override void UpdateTimeAndPosition(SnapResult result)

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{
var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false;
drawableTaikoRuleset.LockPlayfieldAspectRange.Value = false;
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
playfield.ClassicHitTargetPosition.Value = true;

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModDaycore : ModDaycore
{
public override double ScoreMultiplier => 0.3;
}
}

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModDoubleTime : ModDoubleTime
{
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModHalfTime : ModHalfTime
{
public override double ScoreMultiplier => 0.3;
}
}

View File

@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModNightcore : ModNightcore<TaikoHitObject>
{
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}

View File

@ -3,8 +3,11 @@
#nullable disable
using System.Linq;
using osu.Game.Rulesets.Objects.Types;
using System.Threading;
using osu.Framework.Bindables;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
@ -15,7 +18,7 @@ using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects
{
public class DrumRoll : TaikoStrongableHitObject, IHasPath
public class DrumRoll : TaikoStrongableHitObject, IHasPath, IHasSliderVelocity
{
/// <summary>
/// Drum roll distance that results in a duration of 1 speed-adjusted beat length.
@ -35,6 +38,19 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// </summary>
public double Velocity { get; private set; }
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
{
Precision = 0.01,
MinValue = 0.1,
MaxValue = 10
};
public double SliderVelocity
{
get => SliderVelocityBindable.Value;
set => SliderVelocityBindable.Value = value;
}
/// <summary>
/// Numer of ticks per beat length.
/// </summary>
@ -52,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
double scoringDistance = base_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
double scoringDistance = base_distance * difficulty.SliderMultiplier * SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
tickSpacing = timingPoint.BeatLength / TickRate;
@ -81,7 +97,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
FirstTick = first,
TickSpacing = tickSpacing,
StartTime = t,
IsStrong = IsStrong
IsStrong = IsStrong,
Samples = Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToList()
});
first = false;

View File

@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
if (IsStrongBindable.Value != strongSamples.Any())
{
if (IsStrongBindable.Value)
Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
Samples.Add(GetSampleInfo(HitSampleInfo.HIT_FINISH));
else
{
foreach (var sample in strongSamples)

View File

@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public new BindableDouble TimeRange => base.TimeRange;
public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true);
public readonly BindableBool LockPlayfieldAspectRange = new BindableBool(true);
public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
@ -69,7 +69,9 @@ namespace osu.Game.Rulesets.Taiko.UI
const float scroll_rate = 10;
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
float ratio = DrawHeight / 480;
// Width is used because it defines how many notes fit on the playfield.
// We clamp the ratio to the maximum aspect ratio to keep scroll speed consistent on widths lower than the default.
float ratio = Math.Max(DrawSize.X / 768f, TaikoPlayfieldAdjustmentContainer.MAXIMUM_ASPECT);
TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
}
@ -92,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.UI
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer
{
LockPlayfieldMaxAspect = { BindTarget = LockPlayfieldMaxAspect }
LockPlayfieldAspectRange = { BindTarget = LockPlayfieldAspectRange }
};
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Game.Audio;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
@ -17,12 +18,12 @@ namespace osu.Game.Rulesets.Taiko.UI
public void Play(HitType hitType)
{
var hitObject = GetMostValidObject();
var hitSample = GetMostValidObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
if (hitObject == null)
if (hitSample == null)
return;
PlaySamples(new ISampleInfo[] { hitObject.SampleControlPoint.GetSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL) });
PlaySamples(new ISampleInfo[] { new HitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL, hitSample.Bank, volume: hitSample.Volume) });
}
public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead");

View File

@ -11,9 +11,11 @@ namespace osu.Game.Rulesets.Taiko.UI
public partial class TaikoPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
private const float default_aspect = 16f / 9f;
public readonly IBindable<bool> LockPlayfieldMaxAspect = new BindableBool(true);
public const float MAXIMUM_ASPECT = 16f / 9f;
public const float MINIMUM_ASPECT = 5f / 4f;
public readonly IBindable<bool> LockPlayfieldAspectRange = new BindableBool(true);
protected override void Update()
{
@ -26,12 +28,22 @@ namespace osu.Game.Rulesets.Taiko.UI
//
// As a middle-ground, the aspect ratio can still be adjusted in the downwards direction but has a maximum limit.
// This is still a bit weird, because readability changes with window size, but it is what it is.
if (LockPlayfieldMaxAspect.Value && Parent.ChildSize.X / Parent.ChildSize.Y > default_aspect)
height *= Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
if (LockPlayfieldAspectRange.Value)
{
float currentAspect = Parent.ChildSize.X / Parent.ChildSize.Y;
if (currentAspect > MAXIMUM_ASPECT)
height *= currentAspect / MAXIMUM_ASPECT;
else if (currentAspect < MINIMUM_ASPECT)
height *= currentAspect / MINIMUM_ASPECT;
}
// Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions.
height = Math.Min(height, 1f / 3f);
Height = height;
// Position the taiko playfield exactly one playfield from the top of the screen.
// Position the taiko playfield exactly one playfield from the top of the screen, if there is enough space for it.
// Note that the relative height cannot exceed one-third - if that limit is hit, the playfield will be exactly centered.
RelativePositionAxes = Axes.Y;
Y = height;
}

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