1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-13 07:22:54 +08:00

Merge branch 'master' into tourney-switching-ui

This commit is contained in:
Shivam 2020-12-26 15:45:29 +01:00
commit a933483848
395 changed files with 10296 additions and 3266 deletions

View File

@ -1,7 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="osu! SDL" type="DotNetProject" factoryName=".NET Project" folderName="osu!" activateToolWindowBeforeRun="false">
<configuration default="false" name="osu! (legacy osuTK)" type="DotNetProject" factoryName=".NET Project" folderName="osu!" activateToolWindowBeforeRun="false">
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/netcoreapp3.1/osu!.dll" />
<option name="PROGRAM_PARAMETERS" value="--sdl" />
<option name="PROGRAM_PARAMETERS" value="--tk" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/netcoreapp3.1" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />

View File

@ -52,6 +52,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1203.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1222.0" />
</ItemGroup>
</Project>

View File

@ -1,18 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Net;
using Android.OS;
using Android.Provider;
using Android.Views;
using osu.Framework.Android;
namespace osu.Android
{
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)]
[IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk" }, DataMimeType = "application/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })]
public class OsuGameActivity : AndroidGameActivity
{
protected override Framework.Game CreateGame() => new OsuGameAndroid(this);
private static readonly string[] osu_url_schemes = { "osu", "osump" };
private OsuGameAndroid game;
protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this);
protected override void OnCreate(Bundle savedInstanceState)
{
@ -23,8 +35,60 @@ namespace osu.Android
base.OnCreate(savedInstanceState);
// OnNewIntent() only fires for an activity if it's *re-launched* while it's on top of the activity stack.
// on first launch we still have to fire manually.
// reference: https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent)
handleIntent(Intent);
Window.AddFlags(WindowManagerFlags.Fullscreen);
Window.AddFlags(WindowManagerFlags.KeepScreenOn);
}
protected override void OnNewIntent(Intent intent) => handleIntent(intent);
private void handleIntent(Intent intent)
{
switch (intent.Action)
{
case Intent.ActionDefault:
if (intent.Scheme == ContentResolver.SchemeContent)
handleImportFromUri(intent.Data);
else if (osu_url_schemes.Contains(intent.Scheme))
game.HandleLink(intent.DataString);
break;
case Intent.ActionSend:
{
var content = intent.ClipData?.GetItemAt(0);
if (content != null)
handleImportFromUri(content.Uri);
break;
}
}
}
private void handleImportFromUri(Uri uri) => Task.Factory.StartNew(async () =>
{
// there are more performant overloads of this method, but this one is the most backwards-compatible
// (dates back to API 1).
var cursor = ContentResolver?.Query(uri, null, null, null, null);
if (cursor == null)
return;
cursor.MoveToFirst();
var filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName);
string filename = cursor.GetString(filenameColumn);
// SharpCompress requires archive streams to be seekable, which the stream opened by
// OpenInputStream() seems to not necessarily be.
// copy to an arbitrary-access memory stream to be able to proceed with the import.
var copy = new MemoryStream();
using (var stream = ContentResolver.OpenInputStream(uri))
await stream.CopyToAsync(copy);
await game.Import(copy, filename);
}, TaskCreationOptions.LongRunning);
}
}

View File

@ -26,7 +26,7 @@ namespace osu.Desktop
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
private Bindable<User> user;
private IBindable<User> user;
private readonly IBindable<UserStatus> status = new Bindable<UserStatus>();
private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>();

View File

