mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 10:33:30 +08:00
Merge branch 'master' into LNFixHotfix
This commit is contained in:
commit
7d8cef83e6
@ -21,7 +21,7 @@
|
||||
]
|
||||
},
|
||||
"ppy.localisationanalyser.tools": {
|
||||
"version": "2022.809.0",
|
||||
"version": "2023.712.0",
|
||||
"commands": [
|
||||
"localisation"
|
||||
]
|
||||
|
@ -9,6 +9,9 @@ indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[g_*.cs]
|
||||
generated_code = true
|
||||
|
||||
[*.cs]
|
||||
end_of_line = crlf
|
||||
insert_final_newline = true
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -339,6 +339,5 @@ inspectcode
|
||||
|
||||
# Fody (pulled in by Realm) - schema file
|
||||
FodyWeavers.xsd
|
||||
**/FodyWeavers.xml
|
||||
|
||||
.idea/.idea.osu.Desktop/.idea/misc.xml
|
26
app.manifest
26
app.manifest
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity version="1.0.0.0" name="osu!" />
|
||||
<SquirrelAwareVersion xmlns="urn:schema-squirrel-com:asm.v1">1</SquirrelAwareVersion>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
@ -14,33 +15,10 @@
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows Vista -->
|
||||
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
|
||||
<!-- Windows 7 -->
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
|
||||
<!-- Windows 8 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
|
||||
<!-- Windows 8.1 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
|
||||
<!-- Windows 10 -->
|
||||
<!-- Windows 10 and Windows 11 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
|
||||
<dpiAware>true</dpiAware>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
</asmv1:assembly>
|
||||
|
@ -8,13 +8,9 @@
|
||||
<!-- NullabilityInfoContextSupport is disabled by default for Android -->
|
||||
<NullabilityInfoContextSupport>true</NullabilityInfoContextSupport>
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.625.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.815.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="mailto" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
@ -54,9 +54,6 @@ namespace osu.Desktop
|
||||
|
||||
client.OnReady += onReady;
|
||||
|
||||
// safety measure for now, until we performance test / improve backoff for failed connections.
|
||||
client.OnConnectionFailed += (_, _) => client.Deinitialize();
|
||||
|
||||
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network);
|
||||
|
||||
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
|
||||
@ -187,7 +184,7 @@ namespace osu.Desktop
|
||||
return edit.BeatmapInfo.ToString() ?? string.Empty;
|
||||
|
||||
case UserActivity.WatchingReplay watching:
|
||||
return watching.BeatmapInfo.ToString();
|
||||
return watching.BeatmapInfo?.ToString() ?? string.Empty;
|
||||
|
||||
case UserActivity.InLobby lobby:
|
||||
return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value;
|
||||
|
@ -75,7 +75,7 @@ namespace osu.Desktop.LegacyIpc
|
||||
case LegacyIpcDifficultyCalculationRequest req:
|
||||
try
|
||||
{
|
||||
WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile);
|
||||
WorkingBeatmap beatmap = new FlatWorkingBeatmap(req.BeatmapFile);
|
||||
var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance();
|
||||
Mod[] mods = ruleset.ConvertFromLegacyMods((LegacyMods)req.Mods).ToArray();
|
||||
|
||||
|
@ -147,14 +147,12 @@ namespace osu.Desktop
|
||||
{
|
||||
base.SetHost(host);
|
||||
|
||||
var desktopWindow = (SDL2DesktopWindow)host.Window;
|
||||
|
||||
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
|
||||
if (iconStream != null)
|
||||
desktopWindow.SetIconFromStream(iconStream);
|
||||
host.Window.SetIconFromStream(iconStream);
|
||||
|
||||
desktopWindow.CursorState |= CursorState.Hidden;
|
||||
desktopWindow.Title = Name;
|
||||
host.Window.CursorState |= CursorState.Hidden;
|
||||
host.Window.Title = Name;
|
||||
}
|
||||
|
||||
protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo();
|
||||
|
@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity version="1.0.0.0" name="osu!" />
|
||||
<SquirrelAwareVersion xmlns="urn:schema-squirrel-com:asm.v1">1</SquirrelAwareVersion>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
<applicationRequestMinimum>
|
||||
<defaultAssemblyRequest permissionSetReference="Custom" />
|
||||
<PermissionSet class="System.Security.PermissionSet" version="1" Unrestricted="true" ID="Custom" SameSite="site" />
|
||||
</applicationRequestMinimum>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
|
||||
<dpiAware>true</dpiAware>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</asmv1:assembly>
|
@ -8,7 +8,6 @@
|
||||
<Title>osu!</Title>
|
||||
<Product>osu!(lazer)</Product>
|
||||
<ApplicationIcon>lazer.ico</ApplicationIcon>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Version>0.0.0</Version>
|
||||
<FileVersion>0.0.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
@ -27,7 +26,7 @@
|
||||
<PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
|
||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
<PackageReference Include="System.IO.Packaging" Version="7.0.0" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.1.3.18" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.1.4.20" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
<EmbeddedResource Include="lazer.ico" />
|
||||
|
@ -5,7 +5,7 @@
|
||||
<key>CFBundleName</key>
|
||||
<string>osu.Game.Rulesets.Catch.Tests.iOS</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>ppy.osu-Game-Rulesets-Catch-Tests-iOS</string>
|
||||
<string>sh.ppy.catch-ruleset-tests</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
@ -24,7 +25,8 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } },
|
||||
new object[] { LegacyMods.Flashlight, new[] { typeof(CatchModFlashlight) } },
|
||||
new object[] { LegacyMods.Autoplay, new[] { typeof(CatchModAutoplay) } },
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } }
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } },
|
||||
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(catch_mod_mapping))]
|
||||
|
@ -26,6 +26,8 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Replays.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch
|
||||
@ -91,6 +93,9 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.Relax))
|
||||
yield return new CatchModRelax();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.ScoreV2))
|
||||
yield return new ModScoreV2();
|
||||
}
|
||||
|
||||
public override IEnumerable<Mod> GetModsFor(ModType type)
|
||||
@ -140,6 +145,12 @@ namespace osu.Game.Rulesets.Catch
|
||||
new CatchModNoScope(),
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
return new Mod[]
|
||||
{
|
||||
new ModScoreV2(),
|
||||
};
|
||||
|
||||
default:
|
||||
return Array.Empty<Mod>();
|
||||
}
|
||||
@ -202,10 +213,24 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
public int LegacyID => 2;
|
||||
|
||||
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new CatchLegacyScoreSimulator();
|
||||
|
||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
|
||||
|
||||
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
|
||||
|
||||
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
|
||||
|
||||
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
// Todo: osu!catch should not output star rating in the 'aim' attribute.
|
||||
yield return (ATTRIB_ID_AIM, StarRating);
|
||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
||||
}
|
||||
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
@ -36,7 +35,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
StarRating = values[ATTRIB_ID_AIM];
|
||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,9 +25,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
public override int Version => 20220701;
|
||||
|
||||
private readonly IWorkingBeatmap workingBeatmap;
|
||||
|
||||
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
{
|
||||
workingBeatmap = beatmap;
|
||||
}
|
||||
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
@ -38,13 +41,24 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
return new CatchDifficultyAttributes
|
||||
CatchDifficultyAttributes attributes = new CatchDifficultyAttributes
|
||||
{
|
||||
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor,
|
||||
Mods = mods,
|
||||
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
||||
MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)),
|
||||
};
|
||||
|
||||
if (ComputeLegacyScoringValues)
|
||||
{
|
||||
CatchLegacyScoreSimulator sv1Simulator = new CatchLegacyScoreSimulator();
|
||||
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
||||
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
||||
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
||||
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||
|
142
osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs
Normal file
142
osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs
Normal file
@ -0,0 +1,142 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
internal class CatchLegacyScoreSimulator : ILegacyScoreSimulator
|
||||
{
|
||||
public int AccuracyScore { get; private set; }
|
||||
|
||||
public int ComboScore { get; private set; }
|
||||
|
||||
public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore;
|
||||
|
||||
private int legacyBonusScore;
|
||||
private int modernBonusScore;
|
||||
private int combo;
|
||||
|
||||
private double scoreMultiplier;
|
||||
|
||||
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
||||
{
|
||||
IBeatmap baseBeatmap = workingBeatmap.Beatmap;
|
||||
|
||||
int countNormal = 0;
|
||||
int countSlider = 0;
|
||||
int countSpinner = 0;
|
||||
|
||||
foreach (HitObject obj in baseBeatmap.HitObjects)
|
||||
{
|
||||
switch (obj)
|
||||
{
|
||||
case IHasPath:
|
||||
countSlider++;
|
||||
break;
|
||||
|
||||
case IHasDuration:
|
||||
countSpinner++;
|
||||
break;
|
||||
|
||||
default:
|
||||
countNormal++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int objectCount = countNormal + countSlider + countSpinner;
|
||||
|
||||
int drainLength = 0;
|
||||
|
||||
if (baseBeatmap.HitObjects.Count > 0)
|
||||
{
|
||||
int breakLength = baseBeatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum();
|
||||
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
||||
}
|
||||
|
||||
int difficultyPeppyStars = (int)Math.Round(
|
||||
(baseBeatmap.Difficulty.DrainRate
|
||||
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||
+ baseBeatmap.Difficulty.CircleSize
|
||||
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||
|
||||
scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
||||
|
||||
foreach (var obj in playableBeatmap.HitObjects)
|
||||
simulateHit(obj);
|
||||
}
|
||||
|
||||
private void simulateHit(HitObject hitObject)
|
||||
{
|
||||
bool increaseCombo = true;
|
||||
bool addScoreComboMultiplier = false;
|
||||
|
||||
bool isBonus = false;
|
||||
HitResult bonusResult = HitResult.None;
|
||||
|
||||
int scoreIncrease = 0;
|
||||
|
||||
switch (hitObject)
|
||||
{
|
||||
case TinyDroplet:
|
||||
scoreIncrease = 10;
|
||||
increaseCombo = false;
|
||||
break;
|
||||
|
||||
case Droplet:
|
||||
scoreIncrease = 100;
|
||||
break;
|
||||
|
||||
case Fruit:
|
||||
scoreIncrease = 300;
|
||||
addScoreComboMultiplier = true;
|
||||
increaseCombo = true;
|
||||
break;
|
||||
|
||||
case Banana:
|
||||
scoreIncrease = 1100;
|
||||
increaseCombo = false;
|
||||
isBonus = true;
|
||||
bonusResult = HitResult.LargeBonus;
|
||||
break;
|
||||
|
||||
case JuiceStream:
|
||||
foreach (var nested in hitObject.NestedHitObjects)
|
||||
simulateHit(nested);
|
||||
return;
|
||||
|
||||
case BananaShower:
|
||||
foreach (var nested in hitObject.NestedHitObjects)
|
||||
simulateHit(nested);
|
||||
return;
|
||||
}
|
||||
|
||||
if (addScoreComboMultiplier)
|
||||
{
|
||||
// ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...)
|
||||
ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier));
|
||||
}
|
||||
|
||||
if (isBonus)
|
||||
{
|
||||
legacyBonusScore += scoreIncrease;
|
||||
modernBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||
}
|
||||
else
|
||||
AccuracyScore += scoreIncrease;
|
||||
|
||||
if (increaseCombo)
|
||||
combo++;
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,6 @@ using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
@ -24,7 +23,5 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
: base(new THitObject())
|
||||
{
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@ -59,7 +60,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
|
||||
lastDisplayedCombo = combo;
|
||||
|
||||
if (Time.Elapsed < 0)
|
||||
if ((Clock as IGameplayClock)?.IsRewinding == true)
|
||||
{
|
||||
// needs more work to make rewind somehow look good.
|
||||
// basically we want the previous increment to play... or turning off RemoveCompletedTransforms (not feasible from a performance angle).
|
||||
|
@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
@ -96,7 +97,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
comboDisplay.X = Catcher.X;
|
||||
|
||||
if (Time.Elapsed <= 0)
|
||||
if ((Clock as IGameplayClock)?.IsRewinding == true)
|
||||
{
|
||||
// This is probably a wrong value, but currently the true value is not recorded.
|
||||
// Setting `true` will prevent generation of false-positive after-images (with more false-negatives).
|
||||
|
@ -5,7 +5,7 @@
|
||||
<key>CFBundleName</key>
|
||||
<string>osu.Game.Rulesets.Mania.Tests.iOS</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>ppy.osu-Game-Rulesets-Mania-Tests-iOS</string>
|
||||
<string>sh.ppy.mania-ruleset-tests</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
@ -36,7 +37,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
new object[] { LegacyMods.Key3, new[] { typeof(ManiaModKey3) } },
|
||||
new object[] { LegacyMods.Key2, new[] { typeof(ManiaModKey2) } },
|
||||
new object[] { LegacyMods.Mirror, new[] { typeof(ManiaModMirror) } },
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } }
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } },
|
||||
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(mania_mod_mapping))]
|
||||
|
147
osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs
Normal file
147
osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs
Normal file
@ -0,0 +1,147 @@
|
||||
// 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 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.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
public partial class TestSceneMaximumScore : RateAdjustedBeatmapTestScene
|
||||
{
|
||||
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
||||
|
||||
private List<JudgementResult> judgementResults = new List<JudgementResult>();
|
||||
|
||||
[Test]
|
||||
public void TestSimultaneousTickAndNote()
|
||||
{
|
||||
performTest(
|
||||
new List<ManiaHitObject>
|
||||
{
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = 1000,
|
||||
Duration = 2000,
|
||||
Column = 0,
|
||||
},
|
||||
new Note
|
||||
{
|
||||
StartTime = 2000,
|
||||
Column = 1
|
||||
}
|
||||
},
|
||||
new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1000, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(2000, ManiaAction.Key1, ManiaAction.Key2),
|
||||
new ManiaReplayFrame(2001, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(3000)
|
||||
});
|
||||
|
||||
AddAssert("all objects perfectly judged",
|
||||
() => judgementResults.Select(result => result.Type),
|
||||
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
|
||||
AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSimultaneousLongNotes()
|
||||
{
|
||||
performTest(
|
||||
new List<ManiaHitObject>
|
||||
{
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = 1000,
|
||||
Duration = 2000,
|
||||
Column = 0,
|
||||
},
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = 2000,
|
||||
Duration = 2000,
|
||||
Column = 1
|
||||
}
|
||||
},
|
||||
new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1000, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(2000, ManiaAction.Key1, ManiaAction.Key2),
|
||||
new ManiaReplayFrame(3000, ManiaAction.Key2),
|
||||
new ManiaReplayFrame(4000)
|
||||
});
|
||||
|
||||
AddAssert("all objects perfectly judged",
|
||||
() => judgementResults.Select(result => result.Type),
|
||||
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
|
||||
AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
|
||||
}
|
||||
|
||||
private void performTest(List<ManiaHitObject> hitObjects, List<ReplayFrame> frames)
|
||||
{
|
||||
var beatmap = new Beatmap<ManiaHitObject>
|
||||
{
|
||||
HitObjects = hitObjects,
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
|
||||
Ruleset = new ManiaRuleset().RulesetInfo
|
||||
},
|
||||
};
|
||||
|
||||
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
|
||||
|
||||
AddStep("load player", () =>
|
||||
{
|
||||
Beatmap.Value = CreateWorkingBeatmap(beatmap);
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
|
||||
p.OnLoadComplete += _ =>
|
||||
{
|
||||
p.ScoreProcessor.NewJudgement += result =>
|
||||
{
|
||||
if (currentPlayer == p) judgementResults.Add(result);
|
||||
};
|
||||
};
|
||||
|
||||
LoadScreen(currentPlayer = p);
|
||||
judgementResults = new List<JudgementResult>();
|
||||
});
|
||||
|
||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
|
||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
}
|
||||
|
||||
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||
{
|
||||
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
||||
|
||||
protected override bool PauseOnFocusLost => false;
|
||||
|
||||
public ScoreAccessibleReplayPlayer(Score score)
|
||||
: base(score, new PlayerConfiguration
|
||||
{
|
||||
AllowPause = false,
|
||||
ShowResults = false,
|
||||
})
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -117,18 +117,16 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
private void createBarLine(bool major)
|
||||
{
|
||||
foreach (var stage in stages)
|
||||
var obj = new BarLine
|
||||
{
|
||||
var obj = new BarLine
|
||||
{
|
||||
StartTime = Time.Current + 2000,
|
||||
Major = major,
|
||||
};
|
||||
StartTime = Time.Current + 2000,
|
||||
Major = major,
|
||||
};
|
||||
|
||||
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
foreach (var stage in stages)
|
||||
stage.Add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
private ScrollingTestContainer createStage(ScrollingDirection direction, ManiaAction action)
|
||||
|
@ -24,7 +24,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
foreach (var v in base.ToDatabaseAttributes())
|
||||
yield return v;
|
||||
|
||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||
}
|
||||
@ -33,7 +32,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
base.FromDatabaseAttributes(values, onlineInfo);
|
||||
|
||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||
}
|
||||
|
@ -31,9 +31,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
|
||||
public override int Version => 20220902;
|
||||
|
||||
private readonly IWorkingBeatmap workingBeatmap;
|
||||
|
||||
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
{
|
||||
workingBeatmap = beatmap;
|
||||
|
||||
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset);
|
||||
originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty;
|
||||
}
|
||||
@ -46,15 +50,26 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
HitWindows hitWindows = new ManiaHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
return new ManiaDifficultyAttributes
|
||||
ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes
|
||||
{
|
||||
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
|
||||
Mods = mods,
|
||||
// In osu-stable mania, rate-adjustment mods don't affect the hit window.
|
||||
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
|
||||
GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
|
||||
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject)
|
||||
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject),
|
||||
};
|
||||
|
||||
if (ComputeLegacyScoringValues)
|
||||
{
|
||||
ManiaLegacyScoreSimulator sv1Simulator = new ManiaLegacyScoreSimulator();
|
||||
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
||||
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
||||
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
||||
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private static int maxComboForObject(HitObject hitObject)
|
||||
|
@ -0,0 +1,28 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
internal class ManiaLegacyScoreSimulator : ILegacyScoreSimulator
|
||||
{
|
||||
public int AccuracyScore => 0;
|
||||
public int ComboScore { get; private set; }
|
||||
public double BonusScoreRatio => 0;
|
||||
|
||||
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
||||
{
|
||||
double multiplier = mods.Where(m => m is not (ModHidden or ModHardRock or ModDoubleTime or ModFlashlight or ManiaModFadeIn))
|
||||
.Select(m => m.ScoreMultiplier)
|
||||
.Aggregate(1.0, (c, n) => c * n);
|
||||
|
||||
ComboScore = (int)(1000000 * multiplier);
|
||||
}
|
||||
}
|
||||
}
|
22
osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs
Normal file
22
osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs
Normal file
@ -0,0 +1,22 @@
|
||||
// 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.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
{
|
||||
public partial class ManiaDifficultySection : DifficultySection
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
CircleSizeSlider.Label = BeatmapsetsStrings.ShowStatsCsMania;
|
||||
CircleSizeSlider.Description = "The number of columns in the beatmap";
|
||||
if (CircleSizeSlider.Current is BindableNumber<float> circleSizeFloat)
|
||||
circleSizeFloat.Precision = 1;
|
||||
}
|
||||
}
|
||||
}
|
@ -157,6 +157,9 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.Mirror))
|
||||
yield return new ManiaModMirror();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.ScoreV2))
|
||||
yield return new ModScoreV2();
|
||||
}
|
||||
|
||||
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
|
||||
@ -285,6 +288,12 @@ namespace osu.Game.Rulesets.Mania
|
||||
new ModAdaptiveSpeed()
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
return new Mod[]
|
||||
{
|
||||
new ModScoreV2(),
|
||||
};
|
||||
|
||||
default:
|
||||
return Array.Empty<Mod>();
|
||||
}
|
||||
@ -302,6 +311,8 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
public int LegacyID => 3;
|
||||
|
||||
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new ManiaLegacyScoreSimulator();
|
||||
|
||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame();
|
||||
|
||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new ManiaRulesetConfigManager(settings, RulesetInfo);
|
||||
@ -401,7 +412,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 250
|
||||
}, true),
|
||||
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
||||
new StatisticItem("Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[]
|
||||
{
|
||||
new AverageHitError(score.HitEvents),
|
||||
new UnstableRate(score.HitEvents)
|
||||
@ -414,6 +425,8 @@ namespace osu.Game.Rulesets.Mania
|
||||
}
|
||||
|
||||
public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection();
|
||||
|
||||
public override DifficultySection CreateEditorDifficultySection() => new ManiaDifficultySection();
|
||||
}
|
||||
|
||||
public enum PlayfieldType
|
||||
|
@ -33,5 +33,6 @@ namespace osu.Game.Rulesets.Mania
|
||||
HitExplosion,
|
||||
StageBackground,
|
||||
StageForeground,
|
||||
BarLine
|
||||
}
|
||||
}
|
||||
|
@ -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 osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
@ -8,7 +9,15 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
{
|
||||
public class BarLine : ManiaHitObject, IBarLine
|
||||
{
|
||||
public bool Major { get; set; }
|
||||
private HitObjectProperty<bool> major;
|
||||
|
||||
public Bindable<bool> MajorBindable => major.Bindable;
|
||||
|
||||
public bool Major
|
||||
{
|
||||
get => major.Value;
|
||||
set => major.Value = value;
|
||||
}
|
||||
|
||||
public override Judgement CreateJudgement() => new IgnoreJudgement();
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
// 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.Shapes;
|
||||
using osuTK;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
@ -13,45 +15,41 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
/// </summary>
|
||||
public partial class DrawableBarLine : DrawableManiaHitObject<BarLine>
|
||||
{
|
||||
public readonly Bindable<bool> Major = new Bindable<bool>();
|
||||
|
||||
public DrawableBarLine()
|
||||
: this(null!)
|
||||
{
|
||||
}
|
||||
|
||||
public DrawableBarLine(BarLine barLine)
|
||||
: base(barLine)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = barLine.Major ? 1.7f : 1.2f;
|
||||
}
|
||||
|
||||
AddInternal(new Box
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.BarLine), _ => new DefaultBarLine())
|
||||
{
|
||||
Name = "Bar line",
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = barLine.Major ? 0.5f : 0.2f
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
|
||||
if (barLine.Major)
|
||||
{
|
||||
Vector2 size = new Vector2(22, 6);
|
||||
const float line_offset = 4;
|
||||
Major.BindValueChanged(major => Height = major.NewValue ? 1.7f : 1.2f, true);
|
||||
}
|
||||
|
||||
AddInternal(new Circle
|
||||
{
|
||||
Name = "Left line",
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreRight,
|
||||
protected override void OnApply()
|
||||
{
|
||||
base.OnApply();
|
||||
Major.BindTo(HitObject.MajorBindable);
|
||||
}
|
||||
|
||||
Size = size,
|
||||
X = -line_offset,
|
||||
});
|
||||
|
||||
AddInternal(new Circle
|
||||
{
|
||||
Name = "Right line",
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Size = size,
|
||||
X = line_offset,
|
||||
});
|
||||
}
|
||||
protected override void OnFree()
|
||||
{
|
||||
base.OnFree();
|
||||
Major.UnbindFrom(HitObject.MajorBindable);
|
||||
}
|
||||
|
||||
protected override void UpdateStartTimeStateTransforms() => this.FadeOut(150);
|
||||
|
@ -16,6 +16,7 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
@ -298,7 +299,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
return false;
|
||||
|
||||
// do not run any of this logic when rewinding, as it inverts order of presses/releases.
|
||||
if (Time.Elapsed < 0)
|
||||
if ((Clock as IGameplayClock)?.IsRewinding == true)
|
||||
return false;
|
||||
|
||||
if (CheckHittable?.Invoke(this, Time.Current) == false)
|
||||
@ -337,7 +338,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
return;
|
||||
|
||||
// do not run any of this logic when rewinding, as it inverts order of presses/releases.
|
||||
if (Time.Elapsed < 0)
|
||||
if ((Clock as IGameplayClock)?.IsRewinding == true)
|
||||
return;
|
||||
|
||||
Tail.UpdateResult();
|
||||
|
@ -2,7 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Scoring
|
||||
@ -16,6 +21,9 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerable<HitObject> EnumerateHitObjects(IBeatmap beatmap)
|
||||
=> base.EnumerateHitObjects(beatmap).OrderBy(ho => ho, JudgementOrderComparer.DEFAULT);
|
||||
|
||||
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
|
||||
{
|
||||
return 10000 * comboProgress
|
||||
@ -25,5 +33,29 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
|
||||
protected override double GetComboScoreChange(JudgementResult result)
|
||||
=> Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base));
|
||||
|
||||
private class JudgementOrderComparer : IComparer<HitObject>
|
||||
{
|
||||
public static readonly JudgementOrderComparer DEFAULT = new JudgementOrderComparer();
|
||||
|
||||
public int Compare(HitObject? x, HitObject? y)
|
||||
{
|
||||
if (ReferenceEquals(x, y)) return 0;
|
||||
if (ReferenceEquals(x, null)) return -1;
|
||||
if (ReferenceEquals(y, null)) return 1;
|
||||
|
||||
int result = x.GetEndTime().CompareTo(y.GetEndTime());
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
// due to the way input is handled in mania, notes take precedence over ticks in judging order.
|
||||
if (x is Note && y is not Note) return -1;
|
||||
if (x is not Note && y is Note) return 1;
|
||||
|
||||
return x is ManiaHitObject maniaX && y is ManiaHitObject maniaY
|
||||
? maniaX.Column.CompareTo(maniaY.Column)
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
72
osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs
Normal file
72
osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs
Normal file
@ -0,0 +1,72 @@
|
||||
// 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.Shapes;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Default
|
||||
{
|
||||
public partial class DefaultBarLine : CompositeDrawable
|
||||
{
|
||||
private Bindable<bool> major = null!;
|
||||
|
||||
private Drawable mainLine = null!;
|
||||
private Drawable leftAnchor = null!;
|
||||
private Drawable rightAnchor = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableHitObject drawableHitObject)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
AddInternal(mainLine = new Box
|
||||
{
|
||||
Name = "Bar line",
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
});
|
||||
|
||||
Vector2 size = new Vector2(22, 6);
|
||||
const float line_offset = 4;
|
||||
|
||||
AddInternal(leftAnchor = new Circle
|
||||
{
|
||||
Name = "Left anchor",
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreRight,
|
||||
Size = size,
|
||||
X = -line_offset,
|
||||
});
|
||||
|
||||
AddInternal(rightAnchor = new Circle
|
||||
{
|
||||
Name = "Right anchor",
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Size = size,
|
||||
X = line_offset,
|
||||
});
|
||||
|
||||
major = ((DrawableBarLine)drawableHitObject).Major.GetBoundCopy();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
major.BindValueChanged(updateMajor, true);
|
||||
}
|
||||
|
||||
private void updateMajor(ValueChangedEvent<bool> major)
|
||||
{
|
||||
mainLine.Alpha = major.NewValue ? 0.5f : 0.2f;
|
||||
leftAnchor.Alpha = rightAnchor.Alpha = major.NewValue ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -119,6 +119,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
case ManiaSkinComponents.StageForeground:
|
||||
return new LegacyStageForeground();
|
||||
|
||||
case ManiaSkinComponents.BarLine:
|
||||
return null; // Not yet implemented.
|
||||
|
||||
default:
|
||||
throw new UnsupportedSkinComponentException(lookup);
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -25,6 +26,22 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
private readonly List<Stage> stages = new List<Stage>();
|
||||
|
||||
public override Quad SkinnableComponentScreenSpaceDrawQuad
|
||||
{
|
||||
get
|
||||
{
|
||||
RectangleF totalArea = RectangleF.Empty;
|
||||
|
||||
for (int i = 0; i < Stages.Count; ++i)
|
||||
{
|
||||
var stageArea = Stages[i].ScreenSpaceDrawQuad.AABBFloat;
|
||||
totalArea = i == 0 ? stageArea : RectangleF.Union(totalArea, stageArea);
|
||||
}
|
||||
|
||||
return totalArea;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos));
|
||||
|
||||
public ManiaPlayfield(List<StageDefinition> stageDefinitions)
|
||||
|
@ -136,6 +136,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
columnFlow.SetContentForColumn(i, column);
|
||||
AddNested(column);
|
||||
}
|
||||
|
||||
RegisterPool<BarLine, DrawableBarLine>(50, 200);
|
||||
}
|
||||
|
||||
private ISkinSource currentSkin;
|
||||
@ -186,7 +188,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
public override bool Remove(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Remove(h);
|
||||
|
||||
public void Add(BarLine barLine) => base.Add(new DrawableBarLine(barLine));
|
||||
public void Add(BarLine barLine) => base.Add(barLine);
|
||||
|
||||
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
||||
{
|
||||
|
@ -5,7 +5,7 @@
|
||||
<key>CFBundleName</key>
|
||||
<string>osu.Game.Rulesets.Osu.Tests.iOS</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>ppy.osu-Game-Rulesets-Osu-Tests-iOS</string>
|
||||
<string>sh.ppy.osu-ruleset-tests</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
@ -25,6 +25,35 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||
|
||||
[Test]
|
||||
public void TestSelectAfterFadedOut()
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(100, 100),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
}
|
||||
}
|
||||
};
|
||||
AddStep("add slider", () => EditorBeatmap.Add(slider));
|
||||
|
||||
moveMouseToObject(() => slider);
|
||||
|
||||
AddStep("seek after end", () => EditorClock.Seek(750));
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("slider not selected", () => EditorBeatmap.SelectedHitObjects.Count == 0);
|
||||
|
||||
AddStep("seek to visible", () => EditorClock.Seek(650));
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddUntilStep("slider selected", () => EditorBeatmap.SelectedHitObjects.Single() == slider);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContextMenuShownCorrectlyForSelectedSlider()
|
||||
{
|
||||
|
@ -61,6 +61,20 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertControlPointType(0, PathType.Linear);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceWithMouseMovementOutsidePlayfield()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
AddStep("move mouse out of screen", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + Vector2.One));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(2);
|
||||
assertControlPointType(0, PathType.Linear);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceNormalControlPoint()
|
||||
{
|
||||
|
@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[TestCase("uneven-repeat-slider")]
|
||||
[TestCase("old-stacking")]
|
||||
[TestCase("multi-segment-slider")]
|
||||
[TestCase("nan-slider")]
|
||||
public void Test(string name) => base.Test(name);
|
||||
|
||||
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
||||
|
@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
[TestCase(6.7115569159190587d, 206, "diffcalc-test")]
|
||||
[TestCase(1.4391311903612753d, 45, "zero-length-sliders")]
|
||||
[TestCase(0.14102693012101306d, 1, "nan-slider")]
|
||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
@ -28,7 +29,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } },
|
||||
new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } },
|
||||
new object[] { LegacyMods.Target, new[] { typeof(OsuModTargetPractice) } },
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } }
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } },
|
||||
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(osu_mod_mapping))]
|
||||
|
147
osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs
Normal file
147
osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs
Normal file
@ -0,0 +1,147 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public partial class TestSceneSpinnerJudgement : RateAdjustedBeatmapTestScene
|
||||
{
|
||||
private const double time_spinner_start = 2000;
|
||||
private const double time_spinner_end = 4000;
|
||||
|
||||
private List<JudgementResult> judgementResults = new List<JudgementResult>();
|
||||
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
||||
|
||||
[Test]
|
||||
public void TestHitNothing()
|
||||
{
|
||||
performTest(new List<ReplayFrame>());
|
||||
|
||||
AddAssert("all min judgements", () => judgementResults.All(result => result.Type == result.Judgement.MinResult));
|
||||
}
|
||||
|
||||
[TestCase(1)]
|
||||
[TestCase(2)]
|
||||
[TestCase(5)]
|
||||
public void TestNumberOfSpins(int spins)
|
||||
{
|
||||
performTest(generateReplay(spins));
|
||||
|
||||
for (int i = 0; i < spins; ++i)
|
||||
assertResult<SpinnerTick>(i, HitResult.SmallBonus);
|
||||
|
||||
assertResult<SpinnerTick>(spins, HitResult.IgnoreMiss);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitEverything()
|
||||
{
|
||||
performTest(generateReplay(20));
|
||||
|
||||
AddAssert("all max judgements", () => judgementResults.All(result => result.Type == result.Judgement.MaxResult));
|
||||
}
|
||||
|
||||
private static List<ReplayFrame> generateReplay(int spins)
|
||||
{
|
||||
var replayFrames = new List<ReplayFrame>();
|
||||
|
||||
const int frames_per_spin = 30;
|
||||
|
||||
for (int i = 0; i < spins * frames_per_spin; ++i)
|
||||
{
|
||||
float totalProgress = i / (float)(spins * frames_per_spin);
|
||||
float spinProgress = (i % frames_per_spin) / (float)frames_per_spin;
|
||||
double time = time_spinner_start + (time_spinner_end - time_spinner_start) * totalProgress;
|
||||
float posX = MathF.Cos(2 * MathF.PI * spinProgress);
|
||||
float posY = MathF.Sin(2 * MathF.PI * spinProgress);
|
||||
Vector2 finalPos = OsuPlayfield.BASE_SIZE / 2 + new Vector2(posX, posY) * 50;
|
||||
|
||||
replayFrames.Add(new OsuReplayFrame(time, finalPos, OsuAction.LeftButton));
|
||||
}
|
||||
|
||||
return replayFrames;
|
||||
}
|
||||
|
||||
private void performTest(List<ReplayFrame> frames)
|
||||
{
|
||||
AddStep("load player", () =>
|
||||
{
|
||||
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Spinner
|
||||
{
|
||||
StartTime = time_spinner_start,
|
||||
EndTime = time_spinner_end,
|
||||
Position = OsuPlayfield.BASE_SIZE / 2
|
||||
}
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty(),
|
||||
Ruleset = new OsuRuleset().RulesetInfo
|
||||
},
|
||||
});
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
|
||||
p.OnLoadComplete += _ =>
|
||||
{
|
||||
p.ScoreProcessor.NewJudgement += result =>
|
||||
{
|
||||
if (currentPlayer == p) judgementResults.Add(result);
|
||||
};
|
||||
};
|
||||
|
||||
LoadScreen(currentPlayer = p);
|
||||
judgementResults = new List<JudgementResult>();
|
||||
});
|
||||
|
||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
}
|
||||
|
||||
private void assertResult<T>(int index, HitResult expectedResult)
|
||||
{
|
||||
AddAssert($"{typeof(T).ReadableName()} ({index}) judged as {expectedResult}",
|
||||
() => judgementResults.Where(j => j.HitObject is T).OrderBy(j => j.HitObject.StartTime).ElementAt(index).Type,
|
||||
() => Is.EqualTo(expectedResult));
|
||||
}
|
||||
|
||||
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||
{
|
||||
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
||||
|
||||
protected override bool PauseOnFocusLost => false;
|
||||
|
||||
public ScoreAccessibleReplayPlayer(Score score)
|
||||
: base(score, new PlayerConfiguration
|
||||
{
|
||||
AllowPause = false,
|
||||
ShowResults = false,
|
||||
})
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -214,17 +214,24 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
? currSlider.Position + currSlider.Path.PositionAt(1)
|
||||
: currHitObject.Position;
|
||||
|
||||
// Note the use of `StartTime` in the code below doesn't match stable's use of `EndTime`.
|
||||
// This is because in the stable implementation, `UpdateCalculations` is not called on the inner-loop hitobject (j)
|
||||
// and therefore it does not have a correct `EndTime`, but instead the default of `EndTime = StartTime`.
|
||||
//
|
||||
// Effects of this can be seen on https://osu.ppy.sh/beatmapsets/243#osu/1146 at sliders around 86647 ms, where
|
||||
// if we use `EndTime` here it would result in unexpected stacking.
|
||||
|
||||
if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.Position) < stack_distance)
|
||||
{
|
||||
currHitObject.StackHeight++;
|
||||
startTime = beatmap.HitObjects[j].GetEndTime();
|
||||
startTime = beatmap.HitObjects[j].StartTime;
|
||||
}
|
||||
else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, position2) < stack_distance)
|
||||
{
|
||||
// Case for sliders - bump notes down and right, rather than up and left.
|
||||
sliderStack++;
|
||||
beatmap.HitObjects[j].StackHeight -= sliderStack;
|
||||
startTime = beatmap.HitObjects[j].GetEndTime();
|
||||
startTime = beatmap.HitObjects[j].StartTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -93,7 +93,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
yield return (ATTRIB_ID_SPEED, SpeedDifficulty);
|
||||
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
|
||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
|
||||
if (ShouldSerializeFlashlightRating())
|
||||
@ -111,7 +110,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
SpeedDifficulty = values[ATTRIB_ID_SPEED];
|
||||
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
|
||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
||||
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
||||
|
@ -26,9 +26,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
public override int Version => 20220902;
|
||||
|
||||
private readonly IWorkingBeatmap workingBeatmap;
|
||||
|
||||
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
{
|
||||
workingBeatmap = beatmap;
|
||||
}
|
||||
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
@ -71,7 +74,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1
|
||||
);
|
||||
|
||||
double starRating = basePerformance > 0.00001 ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
|
||||
double starRating = basePerformance > 0.00001
|
||||
? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4)
|
||||
: 0;
|
||||
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
double drainRate = beatmap.Difficulty.DrainRate;
|
||||
@ -86,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
|
||||
return new OsuDifficultyAttributes
|
||||
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
|
||||
{
|
||||
StarRating = starRating,
|
||||
Mods = mods,
|
||||
@ -103,6 +108,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
SliderCount = sliderCount,
|
||||
SpinnerCount = spinnerCount,
|
||||
};
|
||||
|
||||
if (ComputeLegacyScoringValues)
|
||||
{
|
||||
OsuLegacyScoreSimulator sv1Simulator = new OsuLegacyScoreSimulator();
|
||||
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
||||
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
||||
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
||||
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||
|
177
osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs
Normal file
177
osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs
Normal file
@ -0,0 +1,177 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
internal class OsuLegacyScoreSimulator : ILegacyScoreSimulator
|
||||
{
|
||||
public int AccuracyScore { get; private set; }
|
||||
|
||||
public int ComboScore { get; private set; }
|
||||
|
||||
public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore;
|
||||
|
||||
private int legacyBonusScore;
|
||||
private int modernBonusScore;
|
||||
private int combo;
|
||||
|
||||
private double scoreMultiplier;
|
||||
private IBeatmap playableBeatmap = null!;
|
||||
|
||||
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
||||
{
|
||||
this.playableBeatmap = playableBeatmap;
|
||||
|
||||
IBeatmap baseBeatmap = workingBeatmap.Beatmap;
|
||||
|
||||
int countNormal = 0;
|
||||
int countSlider = 0;
|
||||
int countSpinner = 0;
|
||||
|
||||
foreach (HitObject obj in workingBeatmap.Beatmap.HitObjects)
|
||||
{
|
||||
switch (obj)
|
||||
{
|
||||
case IHasPath:
|
||||
countSlider++;
|
||||
break;
|
||||
|
||||
case IHasDuration:
|
||||
countSpinner++;
|
||||
break;
|
||||
|
||||
default:
|
||||
countNormal++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int objectCount = countNormal + countSlider + countSpinner;
|
||||
|
||||
int drainLength = 0;
|
||||
|
||||
if (baseBeatmap.HitObjects.Count > 0)
|
||||
{
|
||||
int breakLength = baseBeatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum();
|
||||
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
||||
}
|
||||
|
||||
int difficultyPeppyStars = (int)Math.Round(
|
||||
(baseBeatmap.Difficulty.DrainRate
|
||||
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||
+ baseBeatmap.Difficulty.CircleSize
|
||||
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||
|
||||
scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
||||
|
||||
foreach (var obj in playableBeatmap.HitObjects)
|
||||
simulateHit(obj);
|
||||
}
|
||||
|
||||
private void simulateHit(HitObject hitObject)
|
||||
{
|
||||
bool increaseCombo = true;
|
||||
bool addScoreComboMultiplier = false;
|
||||
|
||||
bool isBonus = false;
|
||||
HitResult bonusResult = HitResult.None;
|
||||
|
||||
int scoreIncrease = 0;
|
||||
|
||||
switch (hitObject)
|
||||
{
|
||||
case SliderHeadCircle:
|
||||
case SliderTailCircle:
|
||||
case SliderRepeat:
|
||||
scoreIncrease = 30;
|
||||
break;
|
||||
|
||||
case SliderTick:
|
||||
scoreIncrease = 10;
|
||||
break;
|
||||
|
||||
case SpinnerBonusTick:
|
||||
scoreIncrease = 1100;
|
||||
increaseCombo = false;
|
||||
isBonus = true;
|
||||
bonusResult = HitResult.LargeBonus;
|
||||
break;
|
||||
|
||||
case SpinnerTick:
|
||||
scoreIncrease = 100;
|
||||
increaseCombo = false;
|
||||
isBonus = true;
|
||||
bonusResult = HitResult.SmallBonus;
|
||||
break;
|
||||
|
||||
case HitCircle:
|
||||
scoreIncrease = 300;
|
||||
addScoreComboMultiplier = true;
|
||||
break;
|
||||
|
||||
case Slider:
|
||||
foreach (var nested in hitObject.NestedHitObjects)
|
||||
simulateHit(nested);
|
||||
|
||||
scoreIncrease = 300;
|
||||
increaseCombo = false;
|
||||
addScoreComboMultiplier = true;
|
||||
break;
|
||||
|
||||
case Spinner spinner:
|
||||
// The spinner object applies a lenience because gameplay mechanics differ from osu-stable.
|
||||
// We'll redo the calculations to match osu-stable here...
|
||||
const double maximum_rotations_per_second = 477.0 / 60;
|
||||
double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(playableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5);
|
||||
double secondsDuration = spinner.Duration / 1000;
|
||||
|
||||
// The total amount of half spins possible for the entire spinner.
|
||||
int totalHalfSpinsPossible = (int)(secondsDuration * maximum_rotations_per_second * 2);
|
||||
// The amount of half spins that are required to successfully complete the spinner (i.e. get a 300).
|
||||
int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond);
|
||||
// To be able to receive bonus points, the spinner must be rotated another 1.5 times.
|
||||
int halfSpinsRequiredBeforeBonus = halfSpinsRequiredForCompletion + 3;
|
||||
|
||||
for (int i = 0; i <= totalHalfSpinsPossible; i++)
|
||||
{
|
||||
if (i > halfSpinsRequiredBeforeBonus && (i - halfSpinsRequiredBeforeBonus) % 2 == 0)
|
||||
simulateHit(new SpinnerBonusTick());
|
||||
else if (i > 1 && i % 2 == 0)
|
||||
simulateHit(new SpinnerTick());
|
||||
}
|
||||
|
||||
scoreIncrease = 300;
|
||||
addScoreComboMultiplier = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (addScoreComboMultiplier)
|
||||
{
|
||||
// ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...)
|
||||
ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier));
|
||||
}
|
||||
|
||||
if (isBonus)
|
||||
{
|
||||
legacyBonusScore += scoreIncrease;
|
||||
modernBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||
}
|
||||
else
|
||||
AccuracyScore += scoreIncrease;
|
||||
|
||||
if (increaseCombo)
|
||||
combo++;
|
||||
}
|
||||
}
|
||||
}
|
@ -22,7 +22,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
||||
protected override bool AlwaysShowWhenSelected => true;
|
||||
|
||||
protected override bool ShouldBeAlive => base.ShouldBeAlive
|
||||
|| (DrawableObject is not DrawableSpinner && 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);
|
||||
|
||||
public override bool IsSelectable =>
|
||||
// Bypass fade out extension from hit markers for selection purposes.
|
||||
// This is to match stable, where even when the afterimage hit markers are still visible, objects are not selectable.
|
||||
base.ShouldBeAlive;
|
||||
|
||||
protected OsuSelectionBlueprint(T hitObject)
|
||||
: base(hitObject)
|
||||
|
@ -62,12 +62,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
private void load()
|
||||
{
|
||||
// Give a bit of breathing room around the playfield content.
|
||||
PlayfieldContentContainer.Padding = new MarginPadding
|
||||
{
|
||||
Vertical = 10,
|
||||
Left = TOOLBOX_CONTRACTED_SIZE_LEFT + 10,
|
||||
Right = TOOLBOX_CONTRACTED_SIZE_RIGHT + 10,
|
||||
};
|
||||
PlayfieldContentContainer.Padding = new MarginPadding(10);
|
||||
|
||||
LayerBelowRuleset.AddRange(new Drawable[]
|
||||
{
|
||||
|
@ -17,6 +17,7 @@ using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -27,11 +28,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider? snapProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// During a transform, the initial origin is stored so it can be used throughout the operation.
|
||||
/// </summary>
|
||||
private Vector2? referenceOrigin;
|
||||
|
||||
/// <summary>
|
||||
/// During a transform, the initial path types of a single selected slider are stored so they
|
||||
/// can be maintained throughout the operation.
|
||||
@ -42,9 +38,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
base.OnSelectionChanged();
|
||||
|
||||
Quad quad = selectedMovableObjects.Length > 0 ? getSurroundingQuad(selectedMovableObjects) : new Quad();
|
||||
Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad();
|
||||
|
||||
SelectionBox.CanRotate = quad.Width > 0 || quad.Height > 0;
|
||||
SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0;
|
||||
SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0;
|
||||
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
|
||||
@ -53,7 +48,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
protected override void OnOperationEnded()
|
||||
{
|
||||
base.OnOperationEnded();
|
||||
referenceOrigin = null;
|
||||
referencePathTypes = null;
|
||||
}
|
||||
|
||||
@ -109,13 +103,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : getSurroundingQuad(hitObjects);
|
||||
var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
|
||||
bool didFlip = false;
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
{
|
||||
var flippedPosition = GetFlippedPosition(direction, flipQuad, h.Position);
|
||||
var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipQuad, h.Position);
|
||||
|
||||
if (!Precision.AlmostEquals(flippedPosition, h.Position))
|
||||
{
|
||||
@ -169,34 +163,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
|
||||
}
|
||||
|
||||
public override bool HandleRotation(float delta)
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
Quad quad = getSurroundingQuad(hitObjects);
|
||||
|
||||
referenceOrigin ??= quad.Centre;
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
{
|
||||
h.Position = RotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);
|
||||
|
||||
if (h is IHasPath path)
|
||||
{
|
||||
foreach (PathControlPoint cp in path.Path.ControlPoints)
|
||||
cp.Position = RotatePointAroundOrigin(cp.Position, Vector2.Zero, delta);
|
||||
}
|
||||
}
|
||||
|
||||
// this isn't always the case but let's be lenient for now.
|
||||
return true;
|
||||
}
|
||||
public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler();
|
||||
|
||||
private void scaleSlider(Slider slider, Vector2 scale)
|
||||
{
|
||||
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList();
|
||||
|
||||
Quad sliderQuad = GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));
|
||||
Quad sliderQuad = GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));
|
||||
|
||||
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
|
||||
scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
|
||||
@ -222,7 +195,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
slider.SnapTo(snapProvider);
|
||||
|
||||
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
||||
Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
|
||||
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
|
||||
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
||||
|
||||
if (xInBounds && yInBounds && slider.Path.HasValidLength)
|
||||
@ -238,10 +211,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
|
||||
{
|
||||
scale = getClampedScale(hitObjects, reference, scale);
|
||||
Quad selectionQuad = getSurroundingQuad(hitObjects);
|
||||
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
h.Position = GetScaledPosition(reference, scale, selectionQuad, h.Position);
|
||||
h.Position = GeometryUtils.GetScaledPosition(reference, scale, selectionQuad, h.Position);
|
||||
}
|
||||
|
||||
private (bool X, bool Y) isQuadInBounds(Quad quad)
|
||||
@ -256,7 +229,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
Quad quad = getSurroundingQuad(hitObjects);
|
||||
Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
|
||||
Vector2 delta = Vector2.Zero;
|
||||
|
||||
@ -286,7 +259,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
|
||||
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
|
||||
|
||||
Quad selectionQuad = getSurroundingQuad(hitObjects);
|
||||
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
|
||||
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
|
||||
Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y);
|
||||
@ -311,26 +284,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
return scale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a gamefield-space quad surrounding the provided hit objects.
|
||||
/// </summary>
|
||||
/// <param name="hitObjects">The hit objects to calculate a quad for.</param>
|
||||
private Quad getSurroundingQuad(OsuHitObject[] hitObjects) =>
|
||||
GetSurroundingQuad(hitObjects.SelectMany(h =>
|
||||
{
|
||||
if (h is IHasPath path)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
h.Position,
|
||||
// can't use EndPosition for reverse slider cases.
|
||||
h.Position + path.Path.PositionAt(1)
|
||||
};
|
||||
}
|
||||
|
||||
return new[] { h.Position };
|
||||
}));
|
||||
|
||||
/// <summary>
|
||||
/// All osu! hitobjects which can be moved/rotated/scaled.
|
||||
/// </summary>
|
||||
|
107
osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs
Normal file
107
osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs
Normal file
@ -0,0 +1,107 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class OsuSelectionRotationHandler : SelectionRotationHandler
|
||||
{
|
||||
[Resolved]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(EditorBeatmap editorBeatmap)
|
||||
{
|
||||
selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
selectedItems.CollectionChanged += (_, __) => updateState();
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
|
||||
CanRotate.Value = quad.Width > 0 || quad.Height > 0;
|
||||
}
|
||||
|
||||
private OsuHitObject[]? objectsInRotation;
|
||||
|
||||
private Vector2? defaultOrigin;
|
||||
private Dictionary<OsuHitObject, Vector2>? originalPositions;
|
||||
private Dictionary<IHasPath, Vector2[]>? originalPathControlPointPositions;
|
||||
|
||||
public override void Begin()
|
||||
{
|
||||
if (objectsInRotation != null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!");
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
objectsInRotation = selectedMovableObjects.ToArray();
|
||||
defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation).Centre;
|
||||
originalPositions = objectsInRotation.ToDictionary(obj => obj, obj => obj.Position);
|
||||
originalPathControlPointPositions = objectsInRotation.OfType<IHasPath>().ToDictionary(
|
||||
obj => obj,
|
||||
obj => obj.Path.ControlPoints.Select(point => point.Position).ToArray());
|
||||
}
|
||||
|
||||
public override void Update(float rotation, Vector2? origin = null)
|
||||
{
|
||||
if (objectsInRotation == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
|
||||
|
||||
Debug.Assert(originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null);
|
||||
|
||||
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
||||
|
||||
foreach (var ho in objectsInRotation)
|
||||
{
|
||||
ho.Position = GeometryUtils.RotatePointAroundOrigin(originalPositions[ho], actualOrigin, rotation);
|
||||
|
||||
if (ho is IHasPath withPath)
|
||||
{
|
||||
var originalPath = originalPathControlPointPositions[withPath];
|
||||
|
||||
for (int i = 0; i < withPath.Path.ControlPoints.Count; ++i)
|
||||
withPath.Path.ControlPoints[i].Position = GeometryUtils.RotatePointAroundOrigin(originalPath[i], Vector2.Zero, rotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Commit()
|
||||
{
|
||||
if (objectsInRotation == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
|
||||
|
||||
changeHandler?.EndChange();
|
||||
|
||||
objectsInRotation = null;
|
||||
originalPositions = null;
|
||||
originalPathControlPointPositions = null;
|
||||
defaultOrigin = null;
|
||||
}
|
||||
|
||||
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
|
||||
.Where(h => h is not Spinner);
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
@ -179,16 +180,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
private Vector2? lastPosition;
|
||||
|
||||
private bool rewinding;
|
||||
|
||||
public void UpdateProgress(double completionProgress)
|
||||
{
|
||||
Position = drawableSlider.HitObject.CurvePositionAt(completionProgress);
|
||||
|
||||
var diff = lastPosition.HasValue ? lastPosition.Value - Position : Position - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f);
|
||||
|
||||
if (Clock.ElapsedFrameTime != 0)
|
||||
rewinding = Clock.ElapsedFrameTime < 0;
|
||||
bool rewinding = (Clock as IGameplayClock)?.IsRewinding == true;
|
||||
|
||||
// Ensure the value is substantially high enough to allow for Atan2 to get a valid angle.
|
||||
if (diff.LengthFast < 0.01f)
|
||||
|
@ -113,6 +113,9 @@ namespace osu.Game.Rulesets.Osu
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.TouchDevice))
|
||||
yield return new OsuModTouchDevice();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.ScoreV2))
|
||||
yield return new ModScoreV2();
|
||||
}
|
||||
|
||||
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
|
||||
@ -212,6 +215,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
return new Mod[]
|
||||
{
|
||||
new OsuModTouchDevice(),
|
||||
new ModScoreV2(),
|
||||
};
|
||||
|
||||
default:
|
||||
@ -253,6 +257,8 @@ namespace osu.Game.Rulesets.Osu
|
||||
|
||||
public int LegacyID => 0;
|
||||
|
||||
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new OsuLegacyScoreSimulator();
|
||||
|
||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame();
|
||||
|
||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
|
||||
@ -313,7 +319,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 250
|
||||
}, true),
|
||||
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
||||
new StatisticItem("Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[]
|
||||
{
|
||||
new AverageHitError(timedHitEvents),
|
||||
new UnstableRate(timedHitEvents)
|
||||
|
@ -0,0 +1 @@
|
||||
{"Mappings":[{"StartTime":77497.0,"Objects":[{"StartTime":77497.0,"EndTime":77497.0,"X":298.0,"Y":290.0},{"StartTime":77533.0,"EndTime":77533.0,"X":276.162567,"Y":293.0336}]}]}
|
18
osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider.osu
Executable file
18
osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/nan-slider.osu
Executable file
@ -0,0 +1,18 @@
|
||||
osu file format v14
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:5.8
|
||||
CircleSize:4
|
||||
OverallDifficulty:9.6
|
||||
ApproachRate:10
|
||||
SliderMultiplier:2
|
||||
SliderTickRate:1
|
||||
|
||||
[TimingPoints]
|
||||
77211,-100,4,3,50,70,0,0
|
||||
77497,8.40402703648439,4,3,51,70,1,8
|
||||
77497,NaN,4,3,51,70,0,8
|
||||
77498,285.714285714286,4,3,51,70,1,0
|
||||
|
||||
[HitObjects]
|
||||
298,290,77497,6,0,B|234:298|192:279|192:279|180:299|180:299|205:311|238:318|238:318|230:347|217:371|217:371|137:370|80:340|80:340|65:259|73:143|102:68|102:68|149:49|199:34|199:34|213:54|213:54|267:38|324:40|324:40|332:18|332:18|385:20|435:27|435:27|480:93|517:204|521:286|521:286|474:329|396:350|396:350|377:329|363:302|363:302|393:287|415:271|415:271|398:254|398:254|362:282|299:290,1,1723.66345596313,10|0,1:0|3:0,3:0:0:0:
|
@ -72,9 +72,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
lastAngle = thisAngle;
|
||||
|
||||
IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation / 2 - Rotation) > 5f;
|
||||
IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f;
|
||||
|
||||
Rotation = (float)Interpolation.Damp(Rotation, currentRotation / 2, 0.99, Math.Abs(Time.Elapsed));
|
||||
Rotation = (float)Interpolation.Damp(Rotation, currentRotation, 0.99, Math.Abs(Time.Elapsed));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Scoring;
|
||||
@ -120,18 +121,22 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "Overshoot",
|
||||
Font = OsuFont.GetFont(size: 12),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Padding = new MarginPadding(3),
|
||||
Origin = Anchor.BottomLeft,
|
||||
Padding = new MarginPadding(2),
|
||||
Rotation = -rotation,
|
||||
RelativePositionAxes = Axes.Both,
|
||||
Y = -(inner_portion + line_extension) / 2,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "Undershoot",
|
||||
Font = OsuFont.GetFont(size: 12),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Padding = new MarginPadding(3),
|
||||
Origin = Anchor.TopRight,
|
||||
Rotation = -rotation,
|
||||
Padding = new MarginPadding(2),
|
||||
RelativePositionAxes = Axes.Both,
|
||||
Y = (inner_portion + line_extension) / 2,
|
||||
},
|
||||
|
@ -5,7 +5,7 @@
|
||||
<key>CFBundleName</key>
|
||||
<string>osu.Game.Rulesets.Taiko.Tests.iOS</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>ppy.osu-Game-Rulesets-Taiko-Tests-iOS</string>
|
||||
<string>sh.ppy.taiko-ruleset-tests</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
@ -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 System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
@ -10,6 +11,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
@ -36,11 +38,12 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
||||
() => Is.EqualTo(expectedResult));
|
||||
}
|
||||
|
||||
protected void PerformTest(List<ReplayFrame> frames, Beatmap<TaikoHitObject>? beatmap = null)
|
||||
protected void PerformTest(List<ReplayFrame> frames, Beatmap<TaikoHitObject>? beatmap = null, Mod[]? mods = null)
|
||||
{
|
||||
AddStep("load player", () =>
|
||||
{
|
||||
Beatmap.Value = CreateWorkingBeatmap(beatmap);
|
||||
SelectedMods.Value = mods ?? Array.Empty<Mod>();
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
|
||||
|
@ -75,6 +75,25 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
||||
AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitNoneStrongDrumRoll()
|
||||
{
|
||||
PerformTest(new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
}, CreateBeatmap(createDrumRoll(true)));
|
||||
|
||||
AssertJudgementCount(12);
|
||||
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
AssertResult<DrumRollTick>(i, HitResult.IgnoreMiss);
|
||||
AssertResult<DrumRollTick.StrongNestedHit>(i, HitResult.IgnoreMiss);
|
||||
}
|
||||
|
||||
AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitAllStrongDrumRollWithOneKey()
|
||||
{
|
||||
|
@ -4,10 +4,14 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Replays;
|
||||
using osu.Game.Rulesets.Taiko.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
||||
{
|
||||
@ -32,6 +36,28 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
||||
AssertResult<Hit>(0, HitResult.Great);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitWithBothKeysOnSameFrameDoesNotFallThroughToNextObject()
|
||||
{
|
||||
PerformTest(new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
new TaikoReplayFrame(1000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
|
||||
}, CreateBeatmap(new Hit
|
||||
{
|
||||
Type = HitType.Centre,
|
||||
StartTime = 1000,
|
||||
}, new Hit
|
||||
{
|
||||
Type = HitType.Centre,
|
||||
StartTime = 1020
|
||||
}));
|
||||
|
||||
AssertJudgementCount(2);
|
||||
AssertResult<Hit>(0, HitResult.Great);
|
||||
AssertResult<Hit>(1, HitResult.Miss);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitRimHit()
|
||||
{
|
||||
@ -157,5 +183,58 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
||||
AssertJudgementCount(1);
|
||||
AssertResult<Hit>(0, HitResult.Ok);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStrongHitOneKeyWithHidden()
|
||||
{
|
||||
const double hit_time = 1000;
|
||||
|
||||
var beatmap = CreateBeatmap(new Hit
|
||||
{
|
||||
Type = HitType.Centre,
|
||||
StartTime = hit_time,
|
||||
IsStrong = true
|
||||
});
|
||||
|
||||
var hitWindows = new TaikoHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
PerformTest(new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) - 1, TaikoAction.LeftCentre),
|
||||
}, beatmap, new Mod[] { new TaikoModHidden() });
|
||||
|
||||
AssertJudgementCount(2);
|
||||
AssertResult<Hit>(0, HitResult.Ok);
|
||||
AssertResult<Hit.StrongNestedHit>(0, HitResult.IgnoreMiss);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStrongHitTwoKeysWithHidden()
|
||||
{
|
||||
const double hit_time = 1000;
|
||||
|
||||
var beatmap = CreateBeatmap(new Hit
|
||||
{
|
||||
Type = HitType.Centre,
|
||||
StartTime = hit_time,
|
||||
IsStrong = true
|
||||
});
|
||||
|
||||
var hitWindows = new TaikoHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
PerformTest(new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) - 1, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) + DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW - 2, TaikoAction.LeftCentre, TaikoAction.RightCentre),
|
||||
}, beatmap, new Mod[] { new TaikoModHidden() });
|
||||
|
||||
AssertJudgementCount(2);
|
||||
AssertResult<Hit>(0, HitResult.Ok);
|
||||
AssertResult<Hit.StrongNestedHit>(0, HitResult.LargeBonus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -114,5 +114,75 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
||||
|
||||
AddAssert("all tick offsets are 0", () => JudgementResults.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure input is correctly sent to subsequent hits if a swell is fully completed.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitSwellThenHitHit()
|
||||
{
|
||||
const double swell_time = 1000;
|
||||
const double hit_time = 1150;
|
||||
|
||||
Swell swell = new Swell
|
||||
{
|
||||
StartTime = swell_time,
|
||||
Duration = 100,
|
||||
RequiredHits = 1
|
||||
};
|
||||
|
||||
Hit hit = new Hit
|
||||
{
|
||||
StartTime = hit_time
|
||||
};
|
||||
|
||||
List<ReplayFrame> frames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
new TaikoReplayFrame(swell_time, TaikoAction.LeftRim),
|
||||
new TaikoReplayFrame(hit_time, TaikoAction.RightCentre),
|
||||
};
|
||||
|
||||
PerformTest(frames, CreateBeatmap(swell, hit));
|
||||
|
||||
AssertJudgementCount(3);
|
||||
|
||||
AssertResult<SwellTick>(0, HitResult.IgnoreHit);
|
||||
AssertResult<Swell>(0, HitResult.LargeBonus);
|
||||
AssertResult<Hit>(0, HitResult.Great);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMissSwellThenHitHit()
|
||||
{
|
||||
const double swell_time = 1000;
|
||||
const double hit_time = 1150;
|
||||
|
||||
Swell swell = new Swell
|
||||
{
|
||||
StartTime = swell_time,
|
||||
Duration = 100,
|
||||
RequiredHits = 1
|
||||
};
|
||||
|
||||
Hit hit = new Hit
|
||||
{
|
||||
StartTime = hit_time
|
||||
};
|
||||
|
||||
List<ReplayFrame> frames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
new TaikoReplayFrame(hit_time, TaikoAction.RightCentre),
|
||||
};
|
||||
|
||||
PerformTest(frames, CreateBeatmap(swell, hit));
|
||||
|
||||
AssertJudgementCount(3);
|
||||
|
||||
AssertResult<SwellTick>(0, HitResult.IgnoreMiss);
|
||||
AssertResult<Swell>(0, HitResult.IgnoreMiss);
|
||||
AssertResult<Hit>(0, HitResult.Great);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,71 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneTaikoModHidden : TaikoModTestScene
|
||||
{
|
||||
private Func<bool> checkAllMaxResultJudgements(int count) => ()
|
||||
=> Player.ScoreProcessor.JudgedHits >= count
|
||||
&& Player.Results.All(result => result.Type == result.Judgement.MaxResult);
|
||||
|
||||
[Test]
|
||||
public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModHidden(),
|
||||
Autoplay = true,
|
||||
PassCondition = checkSomeAutoplayHits
|
||||
PassCondition = checkAllMaxResultJudgements(4),
|
||||
});
|
||||
|
||||
private bool checkSomeAutoplayHits()
|
||||
=> Player.ScoreProcessor.JudgedHits >= 4
|
||||
&& Player.Results.All(result => result.Type == result.Judgement.MaxResult);
|
||||
[Test]
|
||||
public void TestHitTwoNotesWithinShortPeriod()
|
||||
{
|
||||
const double hit_time = 1;
|
||||
|
||||
var beatmap = new Beatmap<TaikoHitObject>
|
||||
{
|
||||
HitObjects = new List<TaikoHitObject>
|
||||
{
|
||||
new Hit
|
||||
{
|
||||
Type = HitType.Rim,
|
||||
StartTime = hit_time,
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
Type = HitType.Centre,
|
||||
StartTime = hit_time * 2,
|
||||
},
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
SliderTickRate = 4,
|
||||
OverallDifficulty = 0,
|
||||
},
|
||||
Ruleset = new TaikoRuleset().RulesetInfo
|
||||
},
|
||||
};
|
||||
|
||||
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModHidden(),
|
||||
Autoplay = true,
|
||||
PassCondition = checkAllMaxResultJudgements(2),
|
||||
Beatmap = beatmap,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
@ -25,7 +26,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
new object[] { LegacyMods.Flashlight, new[] { typeof(TaikoModFlashlight) } },
|
||||
new object[] { LegacyMods.Autoplay, new[] { typeof(TaikoModAutoplay) } },
|
||||
new object[] { LegacyMods.Random, new[] { typeof(TaikoModRandom) } },
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } }
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } },
|
||||
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(taiko_mod_mapping))]
|
||||
|
@ -72,13 +72,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
});
|
||||
|
||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
|
||||
seekTo(200);
|
||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -100,13 +100,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
});
|
||||
|
||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||
|
||||
seekTo(200);
|
||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -145,23 +145,23 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
});
|
||||
|
||||
AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first));
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
|
||||
|
||||
seekTo(120);
|
||||
AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first));
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
|
||||
|
||||
seekTo(480);
|
||||
AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second));
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||
|
||||
seekTo(700);
|
||||
AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second));
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -174,8 +174,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
StartTime = 100,
|
||||
Samples = new List<HitSampleInfo>
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"),
|
||||
new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM),
|
||||
new HitSampleInfo(HitSampleInfo.HIT_FINISH, HitSampleInfo.BANK_DRUM) // implies strong
|
||||
}
|
||||
};
|
||||
hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
@ -184,13 +184,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
});
|
||||
|
||||
AddAssert("most valid object is nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit.StrongNestedHit>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||
checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
|
||||
checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
|
||||
|
||||
seekTo(200);
|
||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||
checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
|
||||
checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -213,18 +213,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
});
|
||||
|
||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
|
||||
seekTo(600);
|
||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
|
||||
seekTo(1200);
|
||||
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -247,18 +247,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
});
|
||||
|
||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||
|
||||
seekTo(600);
|
||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||
|
||||
seekTo(1200);
|
||||
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -272,8 +272,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
EndTime = 1100,
|
||||
Samples = new List<HitSampleInfo>
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"),
|
||||
new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM),
|
||||
new HitSampleInfo(HitSampleInfo.HIT_FINISH, HitSampleInfo.BANK_DRUM) // implies strong
|
||||
}
|
||||
};
|
||||
drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
@ -282,18 +282,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
});
|
||||
|
||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||
checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
|
||||
checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
|
||||
|
||||
seekTo(600);
|
||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||
checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
|
||||
checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
|
||||
|
||||
seekTo(1200);
|
||||
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||
checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
|
||||
checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -319,18 +319,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
|
||||
// But for sample playback purposes they can be ignored as noise.
|
||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
|
||||
seekTo(600);
|
||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
|
||||
seekTo(1200);
|
||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -344,7 +344,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
EndTime = 1100,
|
||||
Samples = new List<HitSampleInfo>
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum")
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM)
|
||||
}
|
||||
};
|
||||
swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
@ -356,25 +356,26 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
|
||||
// But for sample playback purposes they can be ignored as noise.
|
||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
|
||||
|
||||
seekTo(600);
|
||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
|
||||
|
||||
seekTo(1200);
|
||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
|
||||
}
|
||||
|
||||
private void checkSound(HitType hitType, string expectedName, string expectedBank)
|
||||
private void checkSamples(HitType hitType, bool strong, string expectedSamplesCsv, string expectedBank)
|
||||
{
|
||||
AddStep($"hit {hitType}", () => triggerSource.Play(hitType));
|
||||
AddAssert($"last played sample is {expectedName}", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Single().Name, () => Is.EqualTo(expectedName));
|
||||
AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Single().Bank, () => Is.EqualTo(expectedBank));
|
||||
AddStep($"hit {hitType}", () => triggerSource.Play(hitType, strong));
|
||||
AddAssert($"last played sample is {expectedSamplesCsv}", () => string.Join(',', triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Select(s => s.Name)),
|
||||
() => Is.EqualTo(expectedSamplesCsv));
|
||||
AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().First().Bank, () => Is.EqualTo(expectedBank));
|
||||
}
|
||||
|
||||
private void seekTo(double time) => AddStep($"seek to {time}", () => gameplayClock.Seek(time));
|
||||
|
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
|
||||
private void addFlyingHit(HitType hitType)
|
||||
{
|
||||
var tick = new DrumRollTick { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current };
|
||||
var tick = new DrumRollTick(new DrumRoll()) { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current };
|
||||
|
||||
DrawableDrumRollTick h;
|
||||
DrawableRuleset.Playfield.Add(h = new DrawableDrumRollTick(tick) { JudgementType = hitType });
|
||||
|
@ -3,15 +3,16 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Taiko has some interesting rules for legacy mappings.
|
||||
/// Taiko doesn't output any samples. They are all handled externally by <see cref="DrumSamplePlayer"/>.
|
||||
/// </summary>
|
||||
[HeadlessTest]
|
||||
public partial class TestSceneSampleOutput : TestSceneTaikoPlayer
|
||||
@ -26,10 +27,10 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
HitSampleInfo.HIT_FINISH,
|
||||
HitSampleInfo.HIT_WHISTLE,
|
||||
HitSampleInfo.HIT_WHISTLE,
|
||||
HitSampleInfo.HIT_WHISTLE,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
};
|
||||
|
||||
var actualSampleNames = new List<string>();
|
||||
@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
|
||||
AddUntilStep("all samples collected", () => actualSampleNames.Count == expectedSampleNames.Length);
|
||||
|
||||
AddAssert("samples are correct", () => actualSampleNames.SequenceEqual(expectedSampleNames));
|
||||
AddAssert("samples are correct", () => actualSampleNames, () => Is.EqualTo(expectedSampleNames));
|
||||
}
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TaikoBeatmapConversionTest().GetBeatmap("sample-to-type-conversions");
|
||||
|
@ -48,7 +48,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
foreach (var v in base.ToDatabaseAttributes())
|
||||
yield return v;
|
||||
|
||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||
}
|
||||
@ -57,7 +56,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
base.FromDatabaseAttributes(values, onlineInfo);
|
||||
|
||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||
}
|
||||
|
@ -25,9 +25,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
|
||||
public override int Version => 20220902;
|
||||
|
||||
private readonly IWorkingBeatmap workingBeatmap;
|
||||
|
||||
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
{
|
||||
workingBeatmap = beatmap;
|
||||
}
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
@ -84,7 +87,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
HitWindows hitWindows = new TaikoHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
return new TaikoDifficultyAttributes
|
||||
TaikoDifficultyAttributes attributes = new TaikoDifficultyAttributes
|
||||
{
|
||||
StarRating = starRating,
|
||||
Mods = mods,
|
||||
@ -95,6 +98,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
|
||||
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
|
||||
};
|
||||
|
||||
if (ComputeLegacyScoringValues)
|
||||
{
|
||||
TaikoLegacyScoreSimulator sv1Simulator = new TaikoLegacyScoreSimulator();
|
||||
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
||||
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
||||
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
||||
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
202
osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs
Normal file
202
osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs
Normal file
@ -0,0 +1,202 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
internal class TaikoLegacyScoreSimulator : ILegacyScoreSimulator
|
||||
{
|
||||
public int AccuracyScore { get; private set; }
|
||||
|
||||
public int ComboScore { get; private set; }
|
||||
|
||||
public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore;
|
||||
|
||||
private int legacyBonusScore;
|
||||
private int modernBonusScore;
|
||||
private int combo;
|
||||
|
||||
private double modMultiplier;
|
||||
private int difficultyPeppyStars;
|
||||
private IBeatmap playableBeatmap = null!;
|
||||
private IReadOnlyList<Mod> mods = null!;
|
||||
|
||||
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
||||
{
|
||||
this.playableBeatmap = playableBeatmap;
|
||||
this.mods = mods;
|
||||
|
||||
IBeatmap baseBeatmap = workingBeatmap.Beatmap;
|
||||
|
||||
int countNormal = 0;
|
||||
int countSlider = 0;
|
||||
int countSpinner = 0;
|
||||
|
||||
foreach (HitObject obj in baseBeatmap.HitObjects)
|
||||
{
|
||||
switch (obj)
|
||||
{
|
||||
case IHasPath:
|
||||
countSlider++;
|
||||
break;
|
||||
|
||||
case IHasDuration:
|
||||
countSpinner++;
|
||||
break;
|
||||
|
||||
default:
|
||||
countNormal++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int objectCount = countNormal + countSlider + countSpinner;
|
||||
|
||||
int drainLength = 0;
|
||||
|
||||
if (baseBeatmap.HitObjects.Count > 0)
|
||||
{
|
||||
int breakLength = baseBeatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum();
|
||||
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
||||
}
|
||||
|
||||
difficultyPeppyStars = (int)Math.Round(
|
||||
(baseBeatmap.Difficulty.DrainRate
|
||||
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||
+ baseBeatmap.Difficulty.CircleSize
|
||||
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||
|
||||
modMultiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
||||
|
||||
foreach (var obj in playableBeatmap.HitObjects)
|
||||
simulateHit(obj);
|
||||
}
|
||||
|
||||
private void simulateHit(HitObject hitObject)
|
||||
{
|
||||
bool increaseCombo = true;
|
||||
bool addScoreComboMultiplier = false;
|
||||
|
||||
bool isBonus = false;
|
||||
HitResult bonusResult = HitResult.None;
|
||||
|
||||
int scoreIncrease = 0;
|
||||
|
||||
switch (hitObject)
|
||||
{
|
||||
case SwellTick:
|
||||
scoreIncrease = 300;
|
||||
increaseCombo = false;
|
||||
break;
|
||||
|
||||
case DrumRollTick:
|
||||
scoreIncrease = 300;
|
||||
increaseCombo = false;
|
||||
isBonus = true;
|
||||
bonusResult = HitResult.SmallBonus;
|
||||
break;
|
||||
|
||||
case Swell swell:
|
||||
// The taiko swell generally does not match the osu-stable implementation in any way.
|
||||
// We'll redo the calculations to match osu-stable here...
|
||||
double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(playableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5);
|
||||
double secondsDuration = swell.Duration / 1000;
|
||||
|
||||
// The amount of half spins that are required to successfully complete the spinner (i.e. get a 300).
|
||||
int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond);
|
||||
|
||||
halfSpinsRequiredForCompletion = (int)Math.Max(1, halfSpinsRequiredForCompletion * 1.65f);
|
||||
|
||||
if (mods.Any(m => m is ModDoubleTime))
|
||||
halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 0.75f));
|
||||
if (mods.Any(m => m is ModHalfTime))
|
||||
halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 1.5f));
|
||||
|
||||
for (int i = 0; i <= halfSpinsRequiredForCompletion; i++)
|
||||
simulateHit(new SwellTick());
|
||||
|
||||
scoreIncrease = 300;
|
||||
addScoreComboMultiplier = true;
|
||||
increaseCombo = false;
|
||||
isBonus = true;
|
||||
bonusResult = HitResult.LargeBonus;
|
||||
break;
|
||||
|
||||
case Hit:
|
||||
scoreIncrease = 300;
|
||||
addScoreComboMultiplier = true;
|
||||
break;
|
||||
|
||||
case DrumRoll:
|
||||
foreach (var nested in hitObject.NestedHitObjects)
|
||||
simulateHit(nested);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hitObject is DrumRollTick tick)
|
||||
{
|
||||
if (playableBeatmap.ControlPointInfo.EffectPointAt(tick.Parent.StartTime).KiaiMode)
|
||||
scoreIncrease = (int)(scoreIncrease * 1.2f);
|
||||
|
||||
if (tick.IsStrong)
|
||||
scoreIncrease += scoreIncrease / 5;
|
||||
}
|
||||
|
||||
// The score increase directly contributed to by the combo-multiplied portion.
|
||||
int comboScoreIncrease = 0;
|
||||
|
||||
if (addScoreComboMultiplier)
|
||||
{
|
||||
int oldScoreIncrease = scoreIncrease;
|
||||
|
||||
// ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...)
|
||||
scoreIncrease += (int)(scoreIncrease / 35 * 2 * (difficultyPeppyStars + 1) * modMultiplier) * (Math.Min(100, combo) / 10);
|
||||
|
||||
if (hitObject is Swell)
|
||||
{
|
||||
if (playableBeatmap.ControlPointInfo.EffectPointAt(hitObject.GetEndTime()).KiaiMode)
|
||||
scoreIncrease = (int)(scoreIncrease * 1.2f);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (playableBeatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode)
|
||||
scoreIncrease = (int)(scoreIncrease * 1.2f);
|
||||
}
|
||||
|
||||
comboScoreIncrease = scoreIncrease - oldScoreIncrease;
|
||||
}
|
||||
|
||||
if (hitObject is Swell || (hitObject is TaikoStrongableHitObject strongable && strongable.IsStrong))
|
||||
{
|
||||
scoreIncrease *= 2;
|
||||
comboScoreIncrease *= 2;
|
||||
}
|
||||
|
||||
scoreIncrease -= comboScoreIncrease;
|
||||
|
||||
if (addScoreComboMultiplier)
|
||||
ComboScore += comboScoreIncrease;
|
||||
|
||||
if (isBonus)
|
||||
{
|
||||
legacyBonusScore += scoreIncrease;
|
||||
modernBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||
}
|
||||
else
|
||||
AccuracyScore += scoreIncrease;
|
||||
|
||||
if (increaseCombo)
|
||||
combo++;
|
||||
}
|
||||
}
|
||||
}
|
@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss;
|
||||
|
||||
// TODO: The detection of rulesets is temporary until the leftover old skills have been reworked.
|
||||
bool isConvert = score.BeatmapInfo.Ruleset.OnlineID != 1;
|
||||
bool isConvert = score.BeatmapInfo!.Ruleset.OnlineID != 1;
|
||||
|
||||
double multiplier = 1.13;
|
||||
|
||||
|
@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
public partial class TaikoHitObjectComposer : HitObjectComposer<TaikoHitObject>
|
||||
{
|
||||
protected override bool ApplyHorizontalCentering => false;
|
||||
|
||||
public TaikoHitObjectComposer(TaikoRuleset ruleset)
|
||||
: base(ruleset)
|
||||
{
|
||||
|
@ -62,6 +62,8 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
hitObject.LifetimeEnd = state == ArmedState.Idle || !hitObject.AllJudged
|
||||
? hitObject.HitObject.GetEndTime() + hitObject.HitObject.HitWindows.WindowFor(HitResult.Miss)
|
||||
: hitObject.HitStateUpdateTime;
|
||||
// extend the lifetime end of the object in order to allow its nested strong hit (if any) to be judged.
|
||||
hitObject.LifetimeEnd += DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW;
|
||||
}
|
||||
|
||||
break;
|
||||
|
@ -195,14 +195,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
}
|
||||
|
||||
public override void OnKilled()
|
||||
{
|
||||
base.OnKilled();
|
||||
|
||||
if (Time.Current > ParentHitObject.HitObject.GetEndTime() && !Judged)
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
}
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;
|
||||
}
|
||||
}
|
||||
|
@ -108,14 +108,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
}
|
||||
|
||||
public override void OnKilled()
|
||||
{
|
||||
base.OnKilled();
|
||||
|
||||
if (Time.Current > ParentHitObject.HitObject.GetEndTime() && !Judged)
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
}
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;
|
||||
}
|
||||
}
|
||||
|
@ -4,14 +4,12 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||
@ -37,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
private bool validActionPressed;
|
||||
|
||||
private bool pressHandledThisFrame;
|
||||
private double? lastPressHandleTime;
|
||||
|
||||
private readonly Bindable<HitType> type = new Bindable<HitType>();
|
||||
|
||||
@ -78,7 +76,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
HitActions = null;
|
||||
HitAction = null;
|
||||
validActionPressed = pressHandledThisFrame = false;
|
||||
validActionPressed = false;
|
||||
lastPressHandleTime = null;
|
||||
}
|
||||
|
||||
private void updateActionsFromType()
|
||||
@ -93,40 +92,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
? new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit)
|
||||
: new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
|
||||
|
||||
public override IEnumerable<HitSampleInfo> GetSamples()
|
||||
{
|
||||
// normal and claps are always handled by the drum (see DrumSampleMapping).
|
||||
// in addition, whistles are excluded as they are an alternative rim marker.
|
||||
|
||||
var samples = HitObject.Samples.Where(s =>
|
||||
s.Name != HitSampleInfo.HIT_NORMAL
|
||||
&& s.Name != HitSampleInfo.HIT_CLAP
|
||||
&& s.Name != HitSampleInfo.HIT_WHISTLE);
|
||||
|
||||
if (HitObject.Type == HitType.Rim && HitObject.IsStrong)
|
||||
{
|
||||
// strong + rim always maps to whistle.
|
||||
// TODO: this should really be in the legacy decoder, but can't be because legacy encoding parity would be broken.
|
||||
// when we add a taiko editor, this is probably not going to play nice.
|
||||
|
||||
var corrected = samples.ToList();
|
||||
|
||||
for (int i = 0; i < corrected.Count; i++)
|
||||
{
|
||||
var s = corrected[i];
|
||||
|
||||
if (s.Name != HitSampleInfo.HIT_FINISH)
|
||||
continue;
|
||||
|
||||
corrected[i] = s.With(HitSampleInfo.HIT_WHISTLE);
|
||||
}
|
||||
|
||||
return corrected;
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
Debug.Assert(HitObject.HitWindows != null);
|
||||
@ -150,7 +115,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
|
||||
{
|
||||
if (pressHandledThisFrame)
|
||||
if (lastPressHandleTime == Time.Current)
|
||||
return true;
|
||||
if (Judged)
|
||||
return false;
|
||||
@ -164,7 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
// Regardless of whether we've hit or not, any secondary key presses in the same frame should be discarded
|
||||
// E.g. hitting a non-strong centre as a strong should not fall through and perform a hit on the next note
|
||||
pressHandledThisFrame = true;
|
||||
lastPressHandleTime = Time.Current;
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -175,15 +140,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
base.OnReleased(e);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// The input manager processes all input prior to us updating, so this is the perfect time
|
||||
// for us to remove the extra press blocking, before input is handled in the next frame
|
||||
pressHandledThisFrame = false;
|
||||
}
|
||||
|
||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||
{
|
||||
Debug.Assert(HitObject.HitWindows != null);
|
||||
@ -231,7 +187,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
/// The lenience for the second key press.
|
||||
/// This does not adjust by map difficulty in ScoreV2 yet.
|
||||
/// </summary>
|
||||
private const double second_hit_window = 30;
|
||||
public const double SECOND_HIT_WINDOW = 30;
|
||||
|
||||
public StrongNestedHit()
|
||||
: this(null)
|
||||
@ -259,12 +215,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
if (!userTriggered)
|
||||
{
|
||||
if (timeOffset - ParentHitObject.Result.TimeOffset > second_hit_window)
|
||||
if (timeOffset - ParentHitObject.Result.TimeOffset > SECOND_HIT_WINDOW)
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= second_hit_window)
|
||||
if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= SECOND_HIT_WINDOW)
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
}
|
||||
|
||||
|
@ -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 osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Judgements;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
@ -16,5 +17,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
: base(nestedHit)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnKilled()
|
||||
{
|
||||
base.OnKilled();
|
||||
|
||||
// usually, the strong nested hit isn't judged itself, it is judged by its parent object.
|
||||
// however, in rare cases (see: drum rolls, hits with hidden active),
|
||||
// it can happen that the hit window of the nested strong hit extends past the lifetime of the parent object.
|
||||
// this is a safety to prevent such cases from causing the nested hit to never be judged and as such prevent gameplay from completing.
|
||||
if (!Judged && Time.Current > ParentHitObject?.HitObject.GetEndTime())
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -276,6 +276,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
if (Time.Current < HitObject.StartTime)
|
||||
return false;
|
||||
|
||||
if (AllJudged)
|
||||
return false;
|
||||
|
||||
bool isCentre = e.Action == TaikoAction.LeftCentre || e.Action == TaikoAction.RightCentre;
|
||||
|
||||
// Ensure alternating centre and rim hits
|
||||
|
@ -119,8 +119,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
public override bool RemoveWhenNotAlive => false;
|
||||
}
|
||||
|
||||
// Most osu!taiko hitsounds are managed by the drum (see DrumSampleTriggerSource).
|
||||
public override IEnumerable<HitSampleInfo> GetSamples() => Enumerable.Empty<HitSampleInfo>();
|
||||
// osu!taiko hitsounds are managed by the drum (see DrumSampleTriggerSource).
|
||||
public sealed override IEnumerable<HitSampleInfo> GetSamples() => Enumerable.Empty<HitSampleInfo>();
|
||||
}
|
||||
|
||||
public abstract partial class DrawableTaikoHitObject<TObject> : DrawableTaikoHitObject
|
||||
|
@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
AddNested(new DrumRollTick
|
||||
AddNested(new DrumRollTick(this)
|
||||
{
|
||||
FirstTick = first,
|
||||
TickSpacing = tickSpacing,
|
||||
|
@ -9,6 +9,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
{
|
||||
public class DrumRollTick : TaikoStrongableHitObject
|
||||
{
|
||||
public readonly DrumRoll Parent;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this is the first (initial) tick of the slider.
|
||||
/// </summary>
|
||||
@ -25,6 +27,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
/// </summary>
|
||||
public double HitWindow => TickSpacing / 2;
|
||||
|
||||
public DrumRollTick(DrumRoll parent)
|
||||
{
|
||||
Parent = parent;
|
||||
}
|
||||
|
||||
public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement();
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
|
@ -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.Allocation;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
{
|
||||
internal partial class ArgonDrumSamplePlayer : DrumSamplePlayer
|
||||
{
|
||||
private ArgonFlourishTriggerSource argonFlourishTrigger = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(Playfield playfield, IPooledSampleProvider sampleProvider)
|
||||
{
|
||||
var hitObjectContainer = playfield.HitObjectContainer;
|
||||
|
||||
// Warm up pools for non-standard samples.
|
||||
sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_NORMAL), true));
|
||||
sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_CLAP), true));
|
||||
sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_FLOURISH), true));
|
||||
|
||||
// We want to play back flourishes in an isolated source as to not have them cancelled.
|
||||
AddInternal(argonFlourishTrigger = new ArgonFlourishTriggerSource(hitObjectContainer));
|
||||
}
|
||||
|
||||
protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) =>
|
||||
new ArgonDrumSampleTriggerSource(hitObjectContainer, balance);
|
||||
|
||||
protected override void Play(DrumSampleTriggerSource triggerSource, HitType hitType, bool strong)
|
||||
{
|
||||
base.Play(triggerSource, hitType, strong);
|
||||
|
||||
// This won't always play something, but the logic for flourish playback is contained within.
|
||||
argonFlourishTrigger.Play(hitType, strong);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
// 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.Game.Audio;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
{
|
||||
public partial class ArgonDrumSampleTriggerSource : DrumSampleTriggerSource
|
||||
{
|
||||
[Resolved]
|
||||
private ISkinSource skinSource { get; set; } = null!;
|
||||
|
||||
public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance)
|
||||
: base(hitObjectContainer, balance)
|
||||
{
|
||||
}
|
||||
|
||||
public override void Play(HitType hitType, bool strong)
|
||||
{
|
||||
TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject;
|
||||
|
||||
if (hitObject == null)
|
||||
return;
|
||||
|
||||
var originalSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL);
|
||||
|
||||
// If the sample is provided by a legacy skin, we should not try and do anything special.
|
||||
if (skinSource.FindProvider(s => s.GetSample(originalSample) != null) is LegacySkinTransformer)
|
||||
{
|
||||
base.Play(hitType, strong);
|
||||
return;
|
||||
}
|
||||
|
||||
// let the magic begin...
|
||||
var samplesToPlay = new List<ISampleInfo> { new VolumeAwareHitSampleInfo(originalSample, strong) };
|
||||
|
||||
PlaySamples(samplesToPlay.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
{
|
||||
internal partial class ArgonFlourishTriggerSource : DrumSampleTriggerSource
|
||||
{
|
||||
private readonly HitObjectContainer hitObjectContainer;
|
||||
|
||||
[Resolved]
|
||||
private ISkinSource skinSource { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum time to leave between flourishes that are added to strong rim hits.
|
||||
/// </summary>
|
||||
private const double time_between_flourishes = 2000;
|
||||
|
||||
public ArgonFlourishTriggerSource(HitObjectContainer hitObjectContainer)
|
||||
: base(hitObjectContainer)
|
||||
{
|
||||
this.hitObjectContainer = hitObjectContainer;
|
||||
}
|
||||
|
||||
public override void Play(HitType hitType, bool strong)
|
||||
{
|
||||
TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject;
|
||||
|
||||
if (hitObject == null)
|
||||
return;
|
||||
|
||||
var originalSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL);
|
||||
|
||||
// If the sample is provided by a legacy skin, we should not try and do anything special.
|
||||
if (skinSource.FindProvider(s => s.GetSample(originalSample) != null) is LegacySkinTransformer)
|
||||
return;
|
||||
|
||||
if (strong && hitType == HitType.Rim && canPlayFlourish(hitObject))
|
||||
PlaySamples(new ISampleInfo[] { new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(HitSampleInfo.HIT_FLOURISH), true) });
|
||||
}
|
||||
|
||||
private bool canPlayFlourish(TaikoHitObject hitObject)
|
||||
{
|
||||
double? lastFlourish = null;
|
||||
|
||||
var hitObjects = hitObjectContainer.AliveObjects
|
||||
.Reverse()
|
||||
.Select(d => d.HitObject)
|
||||
.OfType<Hit>()
|
||||
.Where(h => h.IsStrong && h.Type == HitType.Rim);
|
||||
|
||||
// Add an additional 'flourish' sample to strong rim hits (that are at least `time_between_flourishes` apart).
|
||||
// This is applied to hitobjects in reverse order, as to sound more musically coherent by biasing towards to
|
||||
// end of groups/combos of strong rim hits instead of the start.
|
||||
foreach (var h in hitObjects)
|
||||
{
|
||||
bool canFlourish = lastFlourish == null || lastFlourish - h.StartTime >= time_between_flourishes;
|
||||
|
||||
if (canFlourish)
|
||||
lastFlourish = h.StartTime;
|
||||
|
||||
// hitObject can be either the strong hit itself (if hit late), or its nested strong object (if hit early)
|
||||
// due to `GetMostValidObject()` idiosyncrasies.
|
||||
// whichever it is, if we encounter it during iteration, stop looking.
|
||||
if (h == hitObject || h.NestedHitObjects.Contains(hitObject))
|
||||
return canFlourish;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -60,6 +60,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
// the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield.
|
||||
return Drawable.Empty().With(d => d.Expire());
|
||||
|
||||
case TaikoSkinComponents.DrumSamplePlayer:
|
||||
return new ArgonDrumSamplePlayer();
|
||||
|
||||
case TaikoSkinComponents.TaikoExplosionGreat:
|
||||
case TaikoSkinComponents.TaikoExplosionMiss:
|
||||
case TaikoSkinComponents.TaikoExplosionOk:
|
||||
|
@ -0,0 +1,52 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Audio;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
{
|
||||
public class VolumeAwareHitSampleInfo : HitSampleInfo
|
||||
{
|
||||
public const int SAMPLE_VOLUME_THRESHOLD_HARD = 90;
|
||||
public const int SAMPLE_VOLUME_THRESHOLD_MEDIUM = 60;
|
||||
|
||||
public VolumeAwareHitSampleInfo(HitSampleInfo sampleInfo, bool isStrong = false)
|
||||
: base(sampleInfo.Name, isStrong ? BANK_STRONG : getBank(sampleInfo.Bank, sampleInfo.Name, sampleInfo.Volume), sampleInfo.Suffix, sampleInfo.Volume)
|
||||
{
|
||||
}
|
||||
|
||||
public override IEnumerable<string> LookupNames
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (string name in base.LookupNames)
|
||||
yield return name.Insert(name.LastIndexOf('/') + 1, "Argon/taiko-");
|
||||
}
|
||||
}
|
||||
|
||||
private static string getBank(string originalBank, string sampleName, int volume)
|
||||
{
|
||||
// So basically we're overwriting mapper's bank intentions here.
|
||||
// The rationale is that most taiko beatmaps only use a single bank, but regularly adjust volume.
|
||||
|
||||
switch (sampleName)
|
||||
{
|
||||
case HIT_NORMAL:
|
||||
case HIT_CLAP:
|
||||
{
|
||||
if (volume >= SAMPLE_VOLUME_THRESHOLD_HARD)
|
||||
return BANK_DRUM;
|
||||
|
||||
if (volume >= SAMPLE_VOLUME_THRESHOLD_MEDIUM)
|
||||
return BANK_NORMAL;
|
||||
|
||||
return BANK_SOFT;
|
||||
}
|
||||
|
||||
default:
|
||||
return originalBank;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -8,6 +9,7 @@ using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
@ -26,11 +28,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
private Bindable<int> currentCombo { get; } = new BindableInt();
|
||||
|
||||
private int animationFrame;
|
||||
private double beatLength;
|
||||
|
||||
// required for editor blueprints (not sure why these circle pieces are zero size).
|
||||
public override Quad ScreenSpaceDrawQuad => backgroundLayer.ScreenSpaceDrawQuad;
|
||||
|
||||
private TimingControlPoint timingPoint = TimingControlPoint.DEFAULT;
|
||||
|
||||
public LegacyCirclePiece()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
@ -39,11 +42,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
[Resolved(canBeNull: true)]
|
||||
private GameplayState? gameplayState { get; set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private IBeatSyncProvider? beatSyncProvider { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin, DrawableHitObject drawableHitObject)
|
||||
private void load(ISkinSource skin, DrawableHitObject drawableHitObject, IBeatSyncProvider? beatSyncProvider)
|
||||
{
|
||||
Drawable? getDrawableFor(string lookup)
|
||||
{
|
||||
@ -64,6 +64,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
if (foregroundLayer != null)
|
||||
AddInternal(foregroundLayer);
|
||||
|
||||
drawableHitObject.StartTimeBindable.BindValueChanged(startTime =>
|
||||
{
|
||||
timingPoint = beatSyncProvider?.ControlPoints?.TimingPointAt(startTime.NewValue) ?? TimingControlPoint.DEFAULT;
|
||||
}, true);
|
||||
|
||||
// Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat).
|
||||
// For now just stop at first frame for sanity.
|
||||
foreach (var c in InternalChildren)
|
||||
@ -115,14 +120,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
return;
|
||||
}
|
||||
|
||||
if (beatSyncProvider?.ControlPoints != null)
|
||||
{
|
||||
beatLength = beatSyncProvider.ControlPoints.TimingPointAt(Time.Current).BeatLength;
|
||||
|
||||
animationFrame = Time.Current % ((beatLength * 2) / multiplier) >= beatLength / multiplier ? 0 : 1;
|
||||
|
||||
animatableForegroundLayer.GotoFrame(animationFrame);
|
||||
}
|
||||
animationFrame = Math.Abs(Time.Current - timingPoint.Time) % ((timingPoint.BeatLength * 2) / multiplier) >= timingPoint.BeatLength / multiplier ? 0 : 1;
|
||||
animatableForegroundLayer.GotoFrame(animationFrame);
|
||||
}
|
||||
|
||||
private Color4 accentColour;
|
||||
|
@ -52,6 +52,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.DrumSamplePlayer:
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.CentreHit:
|
||||
case TaikoSkinComponents.RimHit:
|
||||
if (hasHitCircle)
|
||||
|
@ -116,6 +116,9 @@ namespace osu.Game.Rulesets.Taiko
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.Random))
|
||||
yield return new TaikoModRandom();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.ScoreV2))
|
||||
yield return new ModScoreV2();
|
||||
}
|
||||
|
||||
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
|
||||
@ -176,6 +179,12 @@ namespace osu.Game.Rulesets.Taiko
|
||||
new ModAdaptiveSpeed()
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
return new Mod[]
|
||||
{
|
||||
new ModScoreV2(),
|
||||
};
|
||||
|
||||
default:
|
||||
return Array.Empty<Mod>();
|
||||
}
|
||||
@ -197,6 +206,8 @@ namespace osu.Game.Rulesets.Taiko
|
||||
|
||||
public int LegacyID => 1;
|
||||
|
||||
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new TaikoLegacyScoreSimulator();
|
||||
|
||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
|
||||
|
||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TaikoRulesetConfigManager(settings, RulesetInfo);
|
||||
@ -245,7 +256,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 250
|
||||
}, true),
|
||||
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
||||
new StatisticItem("Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[]
|
||||
{
|
||||
new AverageHitError(timedHitEvents),
|
||||
new UnstableRate(timedHitEvents)
|
||||
|
@ -21,6 +21,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
TaikoExplosionKiai,
|
||||
Scroller,
|
||||
Mascot,
|
||||
KiaiGlow
|
||||
KiaiGlow,
|
||||
DrumSamplePlayer
|
||||
}
|
||||
}
|
||||
|
@ -1,57 +1,151 @@
|
||||
// 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.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.UI
|
||||
{
|
||||
internal partial class DrumSamplePlayer : CompositeDrawable, IKeyBindingHandler<TaikoAction>
|
||||
{
|
||||
private readonly DrumSampleTriggerSource leftRimSampleTriggerSource;
|
||||
private readonly DrumSampleTriggerSource leftCentreSampleTriggerSource;
|
||||
private readonly DrumSampleTriggerSource rightCentreSampleTriggerSource;
|
||||
private readonly DrumSampleTriggerSource rightRimSampleTriggerSource;
|
||||
private DrumSampleTriggerSource leftCentreTrigger = null!;
|
||||
private DrumSampleTriggerSource rightCentreTrigger = null!;
|
||||
private DrumSampleTriggerSource leftRimTrigger = null!;
|
||||
private DrumSampleTriggerSource rightRimTrigger = null!;
|
||||
private DrumSampleTriggerSource strongCentreTrigger = null!;
|
||||
private DrumSampleTriggerSource strongRimTrigger = null!;
|
||||
|
||||
public DrumSamplePlayer(HitObjectContainer hitObjectContainer)
|
||||
private double lastHitTime;
|
||||
private TaikoAction? lastAction;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(Playfield playfield)
|
||||
{
|
||||
var hitObjectContainer = playfield.HitObjectContainer;
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
leftRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
|
||||
leftCentreSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
|
||||
rightCentreSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
|
||||
rightRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
|
||||
leftCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Left),
|
||||
rightCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Right),
|
||||
leftRimTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Left),
|
||||
rightRimTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Right),
|
||||
strongCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Centre),
|
||||
strongRimTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Centre)
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance)
|
||||
=> new DrumSampleTriggerSource(hitObjectContainer);
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
|
||||
{
|
||||
if ((Clock as IGameplayClock)?.IsRewinding == true)
|
||||
return false;
|
||||
|
||||
HitType hitType;
|
||||
|
||||
DrumSampleTriggerSource triggerSource;
|
||||
|
||||
bool strong = checkStrongValidity(e.Action, lastAction, Time.Current - lastHitTime);
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case TaikoAction.LeftRim:
|
||||
leftRimSampleTriggerSource.Play(HitType.Rim);
|
||||
break;
|
||||
|
||||
case TaikoAction.LeftCentre:
|
||||
leftCentreSampleTriggerSource.Play(HitType.Centre);
|
||||
hitType = HitType.Centre;
|
||||
triggerSource = strong ? strongCentreTrigger : leftCentreTrigger;
|
||||
break;
|
||||
|
||||
case TaikoAction.RightCentre:
|
||||
rightCentreSampleTriggerSource.Play(HitType.Centre);
|
||||
hitType = HitType.Centre;
|
||||
triggerSource = strong ? strongCentreTrigger : rightCentreTrigger;
|
||||
break;
|
||||
|
||||
case TaikoAction.LeftRim:
|
||||
hitType = HitType.Rim;
|
||||
triggerSource = strong ? strongRimTrigger : leftRimTrigger;
|
||||
break;
|
||||
|
||||
case TaikoAction.RightRim:
|
||||
rightRimSampleTriggerSource.Play(HitType.Rim);
|
||||
hitType = HitType.Rim;
|
||||
triggerSource = strong ? strongRimTrigger : rightRimTrigger;
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strong)
|
||||
{
|
||||
switch (hitType)
|
||||
{
|
||||
case HitType.Centre:
|
||||
flushCenterTriggerSources();
|
||||
break;
|
||||
|
||||
case HitType.Rim:
|
||||
flushRimTriggerSources();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Play(triggerSource, hitType, strong);
|
||||
|
||||
lastHitTime = Time.Current;
|
||||
lastAction = e.Action;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected virtual void Play(DrumSampleTriggerSource triggerSource, HitType hitType, bool strong) =>
|
||||
triggerSource.Play(hitType, strong);
|
||||
|
||||
private bool checkStrongValidity(TaikoAction newAction, TaikoAction? lastAction, double timeBetweenActions)
|
||||
{
|
||||
if (lastAction == null)
|
||||
return false;
|
||||
|
||||
if (timeBetweenActions < 0 || timeBetweenActions > DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW)
|
||||
return false;
|
||||
|
||||
switch (newAction)
|
||||
{
|
||||
case TaikoAction.LeftCentre:
|
||||
return lastAction == TaikoAction.RightCentre;
|
||||
|
||||
case TaikoAction.RightCentre:
|
||||
return lastAction == TaikoAction.LeftCentre;
|
||||
|
||||
case TaikoAction.LeftRim:
|
||||
return lastAction == TaikoAction.RightRim;
|
||||
|
||||
case TaikoAction.RightRim:
|
||||
return lastAction == TaikoAction.LeftRim;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void flushCenterTriggerSources()
|
||||
{
|
||||
leftCentreTrigger.StopAllPlayback();
|
||||
rightCentreTrigger.StopAllPlayback();
|
||||
strongCentreTrigger.StopAllPlayback();
|
||||
}
|
||||
|
||||
private void flushRimTriggerSources()
|
||||
{
|
||||
leftRimTrigger.StopAllPlayback();
|
||||
rightRimTrigger.StopAllPlayback();
|
||||
strongRimTrigger.StopAllPlayback();
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<TaikoAction> e)
|
||||
{
|
||||
}
|
||||
|
@ -2,30 +2,74 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.UI
|
||||
{
|
||||
public partial class DrumSampleTriggerSource : GameplaySampleTriggerSource
|
||||
{
|
||||
public DrumSampleTriggerSource(HitObjectContainer hitObjectContainer)
|
||||
private const double stereo_separation = 0.2;
|
||||
|
||||
public DrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance = SampleBalance.Centre)
|
||||
: base(hitObjectContainer)
|
||||
{
|
||||
switch (balance)
|
||||
{
|
||||
case SampleBalance.Left:
|
||||
AudioContainer.Balance.Value = -stereo_separation;
|
||||
break;
|
||||
|
||||
case SampleBalance.Centre:
|
||||
AudioContainer.Balance.Value = 0;
|
||||
break;
|
||||
|
||||
case SampleBalance.Right:
|
||||
AudioContainer.Balance.Value = stereo_separation;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void Play(HitType hitType)
|
||||
public virtual void Play(HitType hitType, bool strong)
|
||||
{
|
||||
var hitSample = GetMostValidObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
|
||||
TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject;
|
||||
|
||||
if (hitSample == null)
|
||||
if (hitObject == null)
|
||||
return;
|
||||
|
||||
PlaySamples(new ISampleInfo[] { new HitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL, hitSample.Bank, volume: hitSample.Volume) });
|
||||
var baseSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL);
|
||||
|
||||
if (strong)
|
||||
{
|
||||
PlaySamples(new ISampleInfo[]
|
||||
{
|
||||
baseSample,
|
||||
hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH)
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
PlaySamples(new ISampleInfo[] { baseSample });
|
||||
}
|
||||
}
|
||||
|
||||
public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead");
|
||||
|
||||
protected override void ApplySampleInfo(SkinnableSound hitSound, ISampleInfo[] samples)
|
||||
{
|
||||
base.ApplySampleInfo(hitSound, samples);
|
||||
|
||||
hitSound.Balance.Value = -0.05 + RNG.NextDouble(0.1);
|
||||
}
|
||||
}
|
||||
|
||||
public enum SampleBalance
|
||||
{
|
||||
Left,
|
||||
Centre,
|
||||
Right
|
||||
}
|
||||
}
|
||||
|
@ -170,7 +170,10 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
drumRollHitContainer.CreateProxy(),
|
||||
new DrumSamplePlayer(HitObjectContainer),
|
||||
new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumSamplePlayer), _ => new DrumSamplePlayer())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
// this is added at the end of the hierarchy to receive input before taiko objects.
|
||||
// but is proxied below everything to not cover visual effects such as hit explosions.
|
||||
inputDrum,
|
||||
|
@ -5,7 +5,7 @@
|
||||
<key>CFBundleName</key>
|
||||
<string>osu.Game.Tests.iOS</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>ppy.osu-Game-Tests-iOS</string>
|
||||
<string>sh.ppy.osu-tests</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
@ -64,7 +64,7 @@ namespace osu.Game.Tests.Beatmaps
|
||||
[Test]
|
||||
public void TestCachedRetrievalWithFiles() => AddStep("run test", () =>
|
||||
{
|
||||
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID).Detach());
|
||||
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID)!.Detach());
|
||||
|
||||
Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0));
|
||||
|
||||
@ -90,7 +90,7 @@ namespace osu.Game.Tests.Beatmaps
|
||||
[Test]
|
||||
public void TestForcedRefetchRetrievalWithFiles() => AddStep("run test", () =>
|
||||
{
|
||||
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID).Detach());
|
||||
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID)!.Detach());
|
||||
|
||||
Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0));
|
||||
|
||||
@ -102,7 +102,7 @@ namespace osu.Game.Tests.Beatmaps
|
||||
[Test]
|
||||
public void TestSavePreservesCollections() => AddStep("run test", () =>
|
||||
{
|
||||
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID).Detach());
|
||||
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID)!.Detach());
|
||||
|
||||
var working = beatmaps.GetWorkingBeatmap(beatmap);
|
||||
|
||||
|
@ -478,8 +478,8 @@ namespace osu.Game.Tests.Chat
|
||||
Content = "This is a [http://www.simple-test.com simple test] with some [traps] and [[wiki links]]. Don't forget to visit https://osu.ppy.sh (now!)[http://google.com]\uD83D\uDE12"
|
||||
});
|
||||
|
||||
Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now!\0\0\0", result.DisplayContent);
|
||||
Assert.AreEqual(5, result.Links.Count);
|
||||
Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now![emoji]", result.DisplayContent);
|
||||
Assert.AreEqual(4, result.Links.Count);
|
||||
|
||||
Link f = result.Links.Find(l => l.Url == "https://dev.ppy.sh/wiki/wiki links");
|
||||
Assert.That(f, Is.Not.Null);
|
||||
@ -500,27 +500,22 @@ namespace osu.Game.Tests.Chat
|
||||
Assert.That(f, Is.Not.Null);
|
||||
Assert.AreEqual(78, f.Index);
|
||||
Assert.AreEqual(18, f.Length);
|
||||
|
||||
f = result.Links.Find(l => l.Url == "\uD83D\uDE12");
|
||||
Assert.That(f, Is.Not.Null);
|
||||
Assert.AreEqual(101, f.Index);
|
||||
Assert.AreEqual(3, f.Length);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmoji()
|
||||
{
|
||||
Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" });
|
||||
Assert.AreEqual("Hello world\0\0\0<--This is an emoji,There are more:\0\0\0\0\0\0,\0\0\0", result.DisplayContent);
|
||||
Assert.AreEqual(result.Links.Count, 4);
|
||||
Assert.AreEqual(result.Links[0].Index, 11);
|
||||
Assert.AreEqual(result.Links[1].Index, 49);
|
||||
Assert.AreEqual(result.Links[2].Index, 52);
|
||||
Assert.AreEqual(result.Links[3].Index, 56);
|
||||
Assert.AreEqual(result.Links[0].Url, "\uD83D\uDE12");
|
||||
Assert.AreEqual(result.Links[1].Url, "\uD83D\uDE10");
|
||||
Assert.AreEqual(result.Links[2].Url, "\uD83D\uDE00");
|
||||
Assert.AreEqual(result.Links[3].Url, "\uD83D\uDE20");
|
||||
Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more emojis among us:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" });
|
||||
Assert.AreEqual("Hello world[emoji]<--This is an emoji,There are more emojis among us:[emoji][emoji],[emoji]", result.DisplayContent);
|
||||
Assert.AreEqual(result.Links.Count, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmojiWithSuccessiveParens()
|
||||
{
|
||||
Message result = MessageFormatter.FormatMessage(new Message { Content = "\uD83D\uDE10(let's hope this doesn't accidentally turn into a link)" });
|
||||
Assert.AreEqual("[emoji](let's hope this doesn't accidentally turn into a link)", result.DisplayContent);
|
||||
Assert.AreEqual(result.Links.Count, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -42,7 +42,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
return Realm.Run(r =>
|
||||
{
|
||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
||||
});
|
||||
});
|
||||
@ -51,7 +51,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
Realm.Write(r =>
|
||||
{
|
||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||
foreach (var b in beatmapSetInfo.Beatmaps)
|
||||
b.StarRating = -1;
|
||||
});
|
||||
@ -66,7 +66,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
return Realm.Run(r =>
|
||||
{
|
||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
||||
});
|
||||
});
|
||||
@ -79,7 +79,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
return Realm.Run(r =>
|
||||
{
|
||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
||||
});
|
||||
});
|
||||
@ -90,7 +90,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
Realm.Write(r =>
|
||||
{
|
||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||
foreach (var b in beatmapSetInfo.Beatmaps)
|
||||
b.StarRating = -1;
|
||||
});
|
||||
@ -107,7 +107,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
return Realm.Run(r =>
|
||||
{
|
||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||
return beatmapSetInfo.Beatmaps.All(b => b.StarRating == -1);
|
||||
});
|
||||
});
|
||||
@ -118,7 +118,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
return Realm.Run(r =>
|
||||
{
|
||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
||||
});
|
||||
});
|
||||
|
@ -417,6 +417,60 @@ namespace osu.Game.Tests.Database
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImport_Modify_Revert()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||
|
||||
await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First());
|
||||
|
||||
var score = realm.Run(r => r.All<ScoreInfo>().Single());
|
||||
|
||||
string originalHash = imported.Beatmaps.First().Hash;
|
||||
const string modified_hash = "new_hash";
|
||||
|
||||
Assert.That(imported.Beatmaps.First().Scores.Single(), Is.EqualTo(score));
|
||||
|
||||
Assert.That(score.BeatmapHash, Is.EqualTo(originalHash));
|
||||
Assert.That(score.BeatmapInfo, Is.EqualTo(imported.Beatmaps.First()));
|
||||
|
||||
// imitate making local changes via editor
|
||||
// ReSharper disable once MethodHasAsyncOverload
|
||||
realm.Write(r =>
|
||||
{
|
||||
BeatmapInfo beatmap = imported.Beatmaps.First();
|
||||
beatmap.Hash = modified_hash;
|
||||
beatmap.ResetOnlineInfo();
|
||||
beatmap.UpdateLocalScores(r);
|
||||
});
|
||||
|
||||
Assert.That(!imported.Beatmaps.First().Scores.Any());
|
||||
|
||||
Assert.That(score.BeatmapInfo, Is.Null);
|
||||
Assert.That(score.BeatmapHash, Is.EqualTo(originalHash));
|
||||
|
||||
// imitate reverting the local changes made above
|
||||
// ReSharper disable once MethodHasAsyncOverload
|
||||
realm.Write(r =>
|
||||
{
|
||||
BeatmapInfo beatmap = imported.Beatmaps.First();
|
||||
beatmap.Hash = originalHash;
|
||||
beatmap.ResetOnlineInfo();
|
||||
beatmap.UpdateLocalScores(r);
|
||||
});
|
||||
|
||||
Assert.That(imported.Beatmaps.First().Scores.Single(), Is.EqualTo(score));
|
||||
|
||||
Assert.That(score.BeatmapHash, Is.EqualTo(originalHash));
|
||||
Assert.That(score.BeatmapInfo, Is.EqualTo(imported.Beatmaps.First()));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImport_ThenModifyMapWithScore_ThenImport()
|
||||
{
|
||||
@ -431,19 +485,19 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First());
|
||||
|
||||
Assert.That(imported.Beatmaps.First().Scores.Any());
|
||||
|
||||
// imitate making local changes via editor
|
||||
// ReSharper disable once MethodHasAsyncOverload
|
||||
realm.Write(_ =>
|
||||
realm.Write(r =>
|
||||
{
|
||||
BeatmapInfo beatmap = imported.Beatmaps.First();
|
||||
beatmap.Hash = "new_hash";
|
||||
beatmap.ResetOnlineInfo();
|
||||
beatmap.UpdateLocalScores(r);
|
||||
});
|
||||
|
||||
// for now, making changes to a beatmap doesn't remove the backlink from the score to the beatmap.
|
||||
// the logic of ensuring that scores match the beatmap is upheld via comparing the hash in usages (see: https://github.com/ppy/osu/pull/22539).
|
||||
// TODO: revisit when fixing https://github.com/ppy/osu/issues/24069.
|
||||
Assert.That(imported.Beatmaps.First().Scores.Any());
|
||||
Assert.That(!imported.Beatmaps.First().Scores.Any());
|
||||
|
||||
var importedSecondTime = await importer.Import(new ImportTask(temp));
|
||||
|
||||
@ -461,6 +515,7 @@ namespace osu.Game.Tests.Database
|
||||
Assert.That(importedFirstTimeBeatmap.Hash != importedSecondTimeBeatmap.Hash);
|
||||
Assert.That(!importedFirstTimeBeatmap.Scores.Any());
|
||||
Assert.That(importedSecondTimeBeatmap.Scores.Count() == 1);
|
||||
Assert.That(importedSecondTimeBeatmap.Scores.Single().BeatmapInfo, Is.EqualTo(importedSecondTimeBeatmap));
|
||||
});
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user