@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Catch.Tests
public float Position
{
get => HitObject?.X ?? position;
get => HitObject?.EffectiveX ?? position;
set => position = value;
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@ -12,8 +13,12 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests
@ -24,7 +29,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[Resolved]
private OsuConfigManager config { get; set; }
private Container droppedObjectContainer;
private Container<CaughtObject> droppedObjectContainer;
private TestCatcher catcher;
@ -37,7 +42,7 @@ namespace osu.Game.Rulesets.Catch.Tests
};
var trailContainer = new Container();
droppedObjectContainer = new Container();
droppedObjectContainer = new Container<CaughtObject>();
catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty);
Child = new Container
@ -52,6 +57,50 @@ namespace osu.Game.Rulesets.Catch.Tests
};
});
[Test]
public void TestCatcherHyperStateReverted()
{
DrawableCatchHitObject drawableObject1 = null;
DrawableCatchHitObject drawableObject2 = null;
JudgementResult result1 = null;
JudgementResult result2 = null;
AddStep("catch hyper fruit", () =>
{
attemptCatch(new Fruit { HyperDashTarget = new Fruit { X = 100 } }, out drawableObject1, out result1);
});
AddStep("catch normal fruit", () =>
{
attemptCatch(new Fruit(), out drawableObject2, out result2);
});
AddStep("revert second result", () =>
{
catcher.OnRevertResult(drawableObject2, result2);
});
checkHyperDash(true);
AddStep("revert first result", () =>
{
catcher.OnRevertResult(drawableObject1, result1);
});
checkHyperDash(false);
}
[Test]
public void TestCatcherAnimationStateReverted()
{
DrawableCatchHitObject drawableObject = null;
JudgementResult result = null;
AddStep("catch kiai fruit", () =>
{
attemptCatch(new TestKiaiFruit(), out drawableObject, out result);
});
checkState(CatcherAnimationState.Kiai);
AddStep("revert result", () =>
{
catcher.OnRevertResult(drawableObject, result);
});
checkState(CatcherAnimationState.Idle);
}
[Test]
public void TestCatcherCatchWidth()
{
@ -149,13 +198,22 @@ namespace osu.Game.Rulesets.Catch.Tests
AddAssert("fruits are dropped", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10);
}
[TestCase(true)]
[TestCase(false)]
public void TestHitLighting(bool enabled)
[Test]
public void TestHitLightingColour()
{
AddStep($"{(enabled ? "enable" : "disable")} hit lighting", () => config.Set(OsuSetting.HitLighting, enabled));
var fruitColour = SkinConfiguration.DefaultComboColours[1];
AddStep("enable hit lighting", () => config.Set(OsuSetting.HitLighting, true));
AddStep("catch fruit", () => attemptCatch(new Fruit()));
AddAssert("check hit lighting", () => catcher.ChildrenOfType<HitExplosion>().Any() == enabled);
AddAssert("correct hit lighting colour", () =>
catcher.ChildrenOfType<HitExplosion>().First()?.ObjectColour == fruitColour);
}
[Test]
public void TestHitLightingDisabled()
{
AddStep("disable hit lighting", () => config.Set(OsuSetting.HitLighting, false));
AddStep("catch fruit", () => attemptCatch(new Fruit()));
AddAssert("no hit lighting", () => !catcher.ChildrenOfType<HitExplosion>().Any());
}
private void checkPlate(int count) => AddAssert($"{count} objects on the plate", () => catcher.CaughtObjects.Count() == count);
@ -166,17 +224,60 @@ namespace osu.Game.Rulesets.Catch.Tests
private void attemptCatch(CatchHitObject hitObject, int count = 1)
{
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
for (var i = 0; i < count; i++)
catcher.AttemptCatch(hitObject);
attemptCatch(hitObject, out _, out _);
}
private void attemptCatch(CatchHitObject hitObject, out DrawableCatchHitObject drawableObject, out JudgementResult result)
{
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
drawableObject = createDrawableObject(hitObject);
result = createResult(hitObject);
applyResult(drawableObject, result);
}
private void applyResult(DrawableCatchHitObject drawableObject, JudgementResult result)
{
// Load DHO to set colour of hit explosion correctly
Add(drawableObject);
drawableObject.OnLoadComplete += _ =>
{
catcher.OnNewResult(drawableObject, result);
drawableObject.Expire();
};
}
private JudgementResult createResult(CatchHitObject hitObject)
{
return new CatchJudgementResult(hitObject, hitObject.CreateJudgement())
{
Type = catcher.CanCatch(hitObject) ? HitResult.Great : HitResult.Miss
};
}
private DrawableCatchHitObject createDrawableObject(CatchHitObject hitObject)
{
switch (hitObject)
{
case Banana banana:
return new DrawableBanana(banana);
case Droplet droplet:
return new DrawableDroplet(droplet);
case Fruit fruit:
return new DrawableFruit(fruit);
default:
throw new ArgumentOutOfRangeException(nameof(hitObject));
}
}
public class TestCatcher : Catcher
{
public IEnumerable<DrawablePalpableCatchHitObject> CaughtObjects => this.ChildrenOfType<DrawablePalpableCatchHitObject>();
public IEnumerable<CaughtObject> CaughtObjects => this.ChildrenOfType<CaughtObject>();
public TestCatcher(Container trailsTarget, Container droppedObjectTarget, BeatmapDifficulty difficulty)
public TestCatcher(Container trailsTarget, Container<CaughtObject> droppedObjectTarget, BeatmapDifficulty difficulty)
: base(trailsTarget, droppedObjectTarget, difficulty)
{
}

View File

@ -15,7 +15,6 @@ using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Tests
@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private void attemptCatch(Fruit fruit)
{
fruit.X += catcher.X;
fruit.X = fruit.OriginalX + catcher.X;
fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty
{
CircleSize = circleSize
@ -58,10 +57,9 @@ namespace osu.Game.Rulesets.Catch.Tests
Schedule(() =>
{
bool caught = area.AttemptCatch(fruit);
area.OnNewResult(drawable, new JudgementResult(fruit, new CatchJudgement())
area.OnNewResult(drawable, new CatchJudgementResult(fruit, new CatchJudgement())
{
Type = caught ? HitResult.Great : HitResult.Miss
Type = area.MovableCatcher.CanCatch(fruit) ? HitResult.Great : HitResult.Miss
});
drawable.Expire();
@ -75,7 +73,10 @@ namespace osu.Game.Rulesets.Catch.Tests
SetContents(() =>
{
var droppedObjectContainer = new Container();
var droppedObjectContainer = new Container<CaughtObject>
{
RelativeSizeAxes = Axes.Both
};
return new CatchInputManager(catchRuleset)
{
@ -101,7 +102,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private class TestCatcherArea : CatcherArea
{
public TestCatcherArea(Container droppedObjectContainer, BeatmapDifficulty beatmapDifficulty)
public TestCatcherArea(Container<CaughtObject> droppedObjectContainer, BeatmapDifficulty beatmapDifficulty)
: base(droppedObjectContainer, beatmapDifficulty)
{
}

View File

@ -5,19 +5,20 @@ using NUnit.Framework;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneFruitRandomness : OsuTestScene
{
private readonly TestDrawableFruit drawableFruit;
private readonly TestDrawableBanana drawableBanana;
private readonly DrawableFruit drawableFruit;
private readonly DrawableBanana drawableBanana;
public TestSceneFruitRandomness()
{
drawableFruit = new TestDrawableFruit(new Fruit());
drawableBanana = new TestDrawableBanana(new Banana());
drawableFruit = new DrawableFruit(new Fruit());
drawableBanana = new DrawableBanana(new Banana());
Add(new TestDrawableCatchHitObjectSpecimen(drawableFruit) { X = -200 });
Add(new TestDrawableCatchHitObjectSpecimen(drawableBanana));
@ -37,16 +38,16 @@ namespace osu.Game.Rulesets.Catch.Tests
float fruitRotation = 0;
float bananaRotation = 0;
float bananaScale = 0;
Vector2 bananaSize = new Vector2();
Color4 bananaColour = new Color4();
AddStep("Initialize start time", () =>
{
drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time;
fruitRotation = drawableFruit.InnerRotation;
bananaRotation = drawableBanana.InnerRotation;
bananaScale = drawableBanana.InnerScale;
fruitRotation = drawableFruit.DisplayRotation;
bananaRotation = drawableBanana.DisplayRotation;
bananaSize = drawableBanana.DisplaySize;
bananaColour = drawableBanana.AccentColour.Value;
});
@ -55,9 +56,9 @@ namespace osu.Game.Rulesets.Catch.Tests
drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = another_start_time;
});
AddAssert("fruit rotation is changed", () => drawableFruit.InnerRotation != fruitRotation);
AddAssert("banana rotation is changed", () => drawableBanana.InnerRotation != bananaRotation);
AddAssert("banana scale is changed", () => drawableBanana.InnerScale != bananaScale);
AddAssert("fruit rotation is changed", () => drawableFruit.DisplayRotation != fruitRotation);
AddAssert("banana rotation is changed", () => drawableBanana.DisplayRotation != bananaRotation);
AddAssert("banana size is changed", () => drawableBanana.DisplaySize != bananaSize);
AddAssert("banana colour is changed", () => drawableBanana.AccentColour.Value != bananaColour);
AddStep("reset start time", () =>
@ -65,32 +66,11 @@ namespace osu.Game.Rulesets.Catch.Tests
drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time;
});
AddAssert("rotation and scale restored", () =>
drawableFruit.InnerRotation == fruitRotation &&
drawableBanana.InnerRotation == bananaRotation &&
drawableBanana.InnerScale == bananaScale &&
AddAssert("rotation and size restored", () =>
drawableFruit.DisplayRotation == fruitRotation &&
drawableBanana.DisplayRotation == bananaRotation &&
drawableBanana.DisplaySize == bananaSize &&
drawableBanana.AccentColour.Value == bananaColour);
}
private class TestDrawableFruit : DrawableFruit
{
public float InnerRotation => ScaleContainer.Rotation;
public TestDrawableFruit(Fruit h)
: base(h)
{
}
}
private class TestDrawableBanana : DrawableBanana
{
public float InnerRotation => ScaleContainer.Rotation;
public float InnerScale => ScaleContainer.Scale.X;
public TestDrawableBanana(Banana h)
: base(h)
{
}
}
}
}

View File

@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("create hyper-dashing catcher", () =>
{
Child = setupSkinHierarchy(catcherArea = new CatcherArea(new Container())
Child = setupSkinHierarchy(catcherArea = new CatcherArea(new Container<CaughtObject>())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
case JuiceStream juiceStream:
// Todo: BUG!! Stable used the last control point as the final position of the path, but it should use the computed path instead.
lastPosition = juiceStream.X + juiceStream.Path.ControlPoints[^1].Position.Value.X;
lastPosition = juiceStream.OriginalX + juiceStream.Path.ControlPoints[^1].Position.Value.X;
// Todo: BUG!! Stable attempted to use the end time of the stream, but referenced it too early in execution and used the start time instead.
lastStartTime = juiceStream.StartTime;
@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
catchObject.XOffset = 0;
if (catchObject is TinyDroplet)
catchObject.XOffset = Math.Clamp(rng.Next(-20, 20), -catchObject.X, CatchPlayfield.WIDTH - catchObject.X);
catchObject.XOffset = Math.Clamp(rng.Next(-20, 20), -catchObject.OriginalX, CatchPlayfield.WIDTH - catchObject.OriginalX);
else if (catchObject is Droplet)
rng.Next(); // osu!stable retrieved a random droplet rotation
}
@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, FastRandom rng)
{
float offsetPosition = hitObject.X;
float offsetPosition = hitObject.OriginalX;
double startTime = hitObject.StartTime;
if (lastPosition == null)
@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
if (positionDiff == 0)
{
applyRandomOffset(ref offsetPosition, timeDiff / 4d, rng);
hitObject.XOffset = offsetPosition - hitObject.X;
hitObject.XOffset = offsetPosition - hitObject.OriginalX;
return;
}
@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
if (Math.Abs(positionDiff) < timeDiff / 3)
applyOffset(ref offsetPosition, positionDiff);
hitObject.XOffset = offsetPosition - hitObject.X;
hitObject.XOffset = offsetPosition - hitObject.OriginalX;
lastPosition = offsetPosition;
lastStartTime = startTime;
@ -230,9 +230,9 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
currentObject.HyperDashTarget = null;
currentObject.DistanceToHyperDash = 0;
int thisDirection = nextObject.X > currentObject.X ? 1 : -1;
int thisDirection = nextObject.EffectiveX > currentObject.EffectiveX ? 1 : -1;
double timeToNext = nextObject.StartTime - currentObject.StartTime - 1000f / 60f / 4; // 1/4th of a frame of grace time, taken from osu-stable
double distanceToNext = Math.Abs(nextObject.X - currentObject.X) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth);
double distanceToNext = Math.Abs(nextObject.EffectiveX - currentObject.EffectiveX) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth);
float distanceToHyper = (float)(timeToNext * Catcher.BASE_SPEED - distanceToNext);
if (distanceToHyper < 0)

View File

@ -5,11 +5,8 @@ namespace osu.Game.Rulesets.Catch
{
public enum CatchSkinComponents
{
FruitBananas,
FruitApple,
FruitGrapes,
FruitOrange,
FruitPear,
Fruit,
Banana,
Droplet,
CatcherIdle,
CatcherFail,

View File

@ -32,8 +32,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
var scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
NormalizedPosition = BaseObject.X * scalingFactor;
LastNormalizedPosition = LastObject.X * scalingFactor;
NormalizedPosition = BaseObject.EffectiveX * scalingFactor;
LastNormalizedPosition = LastObject.EffectiveX * scalingFactor;
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
StrainTime = Math.Max(40, DeltaTime);

View File

@ -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 JetBrains.Annotations;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Catch.Judgements
{
public class CatchJudgementResult : JudgementResult
{
/// <summary>
/// The catcher animation state prior to this judgement.
/// </summary>
public CatcherAnimationState CatcherAnimationState;
/// <summary>
/// Whether the catcher was hyper dashing prior to this judgement.
/// </summary>
public bool CatcherHyperDash;
public CatchJudgementResult([NotNull] HitObject hitObject, [NotNull] Judgement judgement)
: base(hitObject, judgement)
{
}
}
}

View File

@ -14,7 +14,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects
{
public class Banana : Fruit, IHasComboInformation
public class Banana : PalpableCatchHitObject, IHasComboInformation
{
/// <summary>
/// Index of banana in current shower.

View File

@ -4,7 +4,6 @@
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@ -16,38 +15,46 @@ namespace osu.Game.Rulesets.Catch.Objects
{
public const float OBJECT_RADIUS = 64;
// This value is after XOffset applied.
public readonly Bindable<float> XBindable = new Bindable<float>();
// This value is before XOffset applied.
private float originalX;
public readonly Bindable<float> OriginalXBindable = new Bindable<float>();
/// <summary>
/// The horizontal position of the fruit between 0 and <see cref="CatchPlayfield.WIDTH"/>.
/// The horizontal position of the hit object between 0 and <see cref="CatchPlayfield.WIDTH"/>.
/// </summary>
public float X
{
// TODO: I don't like this asymmetry.
get => XBindable.Value;
// originalX is set by `XBindable.BindValueChanged`
set => XBindable.Value = value + xOffset;
set => OriginalXBindable.Value = value;
}
private float xOffset;
float IHasXPosition.X => OriginalXBindable.Value;
public readonly Bindable<float> XOffsetBindable = new Bindable<float>();
/// <summary>
/// A random offset applied to <see cref="X"/>, set by the <see cref="CatchBeatmapProcessor"/>.
/// A random offset applied to the horizontal position, set by the beatmap processing.
/// </summary>
internal float XOffset
public float XOffset
{
get => xOffset;
set
{
xOffset = value;
XBindable.Value = originalX + xOffset;
}
set => XOffsetBindable.Value = value;
}
/// <summary>
/// The horizontal position of the hit object between 0 and <see cref="CatchPlayfield.WIDTH"/>.
/// </summary>
/// <remarks>
/// This value is the original <see cref="X"/> value specified in the beatmap, not affected by the beatmap processing.
/// Use <see cref="EffectiveX"/> for a gameplay.
/// </remarks>
public float OriginalX => OriginalXBindable.Value;
/// <summary>
/// The effective horizontal position of the hit object between 0 and <see cref="CatchPlayfield.WIDTH"/>.
/// </summary>
/// <remarks>
/// This value is the original <see cref="X"/> value plus the offset applied by the beatmap processing.
/// Use <see cref="OriginalX"/> if a value not affected by the offset is desired.
/// </remarks>
public float EffectiveX => OriginalXBindable.Value + XOffsetBindable.Value;
public double TimePreempt = 1000;
public readonly Bindable<int> IndexInBeatmapBindable = new Bindable<int>();
@ -113,10 +120,5 @@ namespace osu.Game.Rulesets.Catch.Objects
}
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
protected CatchHitObject()
{
XBindable.BindValueChanged(x => originalX = x.NewValue - xOffset);
}
}
}

View File

@ -0,0 +1,18 @@
// 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.Catch.Skinning.Default;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
/// <summary>
/// Represents a <see cref="Banana"/> caught by the catcher.
/// </summary>
public class CaughtBanana : CaughtObject
{
public CaughtBanana()
: base(CatchSkinComponents.Banana, _ => new BananaPiece())
{
}
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Catch.Skinning.Default;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
/// <summary>
/// Represents a <see cref="Droplet"/> caught by the catcher.
/// </summary>
public class CaughtDroplet : CaughtObject
{
public override bool StaysOnPlate => false;
public CaughtDroplet()
: base(CatchSkinComponents.Droplet, _ => new DropletPiece())
{
}
}
}

View File

@ -0,0 +1,29 @@
// 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.Catch.Skinning.Default;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
/// <summary>
/// Represents a <see cref="Fruit"/> caught by the catcher.
/// </summary>
public class CaughtFruit : CaughtObject, IHasFruitState
{
public Bindable<FruitVisualRepresentation> VisualRepresentation { get; } = new Bindable<FruitVisualRepresentation>();
public CaughtFruit()
: base(CatchSkinComponents.Fruit, _ => new FruitPiece())
{
}
public override void CopyStateFrom(IHasCatchObjectState objectState)
{
base.CopyStateFrom(objectState);
var fruitState = (IHasFruitState)objectState;
VisualRepresentation.Value = fruitState.VisualRepresentation.Value;
}
}
}

View File

@ -0,0 +1,64 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
/// <summary>
/// Represents a <see cref="PalpableCatchHitObject"/> caught by the catcher.
/// </summary>
[Cached(typeof(IHasCatchObjectState))]
public abstract class CaughtObject : SkinnableDrawable, IHasCatchObjectState
{
public PalpableCatchHitObject HitObject { get; private set; }
public Bindable<Color4> AccentColour { get; } = new Bindable<Color4>();
public Bindable<bool> HyperDash { get; } = new Bindable<bool>();
public Vector2 DisplaySize => Size * Scale;
public float DisplayRotation => Rotation;
/// <summary>
/// Whether this hit object should stay on the catcher plate when the object is caught by the catcher.
/// </summary>
public virtual bool StaysOnPlate => true;
public override bool RemoveWhenNotAlive => true;
protected CaughtObject(CatchSkinComponents skinComponent, Func<ISkinComponent, Drawable> defaultImplementation)
: base(new CatchSkinComponent(skinComponent), defaultImplementation)
{
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.None;
Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2);
}
/// <summary>
/// Copies the hit object visual state from another <see cref="IHasCatchObjectState"/> object.
/// </summary>
public virtual void CopyStateFrom(IHasCatchObjectState objectState)
{
HitObject = objectState.HitObject;
Scale = Vector2.Divide(objectState.DisplaySize, Size);
Rotation = objectState.DisplayRotation;
AccentColour.Value = objectState.AccentColour.Value;
HyperDash.Value = objectState.HyperDash.Value;
}
protected override void FreeAfterUse()
{
ClearTransforms();
Alpha = 1;
base.FreeAfterUse();
}
}
}

View File

@ -2,14 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Skinning.Default;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
public class DrawableBanana : DrawableFruit
public class DrawableBanana : DrawablePalpableCatchHitObject
{
protected override FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => FruitVisualRepresentation.Banana;
public DrawableBanana()
: this(null)
{
@ -20,6 +21,14 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
}
[BackgroundDependencyLoader]
private void load()
{
ScalingContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(CatchSkinComponents.Banana),
_ => new BananaPiece());
}
protected override void LoadComplete()
{
base.LoadComplete();
@ -35,12 +44,12 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
const float end_scale = 0.6f;
const float random_scale_range = 1.6f;
ScaleContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RandomSingle(3)))
.Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt);
ScalingContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RandomSingle(3)))
.Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt);
ScaleContainer.RotateTo(getRandomAngle(1))
.Then()
.RotateTo(getRandomAngle(2), HitObject.TimePreempt);
ScalingContainer.RotateTo(getRandomAngle(1))
.Then()
.RotateTo(getRandomAngle(2), HitObject.TimePreempt);
float getRandomAngle(int series) => 180 * (RandomSingle(series) * 2 - 1);
}

View File

@ -5,7 +5,9 @@ using System;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Utils;
@ -13,11 +15,12 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
public abstract class DrawableCatchHitObject : DrawableHitObject<CatchHitObject>
{
public readonly Bindable<float> XBindable = new Bindable<float>();
public readonly Bindable<float> OriginalXBindable = new Bindable<float>();
public readonly Bindable<float> XOffsetBindable = new Bindable<float>();
protected override double InitialLifetimeOffset => HitObject.TimePreempt;
protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH;
protected override float SamplePlaybackPosition => HitObject.EffectiveX / CatchPlayfield.WIDTH;
public int RandomSeed => HitObject?.RandomSeed ?? 0;
@ -36,21 +39,21 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
base.OnApply();
XBindable.BindTo(HitObject.XBindable);
OriginalXBindable.BindTo(HitObject.OriginalXBindable);
XOffsetBindable.BindTo(HitObject.XOffsetBindable);
}
protected override void OnFree()
{
base.OnFree();
XBindable.UnbindFrom(HitObject.XBindable);
OriginalXBindable.UnbindFrom(HitObject.OriginalXBindable);
XOffsetBindable.UnbindFrom(HitObject.XOffsetBindable);
}
public Func<CatchHitObject, bool> CheckPosition;
public bool IsOnPlate;
public override bool RemoveWhenNotAlive => IsOnPlate;
protected override JudgementResult CreateResult(Judgement judgement) => new CatchJudgementResult(HitObject, judgement);
protected override void CheckForResult(bool userTriggered, double timeOffset)
{

View File

@ -11,8 +11,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
public class DrawableDroplet : DrawablePalpableCatchHitObject
{
public override bool StaysOnPlate => false;
public DrawableDroplet()
: this(null)
{
@ -26,17 +24,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader]
private void load()
{
HyperDash.BindValueChanged(_ => updatePiece(), true);
}
private void updatePiece()
{
ScaleContainer.Child = new SkinnableDrawable(
ScalingContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(CatchSkinComponents.Droplet),
_ => new DropletPiece
{
HyperDash = { BindTarget = HyperDash }
});
_ => new DropletPiece());
}
protected override void UpdateInitialTransforms()
@ -47,7 +37,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
float startRotation = RandomSingle(1) * 20;
double duration = HitObject.TimePreempt + 2000;
ScaleContainer.RotateTo(startRotation).RotateTo(startRotation + 720, duration);
ScalingContainer.RotateTo(startRotation).RotateTo(startRotation + 720, duration);
}
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -11,11 +10,9 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
public class DrawableFruit : DrawablePalpableCatchHitObject
public class DrawableFruit : DrawablePalpableCatchHitObject, IHasFruitState
{
public readonly Bindable<FruitVisualRepresentation> VisualRepresentation = new Bindable<FruitVisualRepresentation>();
protected virtual FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => (FruitVisualRepresentation)(indexInBeatmap % 4);
public Bindable<FruitVisualRepresentation> VisualRepresentation { get; } = new Bindable<FruitVisualRepresentation>();
public DrawableFruit()
: this(null)
@ -32,53 +29,19 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
IndexInBeatmap.BindValueChanged(change =>
{
VisualRepresentation.Value = GetVisualRepresentation(change.NewValue);
VisualRepresentation.Value = (FruitVisualRepresentation)(change.NewValue % 4);
}, true);
VisualRepresentation.BindValueChanged(_ => updatePiece());
HyperDash.BindValueChanged(_ => updatePiece(), true);
ScalingContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(CatchSkinComponents.Fruit),
_ => new FruitPiece());
}
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();
ScaleContainer.RotateTo((RandomSingle(1) - 0.5f) * 40);
}
private void updatePiece()
{
ScaleContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(getComponent(VisualRepresentation.Value)),
_ => new FruitPiece
{
VisualRepresentation = { BindTarget = VisualRepresentation },
HyperDash = { BindTarget = HyperDash },
});
}
private CatchSkinComponents getComponent(FruitVisualRepresentation hitObjectVisualRepresentation)
{
switch (hitObjectVisualRepresentation)
{
case FruitVisualRepresentation.Pear:
return CatchSkinComponents.FruitPear;
case FruitVisualRepresentation.Grape:
return CatchSkinComponents.FruitGrapes;
case FruitVisualRepresentation.Pineapple:
return CatchSkinComponents.FruitApple;
case FruitVisualRepresentation.Raspberry:
return CatchSkinComponents.FruitOrange;
case FruitVisualRepresentation.Banana:
return CatchSkinComponents.FruitBananas;
default:
throw new ArgumentOutOfRangeException(nameof(hitObjectVisualRepresentation), hitObjectVisualRepresentation, null);
}
ScalingContainer.RotateTo((RandomSingle(1) - 0.5f) * 40);
}
}
@ -88,6 +51,5 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
Grape,
Pineapple,
Raspberry,
Banana // banananananannaanana
}
}

View File

@ -7,32 +7,36 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
public abstract class DrawablePalpableCatchHitObject : DrawableCatchHitObject
[Cached(typeof(IHasCatchObjectState))]
public abstract class DrawablePalpableCatchHitObject : DrawableCatchHitObject, IHasCatchObjectState
{
public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject;
public readonly Bindable<bool> HyperDash = new Bindable<bool>();
Bindable<Color4> IHasCatchObjectState.AccentColour => AccentColour;
public readonly Bindable<float> ScaleBindable = new Bindable<float>(1);
public Bindable<bool> HyperDash { get; } = new Bindable<bool>();
public readonly Bindable<int> IndexInBeatmap = new Bindable<int>();
public Bindable<float> ScaleBindable { get; } = new Bindable<float>(1);
public Bindable<int> IndexInBeatmap { get; } = new Bindable<int>();
/// <summary>
/// The multiplicative factor applied to <see cref="ScaleContainer"/> scale relative to <see cref="HitObject"/> scale.
/// The multiplicative factor applied to <see cref="Drawable.Scale"/> relative to <see cref="HitObject"/> scale.
/// </summary>
protected virtual float ScaleFactor => 1;
/// <summary>
/// Whether this hit object should stay on the catcher plate when the object is caught by the catcher.
/// The container internal transforms (such as scaling based on the circle size) are applied to.
/// </summary>
public virtual bool StaysOnPlate => true;
protected readonly Container ScalingContainer;
public float DisplayRadius => CatchHitObject.OBJECT_RADIUS * HitObject.Scale * ScaleFactor;
public Vector2 DisplaySize => ScalingContainer.Size * ScalingContainer.Scale;
protected readonly Container ScaleContainer;
public float DisplayRotation => ScalingContainer.Rotation;
protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h)
: base(h)
@ -40,30 +44,34 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
Origin = Anchor.Centre;
Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2);
AddInternal(ScaleContainer = new Container
AddInternal(ScalingContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2)
});
}
[BackgroundDependencyLoader]
private void load()
{
XBindable.BindValueChanged(x =>
{
if (!IsOnPlate) X = x.NewValue;
}, true);
OriginalXBindable.BindValueChanged(updateXPosition);
XOffsetBindable.BindValueChanged(updateXPosition, true);
ScaleBindable.BindValueChanged(scale =>
{
ScaleContainer.Scale = new Vector2(scale.NewValue * ScaleFactor);
ScalingContainer.Scale = new Vector2(scale.NewValue * ScaleFactor);
Size = DisplaySize;
}, true);
IndexInBeatmap.BindValueChanged(_ => UpdateComboColour());
}
private void updateXPosition(ValueChangedEvent<float> _)
{
X = OriginalXBindable.Value + XOffsetBindable.Value;
}
protected override void OnApply()
{
base.OnApply();

View File

@ -0,0 +1,25 @@
// 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 osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
/// <summary>
/// Provides a visual state of a <see cref="PalpableCatchHitObject"/>.
/// </summary>
public interface IHasCatchObjectState
{
PalpableCatchHitObject HitObject { get; }
Bindable<Color4> AccentColour { get; }
Bindable<bool> HyperDash { get; }
Vector2 DisplaySize { get; }
float DisplayRotation { get; }
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
/// <summary>
/// Provides a visual state of a <see cref="Fruit"/>.
/// </summary>
public interface IHasFruitState : IHasCatchObjectState
{
Bindable<FruitVisualRepresentation> VisualRepresentation { get; }
}
}

View File

@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Objects
AddNested(new TinyDroplet
{
StartTime = t + lastEvent.Value.Time,
X = X + Path.PositionAt(
X = OriginalX + Path.PositionAt(
lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X,
});
}
@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Samples = dropletSamples,
StartTime = e.Time,
X = X + Path.PositionAt(e.PathProgress).X,
X = OriginalX + Path.PositionAt(e.PathProgress).X,
});
break;
@ -104,14 +104,14 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Samples = this.GetNodeSamples(nodeIndex++),
StartTime = e.Time,
X = X + Path.PositionAt(e.PathProgress).X,
X = OriginalX + Path.PositionAt(e.PathProgress).X,
});
break;
}
}
}
public float EndX => X + this.CurvePositionAt(1).X;
public float EndX => OriginalX + this.CurvePositionAt(1).X;
public double Duration
{

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Catch.Replays
void moveToNext(PalpableCatchHitObject h)
{
float positionChange = Math.Abs(lastPosition - h.X);
float positionChange = Math.Abs(lastPosition - h.EffectiveX);
double timeAvailable = h.StartTime - lastTime;
// So we can either make it there without a dash or not.
@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Catch.Replays
// todo: get correct catcher size, based on difficulty CS.
const float catcher_width_half = CatcherArea.CATCHER_SIZE * 0.3f * 0.5f;
if (lastPosition - catcher_width_half < h.X && lastPosition + catcher_width_half > h.X)
if (lastPosition - catcher_width_half < h.EffectiveX && lastPosition + catcher_width_half > h.EffectiveX)
{
// we are already in the correct range.
lastTime = h.StartTime;
@ -66,12 +66,12 @@ namespace osu.Game.Rulesets.Catch.Replays
if (impossibleJump)
{
addFrame(h.StartTime, h.X);
addFrame(h.StartTime, h.EffectiveX);
}
else if (h.HyperDash)
{
addFrame(h.StartTime - timeAvailable, lastPosition);
addFrame(h.StartTime, h.X);
addFrame(h.StartTime, h.EffectiveX);
}
else if (dashRequired)
{
@ -80,23 +80,23 @@ namespace osu.Game.Rulesets.Catch.Replays
double timeWeNeedToSave = timeAtNormalSpeed - timeAvailable;
double timeAtDashSpeed = timeWeNeedToSave / 2;
float midPosition = (float)Interpolation.Lerp(lastPosition, h.X, (float)timeAtDashSpeed / timeAvailable);
float midPosition = (float)Interpolation.Lerp(lastPosition, h.EffectiveX, (float)timeAtDashSpeed / timeAvailable);
// dash movement
addFrame(h.StartTime - timeAvailable + 1, lastPosition, true);
addFrame(h.StartTime - timeAvailable + timeAtDashSpeed, midPosition);
addFrame(h.StartTime, h.X);
addFrame(h.StartTime, h.EffectiveX);
}
else
{
double timeBefore = positionChange / movement_speed;
addFrame(h.StartTime - timeBefore, lastPosition);
addFrame(h.StartTime, h.X);
addFrame(h.StartTime, h.EffectiveX);
}
lastTime = h.StartTime;
lastPosition = h.X;
lastPosition = h.EffectiveX;
}
foreach (var obj in Beatmap.HitObjects)

View File

@ -2,28 +2,24 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Default
{
public class BananaPiece : PulpFormation
public class BananaPiece : CatchHitObjectPiece
{
protected override BorderPiece BorderPiece { get; }
public BananaPiece()
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Pulp
new BananaPulpFormation
{
AccentColour = { BindTarget = AccentColour },
Size = new Vector2(SMALL_PULP),
Y = -0.3f
},
new Pulp
{
AccentColour = { BindTarget = AccentColour },
Size = new Vector2(LARGE_PULP_4 * 0.8f, LARGE_PULP_4 * 2.5f),
Y = 0.05f,
},
BorderPiece = new BorderPiece(),
};
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Default
{
public class BananaPulpFormation : PulpFormation
{
public BananaPulpFormation()
{
AddPulp(new Vector2(0, -0.3f), new Vector2(SMALL_PULP));
AddPulp(new Vector2(0, 0.05f), new Vector2(LARGE_PULP_4 * 0.8f, LARGE_PULP_4 * 2.5f));
}
}
}

View File

@ -0,0 +1,54 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Skinning.Default
{
public abstract class CatchHitObjectPiece : CompositeDrawable
{
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
public readonly Bindable<bool> HyperDash = new Bindable<bool>();
[Resolved]
protected IHasCatchObjectState ObjectState { get; private set; }
/// <summary>
/// A part of this piece that will be faded out while falling in the playfield.
/// </summary>
[CanBeNull]
protected virtual BorderPiece BorderPiece => null;
/// <summary>
/// A part of this piece that will be only visible when <see cref="HyperDash"/> is true.
/// </summary>
[CanBeNull]
protected virtual HyperBorderPiece HyperBorderPiece => null;
protected override void LoadComplete()
{
base.LoadComplete();
AccentColour.BindTo(ObjectState.AccentColour);
HyperDash.BindTo(ObjectState.HyperDash);
HyperDash.BindValueChanged(hyper =>
{
if (HyperBorderPiece != null)
HyperBorderPiece.Alpha = hyper.NewValue ? 1 : 0;
}, true);
}
protected override void Update()
{
if (BorderPiece != null)
BorderPiece.Alpha = (float)Math.Clamp((ObjectState.HitObject.StartTime - Time.Current) / 500, 0, 1);
}
}
}

View File

@ -1,38 +1,29 @@
// 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.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Default
{
public class DropletPiece : CompositeDrawable
public class DropletPiece : CatchHitObjectPiece
{
public readonly Bindable<bool> HyperDash = new Bindable<bool>();
protected override HyperBorderPiece HyperBorderPiece { get; }
public DropletPiece()
{
Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2);
}
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject)
{
InternalChild = new Pulp
InternalChildren = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
AccentColour = { BindTarget = drawableObject.AccentColour }
new Pulp
{
RelativeSizeAxes = Axes.Both,
AccentColour = { BindTarget = AccentColour }
},
HyperBorderPiece = new HyperDropletBorderPiece()
};
if (HyperDash.Value)
{
AddInternal(new HyperDropletBorderPiece());
}
}
}
}

View File

@ -1,18 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Catch.Skinning.Default
{
internal class FruitPiece : CompositeDrawable
internal class FruitPiece : CatchHitObjectPiece
{
/// <summary>
/// Because we're adding a border around the fruit, we need to scale down some.
@ -20,61 +15,32 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
public const float RADIUS_ADJUST = 1.1f;
public readonly Bindable<FruitVisualRepresentation> VisualRepresentation = new Bindable<FruitVisualRepresentation>();
public readonly Bindable<bool> HyperDash = new Bindable<bool>();
[CanBeNull]
private DrawableCatchHitObject drawableHitObject;
[CanBeNull]
private BorderPiece borderPiece;
protected override BorderPiece BorderPiece { get; }
protected override HyperBorderPiece HyperBorderPiece { get; }
public FruitPiece()
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader(permitNulls: true)]
private void load([CanBeNull] DrawableHitObject drawable)
{
drawableHitObject = (DrawableCatchHitObject)drawable;
AddInternal(getFruitFor(VisualRepresentation.Value));
// if it is not part of a DHO, the border is always invisible.
if (drawableHitObject != null)
AddInternal(borderPiece = new BorderPiece());
if (HyperDash.Value)
AddInternal(new HyperBorderPiece());
}
protected override void Update()
{
if (borderPiece != null && drawableHitObject?.HitObject != null)
borderPiece.Alpha = (float)Math.Clamp((drawableHitObject.HitObject.StartTime - Time.Current) / 500, 0, 1);
}
private Drawable getFruitFor(FruitVisualRepresentation representation)
{
switch (representation)
InternalChildren = new Drawable[]
{
case FruitVisualRepresentation.Pear:
return new PearPiece();
new FruitPulpFormation
{
AccentColour = { BindTarget = AccentColour },
VisualRepresentation = { BindTarget = VisualRepresentation }
},
BorderPiece = new BorderPiece(),
HyperBorderPiece = new HyperBorderPiece()
};
}
case FruitVisualRepresentation.Grape:
return new GrapePiece();
protected override void LoadComplete()
{
base.LoadComplete();
case FruitVisualRepresentation.Pineapple:
return new PineapplePiece();
case FruitVisualRepresentation.Banana:
return new BananaPiece();
case FruitVisualRepresentation.Raspberry:
return new RaspberryPiece();
}
return Empty();
var fruitState = (IHasFruitState)ObjectState;
VisualRepresentation.BindTo(fruitState.VisualRepresentation);
}
}
}

View File

@ -0,0 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Default
{
public class FruitPulpFormation : PulpFormation
{
public readonly Bindable<FruitVisualRepresentation> VisualRepresentation = new Bindable<FruitVisualRepresentation>();
protected override void LoadComplete()
{
base.LoadComplete();
VisualRepresentation.BindValueChanged(setFormation, true);
}
private void setFormation(ValueChangedEvent<FruitVisualRepresentation> visualRepresentation)
{
Clear();
switch (visualRepresentation.NewValue)
{
case FruitVisualRepresentation.Pear:
AddPulp(new Vector2(0, -0.33f), new Vector2(SMALL_PULP));
AddPulp(PositionAt(60, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3));
AddPulp(PositionAt(180, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3));
AddPulp(PositionAt(300, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3));
break;
case FruitVisualRepresentation.Grape:
AddPulp(new Vector2(0, -0.25f), new Vector2(SMALL_PULP));
AddPulp(PositionAt(0, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3));
AddPulp(PositionAt(120, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3));
AddPulp(PositionAt(240, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3));
break;
case FruitVisualRepresentation.Pineapple:
AddPulp(new Vector2(0, -0.3f), new Vector2(SMALL_PULP));
AddPulp(PositionAt(45, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4));
AddPulp(PositionAt(135, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4));
AddPulp(PositionAt(225, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4));
AddPulp(PositionAt(315, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4));
break;
case FruitVisualRepresentation.Raspberry:
AddPulp(new Vector2(0, -0.34f), new Vector2(SMALL_PULP));
AddPulp(PositionAt(0, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4));
AddPulp(PositionAt(90, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4));
AddPulp(PositionAt(180, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4));
AddPulp(PositionAt(270, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4));
break;
}
}
}
}

View File

@ -1,42 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Default
{
public class GrapePiece : PulpFormation
{
public GrapePiece()
{
InternalChildren = new Drawable[]
{
new Pulp
{
AccentColour = { BindTarget = AccentColour },
Size = new Vector2(SMALL_PULP),
Y = -0.25f,
},
new Pulp
{
AccentColour = { BindTarget = AccentColour },
Size = new Vector2(LARGE_PULP_3),
Position = PositionAt(0, DISTANCE_FROM_CENTRE_3),
},
new Pulp
{
AccentColour = { BindTarget = AccentColour },
Size = new Vector2(LARGE_PULP_3),
Position = PositionAt(120, DISTANCE_FROM_CENTRE_3),
},
new Pulp
{
Size = new Vector2(LARGE_PULP_3),
AccentColour = { BindTarget = AccentColour },
Position = PositionAt(240, DISTANCE_FROM_CENTRE_3),
},
};
}
}
}

View File

@ -1,42 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Default
{
public class PearPiece : PulpFormation
{
public PearPiece()
{
InternalChildren = new Drawable[]
{
new Pulp
{
AccentColour = { BindTarget = AccentColour },
Size = new Vector2(SMALL_PULP),
Y = -0.33f,
},
new Pulp
{
AccentColour = { BindTarget = AccentColour },
Size = new Vector2(LARGE_PULP_3),
Position = PositionAt(60, DISTANCE_FROM_CENTRE_3),
},
new Pulp
{
AccentColour = { BindTarget = AccentColour },
Size = new Vector2(LARGE_PULP_3),
Position = PositionAt(180, DISTANCE_FROM_CENTRE_3),
},
new Pulp
{
Size = new Vector2(LARGE_PULP_3),
AccentColour = { BindTarget = AccentColour },
Position = PositionAt(300, DISTANCE_FROM_CENTRE_3),
},
};
}
}
}

View File

@ -1,48 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Default
{
public class PineapplePiece : PulpFormation
{
public PineapplePiece()
{
InternalChildren = new Drawable[]
{
new Pulp
{
AccentColour = { BindTarget = AccentColour },
Size = new Vector2(SMALL_PULP),
Y = -0.3f,
},
new Pulp
{
AccentColour = { BindTarget = AccentColour },
Size = new Vector2(LARGE_PULP_4),
Position = PositionAt(45, DISTANCE_FROM_CENTRE_4),
},
new Pulp
{
AccentColour = { BindTarget = AccentColour },
Size = new Vector2(LARGE_PULP_4),
Position = PositionAt(135, DISTANCE_FROM_CENTRE_4),
},
new Pulp
{
AccentColour = { BindTarget = AccentColour },
Size = new Vector2(LARGE_PULP_4),
Position = PositionAt(225, DISTANCE_FROM_CENTRE_4),
},
new Pulp
{
Size = new Vector2(LARGE_PULP_4),
AccentColour = { BindTarget = AccentColour },
Position = PositionAt(315, DISTANCE_FROM_CENTRE_4),
},
};
}
}
}

View File

@ -12,6 +12,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
{
public class Pulp : Circle
{
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
public Pulp()
{
RelativePositionAxes = Axes.Both;
@ -22,8 +24,6 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
Colour = Color4.White.Opacity(0.9f);
}
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
protected override void LoadComplete()
{
base.LoadComplete();

View File

@ -2,12 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
using osuTK.Graphics;
@ -15,7 +12,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
{
public abstract class PulpFormation : CompositeDrawable
{
protected readonly IBindable<Color4> AccentColour = new Bindable<Color4>();
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
protected const float LARGE_PULP_3 = 16f * FruitPiece.RADIUS_ADJUST;
protected const float DISTANCE_FROM_CENTRE_3 = 0.15f;
@ -25,6 +22,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
protected const float SMALL_PULP = LARGE_PULP_3 / 2;
private int pulpsInUse;
protected PulpFormation()
{
RelativeSizeAxes = Axes.Both;
@ -34,11 +33,24 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
distance * MathF.Sin(angle * MathF.PI / 180),
distance * MathF.Cos(angle * MathF.PI / 180));
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject)
protected void Clear()
{
DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject;
AccentColour.BindTo(drawableCatchObject.AccentColour);
for (int i = 0; i < pulpsInUse; i++)
InternalChildren[i].Alpha = 0;
pulpsInUse = 0;
}
protected void AddPulp(Vector2 position, Vector2 size)
{
if (pulpsInUse == InternalChildren.Count)
AddInternal(new Pulp { AccentColour = { BindTarget = AccentColour } });
var pulp = InternalChildren[pulpsInUse];
pulp.Position = position;
pulp.Size = size;
pulp.Alpha = 1;
pulpsInUse++;
}
}
}

View File

@ -1,48 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Default
{
public class RaspberryPiece : PulpFormation
{
public RaspberryPiece()
{
InternalChildren = new Drawable[]
{
new Pulp
{
AccentColour = { BindTarget = AccentColour },
Size = new Vector2(SMALL_PULP),
Y = -0.34f,
},
new Pulp
{
AccentColour = { BindTarget = AccentColour },
Size = new Vector2(LARGE_PULP_4),
Position = PositionAt(0, DISTANCE_FROM_CENTRE_4),
},
new Pulp
{
AccentColour = { BindTarget = AccentColour },
Size = new Vector2(LARGE_PULP_4),
Position = PositionAt(90, DISTANCE_FROM_CENTRE_4),
},
new Pulp
{
AccentColour = { BindTarget = AccentColour },
Size = new Vector2(LARGE_PULP_4),
Position = PositionAt(180, DISTANCE_FROM_CENTRE_4),
},
new Pulp
{
Size = new Vector2(LARGE_PULP_4),
AccentColour = { BindTarget = AccentColour },
Position = PositionAt(270, DISTANCE_FROM_CENTRE_4),
},
};
}
}
}

View File

@ -1,11 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Humanizer;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
using static osu.Game.Skinning.LegacySkinConfiguration;
@ -40,20 +38,21 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
switch (catchSkinComponent.Component)
{
case CatchSkinComponents.FruitApple:
case CatchSkinComponents.FruitBananas:
case CatchSkinComponents.FruitOrange:
case CatchSkinComponents.FruitGrapes:
case CatchSkinComponents.FruitPear:
var lookupName = catchSkinComponent.Component.ToString().Kebaberize();
if (GetTexture(lookupName) != null)
return new LegacyFruitPiece(lookupName);
case CatchSkinComponents.Fruit:
if (GetTexture("fruit-pear") != null)
return new LegacyFruitPiece();
break;
case CatchSkinComponents.Banana:
if (GetTexture("fruit-bananas") != null)
return new LegacyBananaPiece();
break;
case CatchSkinComponents.Droplet:
if (GetTexture("fruit-drop") != null)
return new LegacyFruitPiece("fruit-drop") { Scale = new Vector2(0.8f) };
return new LegacyDropletPiece();
break;

View File

@ -1,83 +1,45 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
internal class LegacyFruitPiece : CompositeDrawable
internal class LegacyFruitPiece : LegacyCatchHitObjectPiece
{
private readonly string lookupName;
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
private readonly Bindable<bool> hyperDash = new Bindable<bool>();
private Sprite colouredSprite;
public LegacyFruitPiece(string lookupName)
{
this.lookupName = lookupName;
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject, ISkinSource skin)
{
var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject;
accentColour.BindTo(drawableCatchObject.AccentColour);
hyperDash.BindTo(drawableCatchObject.HyperDash);
InternalChildren = new Drawable[]
{
colouredSprite = new Sprite
{
Texture = skin.GetTexture(lookupName),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new Sprite
{
Texture = skin.GetTexture($"{lookupName}-overlay"),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
};
if (hyperDash.Value)
{
var hyperDashOverlay = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Blending = BlendingParameters.Additive,
Depth = 1,
Alpha = 0.7f,
Scale = new Vector2(1.2f),
Texture = skin.GetTexture(lookupName),
Colour = skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value ??
skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ??
Catcher.DEFAULT_HYPER_DASH_COLOUR,
};
AddInternal(hyperDashOverlay);
}
}
public readonly Bindable<FruitVisualRepresentation> VisualRepresentation = new Bindable<FruitVisualRepresentation>();
protected override void LoadComplete()
{
base.LoadComplete();
accentColour.BindValueChanged(colour => colouredSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
var fruitState = (IHasFruitState)ObjectState;
VisualRepresentation.BindTo(fruitState.VisualRepresentation);
VisualRepresentation.BindValueChanged(visual => setTexture(visual.NewValue), true);
}
private void setTexture(FruitVisualRepresentation visualRepresentation)
{
switch (visualRepresentation)
{
case FruitVisualRepresentation.Pear:
SetTexture(Skin.GetTexture("fruit-pear"), Skin.GetTexture("fruit-pear-overlay"));
break;
case FruitVisualRepresentation.Grape:
SetTexture(Skin.GetTexture("fruit-grapes"), Skin.GetTexture("fruit-grapes-overlay"));
break;
case FruitVisualRepresentation.Pineapple:
SetTexture(Skin.GetTexture("fruit-apple"), Skin.GetTexture("fruit-apple-overlay"));
break;
case FruitVisualRepresentation.Raspberry:
SetTexture(Skin.GetTexture("fruit-orange"), Skin.GetTexture("fruit-orange-overlay"));
break;
}
}
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Textures;
namespace osu.Game.Rulesets.Catch.Skinning
{
public class LegacyBananaPiece : LegacyCatchHitObjectPiece
{
protected override void LoadComplete()
{
base.LoadComplete();
Texture texture = Skin.GetTexture("fruit-bananas");
Texture overlayTexture = Skin.GetTexture("fruit-bananas-overlay");
SetTexture(texture, overlayTexture);
}
}
}

View File

@ -0,0 +1,90 @@
// 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.Pooling;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Skinning
{
public abstract class LegacyCatchHitObjectPiece : PoolableDrawable
{
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
public readonly Bindable<bool> HyperDash = new Bindable<bool>();
private readonly Sprite colouredSprite;
private readonly Sprite overlaySprite;
private readonly Sprite hyperSprite;
[Resolved]
protected ISkinSource Skin { get; private set; }
[Resolved]
protected IHasCatchObjectState ObjectState { get; private set; }
protected LegacyCatchHitObjectPiece()
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
colouredSprite = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
overlaySprite = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
hyperSprite = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Blending = BlendingParameters.Additive,
Depth = 1,
Alpha = 0,
Scale = new Vector2(1.2f),
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
AccentColour.BindTo(ObjectState.AccentColour);
HyperDash.BindTo(ObjectState.HyperDash);
hyperSprite.Colour = Skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value ??
Skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ??
Catcher.DEFAULT_HYPER_DASH_COLOUR;
AccentColour.BindValueChanged(colour =>
{
colouredSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue);
}, true);
HyperDash.BindValueChanged(hyper =>
{
hyperSprite.Alpha = hyper.NewValue ? 0.7f : 0;
}, true);
}
protected void SetTexture(Texture texture, Texture overlayTexture)
{
colouredSprite.Texture = texture;
overlaySprite.Texture = overlayTexture;
hyperSprite.Texture = texture;
}
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Textures;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning
{
public class LegacyDropletPiece : LegacyCatchHitObjectPiece
{
public LegacyDropletPiece()
{
Scale = new Vector2(0.8f);
}
protected override void LoadComplete()
{
base.LoadComplete();
Texture texture = Skin.GetTexture("fruit-drop");
Texture overlayTexture = Skin.GetTexture("fruit-drop-overlay");
SetTexture(texture, overlayTexture);
}
}
}

View File

@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.UI
public CatchPlayfield(BeatmapDifficulty difficulty, Func<CatchHitObject, DrawableHitObject<CatchHitObject>> createDrawableRepresentation)
{
var droppedObjectContainer = new Container
var droppedObjectContainer = new Container<CaughtObject>
{
RelativeSizeAxes = Axes.Both,
};
@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Catch.UI
((DrawableCatchHitObject)d).CheckPosition = checkIfWeCanCatch;
}
private bool checkIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj);
private bool checkIfWeCanCatch(CatchHitObject obj) => CatcherArea.MovableCatcher.CanCatch(obj);
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
=> CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result);

View File

@ -2,10 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Replays;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Catch.UI
@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
private readonly CatchPlayfield playfield;
public CatchReplayRecorder(Replay target, CatchPlayfield playfield)
public CatchReplayRecorder(Score target, CatchPlayfield playfield)
: base(target)
{
this.playfield = playfield;

View File

@ -14,9 +14,11 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Rulesets.Judgements;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@ -51,9 +53,15 @@ namespace osu.Game.Rulesets.Catch.UI
private CatcherTrailDisplay trails;
private readonly Container droppedObjectTarget;
/// <summary>
/// Contains caught objects on the plate.
/// </summary>
private readonly Container<CaughtObject> caughtObjectContainer;
private readonly Container<DrawablePalpableCatchHitObject> caughtFruitContainer;
/// <summary>
/// Contains objects dropped from the plate.
/// </summary>
private readonly Container<CaughtObject> droppedObjectTarget;
public CatcherAnimationState CurrentState { get; private set; }
@ -106,7 +114,11 @@ namespace osu.Game.Rulesets.Catch.UI
private readonly DrawablePool<HitExplosion> hitExplosionPool;
private readonly Container<HitExplosion> hitExplosionContainer;
public Catcher([NotNull] Container trailsTarget, [NotNull] Container droppedObjectTarget, BeatmapDifficulty difficulty = null)
private readonly DrawablePool<CaughtFruit> caughtFruitPool;
private readonly DrawablePool<CaughtBanana> caughtBananaPool;
private readonly DrawablePool<CaughtDroplet> caughtDropletPool;
public Catcher([NotNull] Container trailsTarget, [NotNull] Container<CaughtObject> droppedObjectTarget, BeatmapDifficulty difficulty = null)
{
this.trailsTarget = trailsTarget;
this.droppedObjectTarget = droppedObjectTarget;
@ -122,7 +134,11 @@ namespace osu.Game.Rulesets.Catch.UI
InternalChildren = new Drawable[]
{
hitExplosionPool = new DrawablePool<HitExplosion>(10),
caughtFruitContainer = new Container<DrawablePalpableCatchHitObject>
caughtFruitPool = new DrawablePool<CaughtFruit>(50),
caughtBananaPool = new DrawablePool<CaughtBanana>(100),
// less capacity is needed compared to fruit because droplet is not stacked
caughtDropletPool = new DrawablePool<CaughtDroplet>(25),
caughtObjectContainer = new Container<CaughtObject>
{
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
@ -170,7 +186,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// <summary>
/// Creates proxied content to be displayed beneath hitobjects.
/// </summary>
public Drawable CreateProxiedContent() => caughtFruitContainer.CreateProxy();
public Drawable CreateProxiedContent() => caughtObjectContainer.CreateProxy();
/// <summary>
/// Calculates the scale of the catcher based off the provided beatmap difficulty.
@ -190,11 +206,9 @@ namespace osu.Game.Rulesets.Catch.UI
internal static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty));
/// <summary>
/// Let the catcher attempt to catch a fruit.
/// Determine if this catcher can catch a <see cref="CatchHitObject"/> in the current position.
/// </summary>
/// <param name="hitObject">The fruit to catch.</param>
/// <returns>Whether the catch is possible.</returns>
public bool AttemptCatch(CatchHitObject hitObject)
public bool CanCatch(CatchHitObject hitObject)
{
if (!(hitObject is PalpableCatchHitObject fruit))
return false;
@ -202,37 +216,72 @@ namespace osu.Game.Rulesets.Catch.UI
var halfCatchWidth = catchWidth * 0.5f;
// this stuff wil disappear once we move fruit to non-relative coordinate space in the future.
var catchObjectPosition = fruit.X;
var catchObjectPosition = fruit.EffectiveX;
var catcherPosition = Position.X;
var validCatch =
catchObjectPosition >= catcherPosition - halfCatchWidth &&
catchObjectPosition <= catcherPosition + halfCatchWidth;
return catchObjectPosition >= catcherPosition - halfCatchWidth &&
catchObjectPosition <= catcherPosition + halfCatchWidth;
}
if (validCatch)
placeCaughtObject(fruit);
public void OnNewResult(DrawableCatchHitObject drawableObject, JudgementResult result)
{
var catchResult = (CatchJudgementResult)result;
catchResult.CatcherAnimationState = CurrentState;
catchResult.CatcherHyperDash = HyperDashing;
if (!(drawableObject is DrawablePalpableCatchHitObject palpableObject)) return;
var hitObject = palpableObject.HitObject;
if (result.IsHit)
{
var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X / 2);
placeCaughtObject(palpableObject, positionInStack);
if (hitLighting.Value)
addLighting(hitObject, positionInStack.X, drawableObject.AccentColour.Value);
}
// droplet doesn't affect the catcher state
if (fruit is TinyDroplet) return validCatch;
if (hitObject is TinyDroplet) return;
if (validCatch && fruit.HyperDash)
if (result.IsHit && hitObject.HyperDash)
{
var target = fruit.HyperDashTarget;
var timeDifference = target.StartTime - fruit.StartTime;
double positionDifference = target.X - catcherPosition;
var target = hitObject.HyperDashTarget;
var timeDifference = target.StartTime - hitObject.StartTime;
double positionDifference = target.EffectiveX - X;
var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
SetHyperDashState(Math.Abs(velocity), target.X);
SetHyperDashState(Math.Abs(velocity), target.EffectiveX);
}
else
SetHyperDashState();
if (validCatch)
updateState(fruit.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle);
else if (!(fruit is Banana))
if (result.IsHit)
updateState(hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle);
else if (!(hitObject is Banana))
updateState(CatcherAnimationState.Fail);
}
return validCatch;
public void OnRevertResult(DrawableCatchHitObject drawableObject, JudgementResult result)
{
var catchResult = (CatchJudgementResult)result;
if (CurrentState != catchResult.CatcherAnimationState)
updateState(catchResult.CatcherAnimationState);
if (HyperDashing != catchResult.CatcherHyperDash)
{
if (catchResult.CatcherHyperDash)
SetHyperDashState(2);
else
SetHyperDashState();
}
caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject);
droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject);
hitExplosionContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject);
}
/// <summary>
@ -415,137 +464,121 @@ namespace osu.Game.Rulesets.Catch.UI
updateCatcher();
}
private void placeCaughtObject(PalpableCatchHitObject source)
private void placeCaughtObject(DrawablePalpableCatchHitObject drawableObject, Vector2 position)
{
var caughtObject = createCaughtObject(source);
var caughtObject = getCaughtObject(drawableObject.HitObject);
if (caughtObject == null) return;
caughtObject.RelativePositionAxes = Axes.None;
caughtObject.X = source.X - X;
caughtObject.IsOnPlate = true;
caughtObject.CopyStateFrom(drawableObject);
caughtObject.Anchor = Anchor.TopCentre;
caughtObject.Origin = Anchor.Centre;
caughtObject.Scale *= 0.5f;
caughtObject.LifetimeStart = source.StartTime;
caughtObject.LifetimeEnd = double.MaxValue;
caughtObject.Position = position;
caughtObject.Scale /= 2;
adjustPositionInStack(caughtObject);
caughtFruitContainer.Add(caughtObject);
addLighting(caughtObject);
caughtObjectContainer.Add(caughtObject);
if (!caughtObject.StaysOnPlate)
removeFromPlate(caughtObject, DroppedObjectAnimation.Explode);
}
private void adjustPositionInStack(DrawablePalpableCatchHitObject caughtObject)
private Vector2 computePositionInStack(Vector2 position, float displayRadius)
{
const float radius_div_2 = CatchHitObject.OBJECT_RADIUS / 2;
const float allowance = 10;
float caughtObjectRadius = caughtObject.DisplayRadius;
while (caughtFruitContainer.Any(f => Vector2Extensions.Distance(f.Position, caughtObject.Position) < (caughtObjectRadius + radius_div_2) / (allowance / 2)))
while (caughtObjectContainer.Any(f => Vector2Extensions.Distance(f.Position, position) < (displayRadius + radius_div_2) / (allowance / 2)))
{
float diff = (caughtObjectRadius + radius_div_2) / allowance;
float diff = (displayRadius + radius_div_2) / allowance;
caughtObject.X += (RNG.NextSingle() - 0.5f) * diff * 2;
caughtObject.Y -= RNG.NextSingle() * diff;
position.X += (RNG.NextSingle() - 0.5f) * diff * 2;
position.Y -= RNG.NextSingle() * diff;
}
caughtObject.X = Math.Clamp(caughtObject.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2);
position.X = Math.Clamp(position.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2);
return position;
}
private void addLighting(DrawablePalpableCatchHitObject caughtObject)
private void addLighting(CatchHitObject hitObject, float x, Color4 colour)
{
if (!hitLighting.Value) return;
HitExplosion hitExplosion = hitExplosionPool.Get();
hitExplosion.X = caughtObject.X;
hitExplosion.Scale = new Vector2(caughtObject.HitObject.Scale);
hitExplosion.ObjectColour = caughtObject.AccentColour.Value;
hitExplosion.HitObject = hitObject;
hitExplosion.X = x;
hitExplosion.Scale = new Vector2(hitObject.Scale);
hitExplosion.ObjectColour = colour;
hitExplosionContainer.Add(hitExplosion);
}
private DrawablePalpableCatchHitObject createCaughtObject(PalpableCatchHitObject source)
private CaughtObject getCaughtObject(PalpableCatchHitObject source)
{
switch (source)
{
case Banana banana:
return new DrawableBanana(banana);
case Fruit _:
return caughtFruitPool.Get();
case Fruit fruit:
return new DrawableFruit(fruit);
case Banana _:
return caughtBananaPool.Get();
case TinyDroplet tiny:
return new DrawableTinyDroplet(tiny);
case Droplet droplet:
return new DrawableDroplet(droplet);
case Droplet _:
return caughtDropletPool.Get();
default:
return null;
}
}
private CaughtObject getDroppedObject(CaughtObject caughtObject)
{
var droppedObject = getCaughtObject(caughtObject.HitObject);
droppedObject.CopyStateFrom(caughtObject);
droppedObject.Anchor = Anchor.TopLeft;
droppedObject.Position = caughtObjectContainer.ToSpaceOfOtherDrawable(caughtObject.DrawPosition, droppedObjectTarget);
return droppedObject;
}
private void clearPlate(DroppedObjectAnimation animation)
{
var caughtObjects = caughtFruitContainer.Children.ToArray();
caughtFruitContainer.Clear(false);
var droppedObjects = caughtObjectContainer.Children.Select(getDroppedObject).ToArray();
droppedObjectTarget.AddRange(caughtObjects);
caughtObjectContainer.Clear(false);
foreach (var caughtObject in caughtObjects)
drop(caughtObject, animation);
droppedObjectTarget.AddRange(droppedObjects);
foreach (var droppedObject in droppedObjects)
applyDropAnimation(droppedObject, animation);
}
private void removeFromPlate(DrawablePalpableCatchHitObject caughtObject, DroppedObjectAnimation animation)
private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation)
{
if (!caughtFruitContainer.Remove(caughtObject))
throw new InvalidOperationException("Can only drop a caught object on the plate");
var droppedObject = getDroppedObject(caughtObject);
droppedObjectTarget.Add(caughtObject);
caughtObjectContainer.Remove(caughtObject);
drop(caughtObject, animation);
droppedObjectTarget.Add(droppedObject);
applyDropAnimation(droppedObject, animation);
}
private void drop(DrawablePalpableCatchHitObject d, DroppedObjectAnimation animation)
private void applyDropAnimation(Drawable d, DroppedObjectAnimation animation)
{
var originalX = d.X * Scale.X;
var startTime = Clock.CurrentTime;
d.Anchor = Anchor.TopLeft;
d.Position = caughtFruitContainer.ToSpaceOfOtherDrawable(d.DrawPosition, droppedObjectTarget);
// we cannot just apply the transforms because DHO clears transforms when state is updated
d.ApplyCustomUpdateState += (o, state) => animate(o, animation, originalX, startTime);
if (d.IsLoaded)
animate(d, animation, originalX, startTime);
}
private void animate(Drawable d, DroppedObjectAnimation animation, float originalX, double startTime)
{
using (d.BeginAbsoluteSequence(startTime))
switch (animation)
{
switch (animation)
{
case DroppedObjectAnimation.Drop:
d.MoveToY(d.Y + 75, 750, Easing.InSine);
d.FadeOut(750);
break;
case DroppedObjectAnimation.Drop:
d.MoveToY(d.Y + 75, 750, Easing.InSine);
d.FadeOut(750);
break;
case DroppedObjectAnimation.Explode:
d.MoveToY(d.Y - 50, 250, Easing.OutSine).Then().MoveToY(d.Y + 50, 500, Easing.InSine);
d.MoveToX(d.X + originalX * 6, 1000);
d.FadeOut(750);
break;
}
d.Expire();
case DroppedObjectAnimation.Explode:
var originalX = droppedObjectTarget.ToSpaceOfOtherDrawable(d.DrawPosition, caughtObjectContainer).X * Scale.X;
d.MoveToY(d.Y - 50, 250, Easing.OutSine).Then().MoveToY(d.Y + 50, 500, Easing.InSine);
d.MoveToX(d.X + originalX * 6, 1000);
d.FadeOut(750);
break;
}
d.Expire();
}
private enum DroppedObjectAnimation

View File

@ -5,7 +5,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Judgements;
@ -22,7 +21,7 @@ namespace osu.Game.Rulesets.Catch.UI
public readonly Catcher MovableCatcher;
private readonly CatchComboDisplay comboDisplay;
public CatcherArea(Container droppedObjectContainer, BeatmapDifficulty difficulty = null)
public CatcherArea(Container<CaughtObject> droppedObjectContainer, BeatmapDifficulty difficulty = null)
{
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
Children = new Drawable[]
@ -42,6 +41,8 @@ namespace osu.Game.Rulesets.Catch.UI
public void OnNewResult(DrawableCatchHitObject hitObject, JudgementResult result)
{
MovableCatcher.OnNewResult(hitObject, result);
if (!result.Type.IsScorable())
return;
@ -56,12 +57,10 @@ namespace osu.Game.Rulesets.Catch.UI
comboDisplay.OnNewResult(hitObject, result);
}
public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result)
=> comboDisplay.OnRevertResult(fruit, result);
public bool AttemptCatch(CatchHitObject obj)
public void OnRevertResult(DrawableCatchHitObject hitObject, JudgementResult result)
{
return MovableCatcher.AttemptCatch(obj);
comboDisplay.OnRevertResult(hitObject, result);
MovableCatcher.OnRevertResult(hitObject, result);
}
protected override void UpdateAfterChildren()

View File

@ -30,5 +30,11 @@ namespace osu.Game.Rulesets.Catch.UI
// Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE;
}
protected override void FreeAfterUse()
{
ClearTransforms();
base.FreeAfterUse();
}
}
}

View File

@ -13,6 +13,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Catch.UI
{
@ -31,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.UI
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new CatchReplayRecorder(replay, (CatchPlayfield)Playfield);
protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield);
protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty, CreateDrawableRepresentation);

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects;
using osuTK;
using osuTK.Graphics;
@ -15,6 +16,7 @@ namespace osu.Game.Rulesets.Catch.UI
public class HitExplosion : PoolableDrawable
{
private Color4 objectColour;
public CatchHitObject HitObject;
public Color4 ObjectColour
{

View File

@ -355,7 +355,11 @@ namespace osu.Game.Rulesets.Mania.Tests
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleReplayPlayer(Score score)
: base(score, false, false)
: base(score, new PlayerConfiguration
{
AllowPause = false,
ShowResults = false,
})
{
}
}

View File

@ -176,7 +176,11 @@ namespace osu.Game.Rulesets.Mania.Tests
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleReplayPlayer(Score score)
: base(score, false, false)
: base(score, new PlayerConfiguration
{
AllowPause = false,
ShowResults = false,
})
{
}
}

View File

@ -0,0 +1,89 @@
// 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.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
public class LegacyManiaJudgementPiece : CompositeDrawable, IAnimatableJudgement
{
private readonly HitResult result;
private readonly Drawable animation;
public LegacyManiaJudgementPiece(HitResult result, Drawable animation)
{
this.result = result;
this.animation = animation;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
float? scorePosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value;
if (scorePosition != null)
scorePosition -= Stage.HIT_TARGET_POSITION + 150;
Y = scorePosition ?? 0;
if (animation != null)
{
InternalChild = animation.With(d =>
{
d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre;
});
}
}
public void PlayAnimation()
{
if (animation == null)
return;
(animation as IFramedAnimation)?.GotoFrame(0);
this.FadeInFromZero(20, Easing.Out)
.Then().Delay(160)
.FadeOutFromOne(40, Easing.In);
switch (result)
{
case HitResult.None:
break;
case HitResult.Miss:
animation.ScaleTo(1.2f).Then().ScaleTo(1, 100, Easing.Out);
animation.RotateTo(0);
animation.RotateTo(RNG.NextSingle(-5.73f, 5.73f), 100, Easing.Out);
break;
default:
animation.ScaleTo(0.8f)
.Then().ScaleTo(1, 40)
// this is actually correct to match stable; there were overlapping transforms.
.Then().ScaleTo(0.85f)
.Then().ScaleTo(0.7f, 40)
.Then().Delay(100)
.Then().ScaleTo(0.4f, 40, Easing.In);
break;
}
}
public Drawable GetAboveHitObjectsProxiedContent() => null;
}
}

View File

@ -136,7 +136,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
string filename = this.GetManiaSkinConfig<string>(hitresult_mapping[result])?.Value
?? default_hitresult_skin_filenames[result];
return this.GetAnimation(filename, true, true);
var animation = this.GetAnimation(filename, true, true);
return animation == null ? null : new LegacyManiaJudgementPiece(result, animation);
}
public override SampleChannel GetSample(ISampleInfo sampleInfo)

View File

@ -5,7 +5,6 @@ using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
@ -20,37 +19,6 @@ namespace osu.Game.Rulesets.Mania.UI
{
}
protected override void ApplyMissAnimations()
{
if (!(JudgementBody.Drawable is DefaultManiaJudgementPiece))
{
// this is temporary logic until mania's skin transformer returns IAnimatableJudgements
JudgementBody.ScaleTo(1.6f);
JudgementBody.ScaleTo(1, 100, Easing.In);
JudgementBody.MoveTo(Vector2.Zero);
JudgementBody.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
JudgementBody.RotateTo(0);
JudgementBody.RotateTo(40, 800, Easing.InQuint);
JudgementBody.FadeOutFromOne(800);
LifetimeEnd = JudgementBody.LatestTransformEndTime;
}
base.ApplyMissAnimations();
}
protected override void ApplyHitAnimations()
{
JudgementBody.ScaleTo(0.8f);
JudgementBody.ScaleTo(1, 250, Easing.OutElastic);
JudgementBody.Delay(50)
.ScaleTo(0.75f, 250)
.FadeOut(200);
}
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
private class DefaultManiaJudgementPiece : DefaultJudgementPiece
@ -66,6 +34,27 @@ namespace osu.Game.Rulesets.Mania.UI
JudgementText.Font = JudgementText.Font.With(size: 25);
}
public override void PlayAnimation()
{
base.PlayAnimation();
switch (Result)
{
case HitResult.None:
case HitResult.Miss:
break;
default:
this.ScaleTo(0.8f);
this.ScaleTo(1, 250, Easing.OutElastic);
this.Delay(50)
.ScaleTo(0.75f, 250)
.FadeOut(200);
break;
}
}
}
}
}

View File

@ -23,6 +23,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mania.UI
{
@ -132,6 +133,6 @@ namespace osu.Game.Rulesets.Mania.UI
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay);
protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new ManiaReplayRecorder(replay);
protected override ReplayRecorder CreateReplayRecorder(Score score) => new ManiaReplayRecorder(score);
}
}

View File

@ -2,18 +2,18 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
public class ManiaReplayRecorder : ReplayRecorder<ManiaAction>
{
public ManiaReplayRecorder(Replay replay)
: base(replay)
public ManiaReplayRecorder(Score score)
: base(score)
{
}

View File

@ -106,7 +106,7 @@ namespace osu.Game.Rulesets.Mania.UI
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Y = HIT_TARGET_POSITION + 150,
Y = HIT_TARGET_POSITION + 150
},
topLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
}

View File

@ -74,7 +74,11 @@ namespace osu.Game.Rulesets.Osu.Tests
private readonly bool userHasCustomColours;
public ExposedPlayer(bool userHasCustomColours)
: base(false, false)
: base(new PlayerConfiguration
{
AllowPause = false,
ShowResults = false,
})
{
this.userHasCustomColours = userHasCustomColours;
}

View File

@ -439,7 +439,11 @@ namespace osu.Game.Rulesets.Osu.Tests
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleReplayPlayer(Score score)
: base(score, false, false)
: base(score, new PlayerConfiguration
{
AllowPause = false,
ShowResults = false,
})
{
}
}

View File

@ -378,7 +378,11 @@ namespace osu.Game.Rulesets.Osu.Tests
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleReplayPlayer(Score score)
: base(score, false, false)
: base(score, new PlayerConfiguration
{
AllowPause = false,
ShowResults = false,
})
{
}
}

View File

@ -12,5 +12,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
public double ApproachRate;
public double OverallDifficulty;
public int HitCircleCount;
public int SpinnerCount;
}
}

View File

@ -48,6 +48,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
return new OsuDifficultyAttributes
{
@ -59,6 +60,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
OverallDifficulty = (80 - hitWindowGreat) / 6,
MaxCombo = maxCombo,
HitCircleCount = hitCirclesCount,
SpinnerCount = spinnerCount,
Skills = skills
};
}

View File

@ -49,10 +49,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
if (mods.Any(m => m is OsuModNoFail))
multiplier *= 0.90;
multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss);
if (mods.Any(m => m is OsuModSpunOut))
multiplier *= 0.95;
multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85);
double aimValue = computeAimValue();
double speedValue = computeSpeedValue();
@ -92,23 +92,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= lengthBonus;
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
aimValue *= Math.Pow(0.97, countMiss);
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (countMiss > 0)
aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), countMiss);
// Combo scaling
if (Attributes.MaxCombo > 0)
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
double approachRateFactor = 1.0;
double approachRateFactor = 0.0;
if (Attributes.ApproachRate > 10.33)
approachRateFactor += 0.3 * (Attributes.ApproachRate - 10.33);
approachRateFactor += 0.4 * (Attributes.ApproachRate - 10.33);
else if (Attributes.ApproachRate < 8.0)
{
approachRateFactor += 0.01 * (8.0 - Attributes.ApproachRate);
}
approachRateFactor += 0.1 * (8.0 - Attributes.ApproachRate);
aimValue *= approachRateFactor;
aimValue *= 1.0 + Math.Min(approachRateFactor, approachRateFactor * (totalHits / 1000.0));
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
if (mods.Any(h => h is OsuModHidden))
@ -137,29 +135,31 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedStrain / 0.0675) - 4.0, 3.0) / 100000.0;
// Longer maps are worth more
speedValue *= 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
speedValue *= lengthBonus;
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
speedValue *= Math.Pow(0.97, countMiss);
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (countMiss > 0)
speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875));
// Combo scaling
if (Attributes.MaxCombo > 0)
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
double approachRateFactor = 1.0;
double approachRateFactor = 0.0;
if (Attributes.ApproachRate > 10.33)
approachRateFactor += 0.3 * (Attributes.ApproachRate - 10.33);
approachRateFactor += 0.4 * (Attributes.ApproachRate - 10.33);
speedValue *= approachRateFactor;
speedValue *= 1.0 + Math.Min(approachRateFactor, approachRateFactor * (totalHits / 1000.0));
if (mods.Any(m => m is OsuModHidden))
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
// Scale the speed value with accuracy _slightly_
speedValue *= 0.02 + accuracy;
// It is important to also consider accuracy difficulty when doing that
speedValue *= 0.96 + Math.Pow(Attributes.OverallDifficulty, 2) / 1600;
// Scale the speed value with accuracy and OD
speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
return speedValue;
}

View File

@ -157,10 +157,16 @@ namespace osu.Game.Rulesets.Osu.Edit
foreach (var h in hitObjects)
{
h.Position = new Vector2(
quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X),
quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y)
);
var newPosition = h.Position;
// guard against no-ops and NaN.
if (scale.X != 0 && quad.Width > 0)
newPosition.X = quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X);
if (scale.Y != 0 && quad.Height > 0)
newPosition.Y = quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y);
h.Position = newPosition;
}
}

View File

@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Mods
var destination = e.MousePosition;
FlashlightPosition = Interpolation.ValueAt(
Math.Clamp(Clock.ElapsedFrameTime, 0, follow_delay), position, destination, 0, follow_delay, Easing.Out);
Math.Min(Math.Abs(Clock.ElapsedFrameTime), follow_delay), position, destination, 0, follow_delay, Easing.Out);
return base.OnMouseMove(e);
}

View File

@ -14,6 +14,7 @@ using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osuTK;
@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.UI
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuFramedReplayInputHandler(replay);
protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new OsuReplayRecorder(replay);
protected override ReplayRecorder CreateReplayRecorder(Score score) => new OsuReplayRecorder(score);
public override double GameplayStartTime
{

View File

@ -2,18 +2,18 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Replays;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.UI
{
public class OsuReplayRecorder : ReplayRecorder<OsuAction>
{
public OsuReplayRecorder(Replay replay)
: base(replay)
public OsuReplayRecorder(Score score)
: base(score)
{
}

View File

@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
public abstract class DrawableTaikoRulesetTestScene : OsuTestScene
{
protected const int DEFAULT_PLAYFIELD_CONTAINER_HEIGHT = 768;
protected DrawableTaikoRuleset DrawableRuleset { get; private set; }
protected Container PlayfieldContainer { get; private set; }
@ -44,10 +46,10 @@ namespace osu.Game.Rulesets.Taiko.Tests
Add(PlayfieldContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
Height = 768,
Height = DEFAULT_PLAYFIELD_CONTAINER_HEIGHT,
Children = new[] { DrawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(new TaikoRuleset().RulesetInfo)) }
});
}

View File

@ -13,12 +13,15 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
public readonly HitResult Type;
public DrawableTestHit(Hit hit, HitResult type = HitResult.Great)
public DrawableTestHit(Hit hit, HitResult type = HitResult.Great, bool kiai = false)
: base(hit)
{
Type = type;
HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new EffectControlPoint { KiaiMode = kiai });
HitObject.ApplyDefaults(controlPoints, new BeatmapDifficulty());
}
protected override void UpdateInitialTransforms()

View File

@ -0,0 +1,58 @@
// 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.Testing;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
{
public abstract class HitObjectApplicationTestScene : OsuTestScene
{
[Cached(typeof(IScrollingInfo))]
private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo
{
Direction = { Value = ScrollingDirection.Left },
TimeRange = { Value = 1000 },
};
private ScrollingHitObjectContainer hitObjectContainer;
[BackgroundDependencyLoader]
private void load()
{
Child = hitObjectContainer = new ScrollingHitObjectContainer
{
RelativeSizeAxes = Axes.X,
Height = 200,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Clock = new FramedClock(new StopwatchClock())
};
}
[SetUpSteps]
public void SetUp()
=> AddStep("clear SHOC", () => hitObjectContainer.Clear(false));
protected void AddHitObject(DrawableHitObject hitObject)
=> AddStep("add to SHOC", () => hitObjectContainer.Add(hitObject));
protected void RemoveHitObject(DrawableHitObject hitObject)
=> AddStep("remove from SHOC", () => hitObjectContainer.Remove(hitObject));
protected TObject PrepareObject<TObject>(TObject hitObject)
where TObject : TaikoHitObject
{
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
return hitObject;
}
}
}

View File

@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
}
};
hoc.Add(new DrawableBarLineMajor(createBarLineAtCurrentTime(true))
hoc.Add(new DrawableBarLine(createBarLineAtCurrentTime(true))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
createDrawableRuleset();
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new StrongHitObject(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new Hit.StrongNestedHit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle);
}
[Test]

View File

@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
IsCentre = (hitObject as Hit)?.Type == HitType.Centre,
IsDrumRoll = hitObject is DrumRoll,
IsSwell = hitObject is Swell,
IsStrong = ((TaikoHitObject)hitObject).IsStrong
IsStrong = (hitObject as TaikoStrongableHitObject)?.IsStrong == true
};
}

View File

@ -0,0 +1,33 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.Tests
{
public class TestSceneBarLineApplication : HitObjectApplicationTestScene
{
[Test]
public void TestApplyNewBarLine()
{
DrawableBarLine barLine = new DrawableBarLine();
AddStep("apply new bar line", () => barLine.Apply(PrepareObject(new BarLine
{
StartTime = 400,
Major = true
}), null));
AddHitObject(barLine);
RemoveHitObject(barLine);
AddStep("apply new bar line", () => barLine.Apply(PrepareObject(new BarLine
{
StartTime = 200,
Major = false
}), null));
AddHitObject(barLine);
}
}
}

View File

@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.Tests
{
public class TestSceneDrumRollApplication : HitObjectApplicationTestScene
{
[Test]
public void TestApplyNewDrumRoll()
{
var drumRoll = new DrawableDrumRoll();
AddStep("apply new drum roll", () => drumRoll.Apply(PrepareObject(new DrumRoll
{
StartTime = 300,
Duration = 500,
IsStrong = false,
TickRate = 2
}), null));
AddHitObject(drumRoll);
RemoveHitObject(drumRoll);
AddStep("apply new drum roll", () => drumRoll.Apply(PrepareObject(new DrumRoll
{
StartTime = 150,
Duration = 400,
IsStrong = true,
TickRate = 16
}), null));
AddHitObject(drumRoll);
}
}
}

View File

@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.Tests
{
public class TestSceneHitApplication : HitObjectApplicationTestScene
{
[Test]
public void TestApplyNewHit()
{
var hit = new DrawableHit();
AddStep("apply new hit", () => hit.Apply(PrepareObject(new Hit
{
Type = HitType.Rim,
IsStrong = false,
StartTime = 300
}), null));
AddHitObject(hit);
RemoveHitObject(hit);
AddStep("apply new hit", () => hit.Apply(PrepareObject(new Hit
{
Type = HitType.Centre,
IsStrong = true,
StartTime = 500
}), null));
AddHitObject(hit);
}
}
}

View File

@ -2,14 +2,16 @@
// 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.Graphics;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Taiko.Objects;
@ -97,7 +99,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
break;
case 6:
PlayfieldContainer.Delay(delay).ResizeTo(new Vector2(1, TaikoPlayfield.DEFAULT_HEIGHT), 500);
PlayfieldContainer.Delay(delay).ResizeTo(new Vector2(1, DEFAULT_PLAYFIELD_CONTAINER_HEIGHT), 500);
break;
}
}
@ -106,13 +108,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great;
var cpi = new ControlPointInfo();
cpi.Add(0, new EffectControlPoint { KiaiMode = kiai });
Hit hit = new Hit();
hit.ApplyDefaults(cpi, new BeatmapDifficulty());
var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) };
var h = new DrawableTestHit(hit, kiai: kiai) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) };
DrawableRuleset.Playfield.Add(h);
@ -123,32 +120,38 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great;
var cpi = new ControlPointInfo();
cpi.Add(0, new EffectControlPoint { KiaiMode = kiai });
Hit hit = new Hit();
hit.ApplyDefaults(cpi, new BeatmapDifficulty());
var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) };
Hit hit = new Hit
{
IsStrong = true,
Samples = createSamples(strong: true)
};
var h = new DrawableTestHit(hit, kiai: kiai) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) };
DrawableRuleset.Playfield.Add(h);
((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great });
((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h.NestedHitObjects.Single(), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great });
}
private void addMissJudgement()
{
DrawableTestHit h;
DrawableRuleset.Playfield.Add(h = new DrawableTestHit(new Hit(), HitResult.Miss));
((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss });
DrawableRuleset.Playfield.Add(h = new DrawableTestHit(new Hit { StartTime = DrawableRuleset.Playfield.Time.Current }, HitResult.Miss)
{
Alpha = 0
});
((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(h.HitObject, new TaikoJudgement()) { Type = HitResult.Miss });
}
private void addBarLine(bool major, double delay = scroll_time)
{
BarLine bl = new BarLine { StartTime = DrawableRuleset.Playfield.Time.Current + delay };
BarLine bl = new BarLine
{
StartTime = DrawableRuleset.Playfield.Time.Current + delay,
Major = major
};
DrawableRuleset.Playfield.Add(major ? new DrawableBarLineMajor(bl) : new DrawableBarLine(bl));
DrawableRuleset.Playfield.Add(bl);
}
private void addSwell(double duration = default_duration)
@ -173,6 +176,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong,
Samples = createSamples(strong: strong),
Duration = duration,
TickRate = 8,
};
@ -190,7 +194,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
Hit h = new Hit
{
StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong
IsStrong = strong,
Samples = createSamples(HitType.Centre, strong)
};
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
@ -203,7 +208,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
Hit h = new Hit
{
StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong
IsStrong = strong,
Samples = createSamples(HitType.Rim, strong)
};
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
@ -211,14 +217,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
DrawableRuleset.Playfield.Add(new DrawableHit(h));
}
private class TestStrongNestedHit : DrawableStrongNestedHit
// TODO: can be removed if a better way of handling colour/strong type and samples is developed
private IList<HitSampleInfo> createSamples(HitType? hitType = null, bool strong = false)
{
public TestStrongNestedHit(DrawableHitObject mainObject)
: base(new StrongHitObject { StartTime = mainObject.HitObject.StartTime }, mainObject)
{
}
var samples = new List<HitSampleInfo>();
public override bool OnPressed(TaikoAction action) => false;
if (hitType == HitType.Rim)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
if (strong)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
return samples;
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Testing;
using osu.Game.Audio;
@ -18,24 +19,33 @@ namespace osu.Game.Rulesets.Taiko.Tests
public override void SetUpSteps()
{
base.SetUpSteps();
AddAssert("has correct samples", () =>
var expectedSampleNames = new[]
{
var names = Player.DrawableRuleset.Playfield.AllHitObjects.OfType<DrawableHit>().Select(h => string.Join(',', h.GetSamples().Select(s => s.Name)));
string.Empty,
string.Empty,
string.Empty,
string.Empty,
HitSampleInfo.HIT_FINISH,
HitSampleInfo.HIT_WHISTLE,
HitSampleInfo.HIT_WHISTLE,
HitSampleInfo.HIT_WHISTLE,
};
var actualSampleNames = new List<string>();
var expected = new[]
{
string.Empty,
string.Empty,
string.Empty,
string.Empty,
HitSampleInfo.HIT_FINISH,
HitSampleInfo.HIT_WHISTLE,
HitSampleInfo.HIT_WHISTLE,
HitSampleInfo.HIT_WHISTLE,
};
// due to pooling we can't access all samples right away due to object re-use,
// so we need to collect as we go.
AddStep("collect sample names", () => Player.DrawableRuleset.Playfield.NewResult += (dho, _) =>
{
if (!(dho is DrawableHit h))
return;
return names.SequenceEqual(expected);
actualSampleNames.Add(string.Join(',', h.GetSamples().Select(s => s.Name)));
});
AddUntilStep("all samples collected", () => actualSampleNames.Count == expectedSampleNames.Length);
AddAssert("samples are correct", () => actualSampleNames.SequenceEqual(expectedSampleNames));
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TaikoBeatmapConversionTest().GetBeatmap("sample-to-type-conversions");

View File

@ -65,8 +65,8 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x =>
{
TaikoHitObject first = x.First();
if (x.Skip(1).Any() && first.CanBeStrong)
first.IsStrong = true;
if (x.Skip(1).Any() && first is TaikoStrongableHitObject strong)
strong.IsStrong = true;
return first;
}).ToList();
}

View File

@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Taiko.Edit
base.UpdateTernaryStates();
selectionRimState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<Hit>(), h => h.Type == HitType.Rim);
selectionStrongState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<TaikoHitObject>(), h => h.IsStrong);
selectionStrongState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<TaikoStrongableHitObject>(), h => h.IsStrong);
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
@ -8,7 +9,13 @@ namespace osu.Game.Rulesets.Taiko.Objects
{
public class BarLine : TaikoHitObject, IBarLine
{
public bool Major { get; set; }
public bool Major
{
get => MajorBindable.Value;
set => MajorBindable.Value = value;
}
public readonly Bindable<bool> MajorBindable = new BindableBool();
public override Judgement CreateJudgement() => new IgnoreJudgement();
}

View File

@ -1,7 +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 JetBrains.Annotations;
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.Objects;
using osuTK;
@ -15,49 +19,123 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
/// </summary>
public class DrawableBarLine : DrawableHitObject<HitObject>
{
public new BarLine HitObject => (BarLine)base.HitObject;
/// <summary>
/// The width of the line tracker.
/// </summary>
private const float tracker_width = 2f;
/// <summary>
/// Fade out time calibrated to a pre-empt of 1000ms.
/// The vertical offset of the triangles from the line tracker.
/// </summary>
private const float base_fadeout_time = 100f;
private const float triangle_offset = 10f;
/// <summary>
/// The size of the triangles.
/// </summary>
private const float triangle_size = 20f;
/// <summary>
/// The visual line tracker.
/// </summary>
protected SkinnableDrawable Line;
private SkinnableDrawable line;
/// <summary>
/// The bar line.
/// Container with triangles. Only visible for major lines.
/// </summary>
protected readonly BarLine BarLine;
private Container triangleContainer;
public DrawableBarLine(BarLine barLine)
private readonly Bindable<bool> major = new Bindable<bool>();
public DrawableBarLine()
: this(null)
{
}
public DrawableBarLine([CanBeNull] BarLine barLine)
: base(barLine)
{
BarLine = barLine;
}
[BackgroundDependencyLoader]
private void load()
{
Anchor = Anchor.CentreLeft;
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Y;
Width = tracker_width;
AddInternal(Line = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.BarLine), _ => new Box
AddRangeInternal(new Drawable[]
{
RelativeSizeAxes = Axes.Both,
EdgeSmoothness = new Vector2(0.5f, 0),
})
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0.75f,
line = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.BarLine), _ => new Box
{
RelativeSizeAxes = Axes.Both,
EdgeSmoothness = new Vector2(0.5f, 0),
})
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
triangleContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Children = new[]
{
new EquilateralTriangle
{
Name = "Top",
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Position = new Vector2(0, -triangle_offset),
Size = new Vector2(-triangle_size),
EdgeSmoothness = new Vector2(1),
},
new EquilateralTriangle
{
Name = "Bottom",
Anchor = Anchor.BottomCentre,
Origin = Anchor.TopCentre,
Position = new Vector2(0, triangle_offset),
Size = new Vector2(triangle_size),
EdgeSmoothness = new Vector2(1),
}
}
}
});
}
protected override void UpdateHitStateTransforms(ArmedState state) => this.FadeOut(150);
protected override void LoadComplete()
{
base.LoadComplete();
major.BindValueChanged(updateMajor, true);
}
private void updateMajor(ValueChangedEvent<bool> major)
{
line.Alpha = major.NewValue ? 1f : 0.75f;
triangleContainer.Alpha = major.NewValue ? 1 : 0;
}
protected override void OnApply()
{
base.OnApply();
major.BindTo(HitObject.MajorBindable);
}
protected override void OnFree()
{
base.OnFree();
major.UnbindFrom(HitObject.MajorBindable);
}
protected override void UpdateHitStateTransforms(ArmedState state)
{
using (BeginAbsoluteSequence(HitObject.StartTime))
this.FadeOutFromOne(150).Expire();
}
}
}

View File

@ -1,67 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osuTK;
using osu.Framework.Graphics.Shapes;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public class DrawableBarLineMajor : DrawableBarLine
{
/// <summary>
/// The vertical offset of the triangles from the line tracker.
/// </summary>
private const float triangle_offfset = 10f;
/// <summary>
/// The size of the triangles.
/// </summary>
private const float triangle_size = 20f;
private readonly Container triangleContainer;
public DrawableBarLineMajor(BarLine barLine)
: base(barLine)
{
AddInternal(triangleContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Children = new[]
{
new EquilateralTriangle
{
Name = "Top",
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Position = new Vector2(0, -triangle_offfset),
Size = new Vector2(-triangle_size),
EdgeSmoothness = new Vector2(1),
},
new EquilateralTriangle
{
Name = "Bottom",
Anchor = Anchor.BottomCentre,
Origin = Anchor.TopCentre,
Position = new Vector2(0, triangle_offfset),
Size = new Vector2(triangle_size),
EdgeSmoothness = new Vector2(1),
}
}
});
Line.Alpha = 1f;
}
protected override void LoadComplete()
{
base.LoadComplete();
using (triangleContainer.BeginAbsoluteSequence(HitObject.StartTime))
triangleContainer.FadeOut(150);
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Utils;
using osu.Game.Graphics;
@ -19,7 +20,7 @@ using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public class DrawableDrumRoll : DrawableTaikoHitObject<DrumRoll>
public class DrawableDrumRoll : DrawableTaikoStrongableHitObject<DrumRoll, DrumRoll.StrongNestedHit>
{
/// <summary>
/// Number of rolling hits required to reach the dark/final colour.
@ -31,15 +32,26 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
/// </summary>
private int rollingHits;
private Container tickContainer;
private readonly Container tickContainer;
private Color4 colourIdle;
private Color4 colourEngaged;
public DrawableDrumRoll(DrumRoll drumRoll)
public DrawableDrumRoll()
: this(null)
{
}
public DrawableDrumRoll([CanBeNull] DrumRoll drumRoll)
: base(drumRoll)
{
RelativeSizeAxes = Axes.Y;
Content.Add(tickContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Depth = float.MinValue
});
}
[BackgroundDependencyLoader]
@ -47,12 +59,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
colourIdle = colours.YellowDark;
colourEngaged = colours.YellowDarker;
Content.Add(tickContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Depth = float.MinValue
});
}
protected override void LoadComplete()
@ -68,6 +74,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
updateColour();
}
protected override void OnFree()
{
base.OnFree();
rollingHits = 0;
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
base.AddNestedHitObject(hitObject);
@ -83,7 +95,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
tickContainer.Clear();
tickContainer.Clear(false);
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
@ -114,7 +126,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
rollingHits = Math.Clamp(rollingHits, 0, rolling_hits_for_engaged_colour);
updateColour();
updateColour(100);
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
@ -154,27 +166,34 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
Content.X = DrawHeight / 2;
}
protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this);
protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRoll.StrongNestedHit hitObject) => new StrongNestedHit(hitObject);
private void updateColour()
private void updateColour(double fadeDuration = 0)
{
Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1);
(MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, 100);
(MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration);
}
private class StrongNestedHit : DrawableStrongNestedHit
public class StrongNestedHit : DrawableStrongNestedHit
{
public StrongNestedHit(StrongHitObject strong, DrawableDrumRoll drumRoll)
: base(strong, drumRoll)
public new DrawableDrumRoll ParentHitObject => (DrawableDrumRoll)base.ParentHitObject;
public StrongNestedHit()
: this(null)
{
}
public StrongNestedHit([CanBeNull] DrumRoll.StrongNestedHit nestedHit)
: base(nestedHit)
{
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (!MainObject.Judged)
if (!ParentHitObject.Judged)
return;
ApplyResult(r => r.Type = MainObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
public override bool OnPressed(TaikoAction action) => false;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Skinning.Default;
@ -9,14 +10,19 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public class DrawableDrumRollTick : DrawableTaikoHitObject<DrumRollTick>
public class DrawableDrumRollTick : DrawableTaikoStrongableHitObject<DrumRollTick, DrumRollTick.StrongNestedHit>
{
/// <summary>
/// The hit type corresponding to the <see cref="TaikoAction"/> that the user pressed to hit this <see cref="DrawableDrumRollTick"/>.
/// </summary>
public HitType JudgementType;
public DrawableDrumRollTick(DrumRollTick tick)
public DrawableDrumRollTick()
: this(null)
{
}
public DrawableDrumRollTick([CanBeNull] DrumRollTick tick)
: base(tick)
{
FillMode = FillMode.Fit;
@ -61,21 +67,28 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
return UpdateResult(true);
}
protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this);
protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRollTick.StrongNestedHit hitObject) => new StrongNestedHit(hitObject);
private class StrongNestedHit : DrawableStrongNestedHit
public class StrongNestedHit : DrawableStrongNestedHit
{
public StrongNestedHit(StrongHitObject strong, DrawableDrumRollTick tick)
: base(strong, tick)
public new DrawableDrumRollTick ParentHitObject => (DrawableDrumRollTick)base.ParentHitObject;
public StrongNestedHit()
: this(null)
{
}
public StrongNestedHit([CanBeNull] DrumRollTick.StrongNestedHit nestedHit)
: base(nestedHit)
{
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (!MainObject.Judged)
if (!ParentHitObject.Judged)
return;
ApplyResult(r => r.Type = MainObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
public override bool OnPressed(TaikoAction action) => false;

View File

@ -5,7 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Audio;
@ -16,7 +16,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public class DrawableHit : DrawableTaikoHitObject<Hit>
public class DrawableHit : DrawableTaikoStrongableHitObject<Hit, Hit.StrongNestedHit>
{
/// <summary>
/// A list of keys which can result in hits for this HitObject.
@ -36,29 +36,51 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private bool pressHandledThisFrame;
private readonly Bindable<HitType> type;
private readonly Bindable<HitType> type = new Bindable<HitType>();
public DrawableHit(Hit hit)
: base(hit)
public DrawableHit()
: this(null)
{
type = HitObject.TypeBindable.GetBoundCopy();
FillMode = FillMode.Fit;
updateActionsFromType();
}
[BackgroundDependencyLoader]
private void load()
public DrawableHit([CanBeNull] Hit hit)
: base(hit)
{
FillMode = FillMode.Fit;
}
protected override void OnApply()
{
type.BindTo(HitObject.TypeBindable);
type.BindValueChanged(_ =>
{
updateActionsFromType();
// will overwrite samples, should only be called on change.
// will overwrite samples, should only be called on subsequent changes
// after the initial application.
updateSamplesFromTypeChange();
RecreatePieces();
});
// action update also has to happen immediately on application.
updateActionsFromType();
base.OnApply();
}
protected override void OnFree()
{
base.OnFree();
type.UnbindFrom(HitObject.TypeBindable);
type.UnbindEvents();
UnproxyContent();
HitActions = null;
HitAction = null;
validActionPressed = pressHandledThisFrame = false;
}
private HitSampleInfo[] getRimSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray();
@ -228,32 +250,37 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
}
}
protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this);
protected override DrawableStrongNestedHit CreateStrongNestedHit(Hit.StrongNestedHit hitObject) => new StrongNestedHit(hitObject);
private class StrongNestedHit : DrawableStrongNestedHit
public class StrongNestedHit : DrawableStrongNestedHit
{
public new DrawableHit ParentHitObject => (DrawableHit)base.ParentHitObject;
/// <summary>
/// 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 new DrawableHit MainObject => (DrawableHit)base.MainObject;
public StrongNestedHit()
: this(null)
{
}
public StrongNestedHit(StrongHitObject strong, DrawableHit hit)
: base(strong, hit)
public StrongNestedHit([CanBeNull] Hit.StrongNestedHit nestedHit)
: base(nestedHit)
{
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (!MainObject.Result.HasResult)
if (!ParentHitObject.Result.HasResult)
{
base.CheckForResult(userTriggered, timeOffset);
return;
}
if (!MainObject.Result.IsHit)
if (!ParentHitObject.Result.IsHit)
{
ApplyResult(r => r.Type = r.Judgement.MinResult);
return;
@ -261,27 +288,27 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (!userTriggered)
{
if (timeOffset - MainObject.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 - MainObject.Result.TimeOffset) <= second_hit_window)
if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= second_hit_window)
ApplyResult(r => r.Type = r.Judgement.MaxResult);
}
public override bool OnPressed(TaikoAction action)
{
// Don't process actions until the main hitobject is hit
if (!MainObject.IsHit)
if (!ParentHitObject.IsHit)
return false;
// Don't process actions if the pressed button was released
if (MainObject.HitAction == null)
if (ParentHitObject.HitAction == null)
return false;
// Don't handle invalid hit action presses
if (!MainObject.HitActions.Contains(action))
if (!ParentHitObject.HitActions.Contains(action))
return false;
return UpdateResult(true);

View File

@ -1,22 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects.Drawables;
using JetBrains.Annotations;
using osu.Game.Rulesets.Taiko.Judgements;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
/// <summary>
/// Used as a nested hitobject to provide <see cref="TaikoStrongJudgement"/>s for <see cref="DrawableTaikoHitObject"/>s.
/// Used as a nested hitobject to provide <see cref="TaikoStrongJudgement"/>s for <see cref="DrawableTaikoStrongableHitObject{TObject,TStrongNestedObject}"/>s.
/// </summary>
public abstract class DrawableStrongNestedHit : DrawableTaikoHitObject
{
public readonly DrawableHitObject MainObject;
public new DrawableTaikoHitObject ParentHitObject => (DrawableTaikoHitObject)base.ParentHitObject;
protected DrawableStrongNestedHit(StrongHitObject strong, DrawableHitObject mainObject)
: base(strong)
protected DrawableStrongNestedHit([CanBeNull] StrongNestedHitObject nestedHit)
: base(nestedHit)
{
MainObject = mainObject;
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@ -35,7 +36,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private readonly CircularContainer targetRing;
private readonly CircularContainer expandingRing;
public DrawableSwell(Swell swell)
public DrawableSwell()
: this(null)
{
}
public DrawableSwell([CanBeNull] Swell swell)
: base(swell)
{
FillMode = FillMode.Fit;
@ -123,12 +129,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
Origin = Anchor.Centre,
});
protected override void LoadComplete()
protected override void OnFree()
{
base.LoadComplete();
base.OnFree();
// We need to set this here because RelativeSizeAxes won't/can't set our size by default with a different RelativeChildSize
Width *= Parent.RelativeChildSize.X;
UnproxyContent();
lastWasCentre = null;
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
@ -146,7 +153,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
ticks.Clear();
ticks.Clear(false);
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
@ -168,7 +175,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
foreach (var t in ticks)
{
if (!t.IsHit)
if (!t.Result.HasResult)
{
nextTick = t;
break;
@ -208,7 +215,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
continue;
}
tick.TriggerResult(false);
if (!tick.Result.HasResult)
tick.TriggerResult(false);
}
ApplyResult(r => r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : r.Judgement.MinResult);

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Taiko.Skinning.Default;
using osu.Game.Skinning;
@ -11,7 +12,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public override bool DisplayResult => false;
public DrawableSwellTick(SwellTick hitObject)
public DrawableSwellTick()
: this(null)
{
}
public DrawableSwellTick([CanBeNull] SwellTick hitObject)
: base(hitObject)
{
}

View File

@ -3,14 +3,12 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input.Bindings;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
@ -24,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private readonly Container nonProxiedContent;
protected DrawableTaikoHitObject(TaikoHitObject hitObject)
protected DrawableTaikoHitObject([CanBeNull] TaikoHitObject hitObject)
: base(hitObject)
{
AddRangeInternal(new[]
@ -115,117 +113,37 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public override Vector2 OriginPosition => new Vector2(DrawHeight / 2);
public new TObject HitObject;
public new TObject HitObject => (TObject)base.HitObject;
protected Vector2 BaseSize;
protected SkinnableDrawable MainPiece;
private readonly Bindable<bool> isStrong;
private readonly Container<DrawableStrongNestedHit> strongHitContainer;
protected DrawableTaikoHitObject(TObject hitObject)
protected DrawableTaikoHitObject([CanBeNull] TObject hitObject)
: base(hitObject)
{
HitObject = hitObject;
isStrong = HitObject.IsStrongBindable.GetBoundCopy();
Anchor = Anchor.CentreLeft;
Origin = Anchor.Custom;
RelativeSizeAxes = Axes.Both;
AddInternal(strongHitContainer = new Container<DrawableStrongNestedHit>());
}
[BackgroundDependencyLoader]
private void load()
protected override void OnApply()
{
isStrong.BindValueChanged(_ =>
{
// will overwrite samples, should only be called on change.
updateSamplesFromStrong();
RecreatePieces();
});
base.OnApply();
RecreatePieces();
}
private HitSampleInfo[] getStrongSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray();
protected override void LoadSamples()
{
base.LoadSamples();
if (HitObject.CanBeStrong)
isStrong.Value = getStrongSamples().Any();
}
private void updateSamplesFromStrong()
{
var strongSamples = getStrongSamples();
if (isStrong.Value != strongSamples.Any())
{
if (isStrong.Value)
HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
else
{
foreach (var sample in strongSamples)
HitObject.Samples.Remove(sample);
}
}
}
protected virtual void RecreatePieces()
{
Size = BaseSize = new Vector2(HitObject.IsStrong ? TaikoHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE);
Size = BaseSize = new Vector2(TaikoHitObject.DEFAULT_SIZE);
MainPiece?.Expire();
Content.Add(MainPiece = CreateMainPiece());
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
base.AddNestedHitObject(hitObject);
switch (hitObject)
{
case DrawableStrongNestedHit strong:
strongHitContainer.Add(strong);
break;
}
}
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
strongHitContainer.Clear();
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
{
switch (hitObject)
{
case StrongHitObject strong:
return CreateStrongHit(strong);
}
return base.CreateNestedHitObject(hitObject);
}
// Most osu!taiko hitsounds are managed by the drum (see DrumSampleMapping).
public override IEnumerable<HitSampleInfo> GetSamples() => Enumerable.Empty<HitSampleInfo>();
protected abstract SkinnableDrawable CreateMainPiece();
/// <summary>
/// Creates the handler for this <see cref="DrawableHitObject"/>'s <see cref="StrongHitObject"/>.
/// This is only invoked if <see cref="TaikoHitObject.IsStrong"/> is true for <see cref="HitObject"/>.
/// </summary>
/// <param name="hitObject">The strong hitobject.</param>
/// <returns>The strong hitobject handler.</returns>
protected virtual DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => null;
}
}

View File

@ -0,0 +1,121 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public abstract class DrawableTaikoStrongableHitObject<TObject, TStrongNestedObject> : DrawableTaikoHitObject<TObject>
where TObject : TaikoStrongableHitObject
where TStrongNestedObject : StrongNestedHitObject
{
private readonly Bindable<bool> isStrong = new BindableBool();
private readonly Container<DrawableStrongNestedHit> strongHitContainer;
protected DrawableTaikoStrongableHitObject([CanBeNull] TObject hitObject)
: base(hitObject)
{
AddInternal(strongHitContainer = new Container<DrawableStrongNestedHit>());
}
protected override void OnApply()
{
isStrong.BindTo(HitObject.IsStrongBindable);
isStrong.BindValueChanged(_ =>
{
// will overwrite samples, should only be called on subsequent changes
// after the initial application.
updateSamplesFromStrong();
RecreatePieces();
});
base.OnApply();
}
protected override void OnFree()
{
base.OnFree();
isStrong.UnbindFrom(HitObject.IsStrongBindable);
// ensure the next application does not accidentally overwrite samples.
isStrong.UnbindEvents();
}
private HitSampleInfo[] getStrongSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray();
protected override void LoadSamples()
{
base.LoadSamples();
isStrong.Value = getStrongSamples().Any();
}
private void updateSamplesFromStrong()
{
var strongSamples = getStrongSamples();
if (isStrong.Value != strongSamples.Any())
{
if (isStrong.Value)
HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
else
{
foreach (var sample in strongSamples)
HitObject.Samples.Remove(sample);
}
}
}
protected override void RecreatePieces()
{
base.RecreatePieces();
if (HitObject.IsStrong)
Size = BaseSize = new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE);
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
base.AddNestedHitObject(hitObject);
switch (hitObject)
{
case DrawableStrongNestedHit strong:
strongHitContainer.Add(strong);
break;
}
}
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
strongHitContainer.Clear(false);
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
{
switch (hitObject)
{
case TStrongNestedObject strong:
return CreateStrongNestedHit(strong);
}
return base.CreateNestedHitObject(hitObject);
}
/// <summary>
/// Creates the handler for this <see cref="DrawableHitObject"/>'s <see cref="StrongNestedHitObject"/>.
/// This is only invoked if <see cref="TaikoStrongableHitObject.IsStrong"/> is true for <see cref="HitObject"/>.
/// </summary>
/// <param name="hitObject">The strong hitobject.</param>
/// <returns>The strong hitobject handler.</returns>
protected abstract DrawableStrongNestedHit CreateStrongNestedHit(TStrongNestedObject hitObject);
}
}

View File

@ -15,7 +15,7 @@ using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects
{
public class DrumRoll : TaikoHitObject, IHasPath
public class DrumRoll : TaikoStrongableHitObject, IHasPath
{
/// <summary>
/// Drum roll distance that results in a duration of 1 speed-adjusted beat length.
@ -109,6 +109,12 @@ namespace osu.Game.Rulesets.Taiko.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
public class StrongNestedHit : StrongNestedHitObject
{
}
#region LegacyBeatmapEncoder
double IHasDistance.Distance => Duration * Velocity;

View File

@ -7,7 +7,7 @@ using osu.Game.Rulesets.Taiko.Judgements;
namespace osu.Game.Rulesets.Taiko.Objects
{
public class DrumRollTick : TaikoHitObject
public class DrumRollTick : TaikoStrongableHitObject
{
/// <summary>
/// Whether this is the first (initial) tick of the slider.
@ -28,5 +28,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
public class StrongNestedHit : StrongNestedHitObject
{
}
}
}

View File

@ -5,7 +5,7 @@ using osu.Framework.Bindables;
namespace osu.Game.Rulesets.Taiko.Objects
{
public class Hit : TaikoHitObject
public class Hit : TaikoStrongableHitObject
{
public readonly Bindable<HitType> TypeBindable = new Bindable<HitType>();
@ -17,5 +17,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
get => TypeBindable.Value;
set => TypeBindable.Value = value;
}
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
public class StrongNestedHit : StrongNestedHitObject
{
}
}
}

View File

@ -7,7 +7,11 @@ using osu.Game.Rulesets.Taiko.Judgements;
namespace osu.Game.Rulesets.Taiko.Objects
{
public class StrongHitObject : TaikoHitObject
/// <summary>
/// Base type for nested strong hits.
/// Used by <see cref="TaikoStrongableHitObject"/>s to represent their strong bonus scoring portions.
/// </summary>
public abstract class StrongNestedHitObject : TaikoHitObject
{
public override Judgement CreateJudgement() => new TaikoStrongJudgement();

View File

@ -17,8 +17,6 @@ namespace osu.Game.Rulesets.Taiko.Objects
set => Duration = value - StartTime;
}
public override bool CanBeStrong => false;
public double Duration { get; set; }
/// <summary>

View File

@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
@ -19,47 +16,6 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// </summary>
public const float DEFAULT_SIZE = 0.45f;
/// <summary>
/// Scale multiplier for a strong drawable taiko hit object.
/// </summary>
public const float STRONG_SCALE = 1.4f;
/// <summary>
/// Default size of a strong drawable taiko hit object.
/// </summary>
public const float DEFAULT_STRONG_SIZE = DEFAULT_SIZE * STRONG_SCALE;
public readonly Bindable<bool> IsStrongBindable = new BindableBool();
/// <summary>
/// Whether this <see cref="TaikoHitObject"/> can be made a "strong" (large) hit.
/// </summary>
public virtual bool CanBeStrong => true;
/// <summary>
/// Whether this HitObject is a "strong" type.
/// Strong hit objects give more points for hitting the hit object with both keys.
/// </summary>
public bool IsStrong
{
get => IsStrongBindable.Value;
set
{
if (value && !CanBeStrong)
throw new InvalidOperationException($"Object of type {GetType()} cannot be strong");
IsStrongBindable.Value = value;
}
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
base.CreateNestedHitObjects(cancellationToken);
if (IsStrong)
AddNested(new StrongHitObject { StartTime = this.GetEndTime() });
}
public override Judgement CreateJudgement() => new TaikoJudgement();
protected override HitWindows CreateHitWindows() => new TaikoHitWindows();

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