Merge branch 'master' into difficulty-adjustment-extension

This commit is contained in:
smoogipoo
2021-02-10 20:42:08 +09:00
679 changed files with 18700 additions and 6150 deletions

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

@ -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

@ -67,8 +67,8 @@ namespace osu.Game.Rulesets.Catch.Mods
{
base.ApplySettings(difficulty);
difficulty.CircleSize = CircleSize.Value;
difficulty.ApproachRate = ApproachRate.Value;
ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
}
}
}

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

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader]
private void load()
{
ScaleContainer.Child = new SkinnableDrawable(
ScalingContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(CatchSkinComponents.Banana),
_ => new BananaPiece());
}
@ -44,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

@ -15,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;
@ -38,22 +39,20 @@ 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,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader]
private void load()
{
ScaleContainer.Child = new SkinnableDrawable(
ScalingContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(CatchSkinComponents.Droplet),
_ => new DropletPiece());
}
@ -39,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

@ -10,9 +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>();
public Bindable<FruitVisualRepresentation> VisualRepresentation { get; } = new Bindable<FruitVisualRepresentation>();
public DrawableFruit()
: this(null)
@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
VisualRepresentation.Value = (FruitVisualRepresentation)(change.NewValue % 4);
}, true);
ScaleContainer.Child = new SkinnableDrawable(
ScalingContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(CatchSkinComponents.Fruit),
_ => new FruitPiece());
}
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
base.UpdateInitialTransforms();
ScaleContainer.RotateTo((RandomSingle(1) - 0.5f) * 40);
ScalingContainer.RotateTo((RandomSingle(1) - 0.5f) * 40);
}
}

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,9 +42,14 @@ 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;
if (timeAvailable < 0)
{
return;
}
// So we can either make it there without a dash or not.
// If positionChange is 0, we don't need to move, so speedRequired should also be 0 (could be NaN if timeAvailable is 0 too)
// The case where positionChange > 0 and timeAvailable == 0 results in PositiveInfinity which provides expected beheaviour.
@ -56,7 +61,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 +71,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 +85,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

@ -29,4 +29,3 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
}
}
}

View File

@ -7,7 +7,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Skinning.Default
@ -17,9 +16,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
public readonly Bindable<bool> HyperDash = new Bindable<bool>();
[Resolved(canBeNull: true)]
[CanBeNull]
protected DrawableHitObject DrawableHitObject { get; private set; }
[Resolved]
protected IHasCatchObjectState ObjectState { get; private set; }
/// <summary>
/// A part of this piece that will be faded out while falling in the playfield.
@ -37,13 +35,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
{
base.LoadComplete();
var hitObject = (DrawablePalpableCatchHitObject)DrawableHitObject;
if (hitObject != null)
{
AccentColour.BindTo(hitObject.AccentColour);
HyperDash.BindTo(hitObject.HyperDash);
}
AccentColour.BindTo(ObjectState.AccentColour);
HyperDash.BindTo(ObjectState.HyperDash);
HyperDash.BindValueChanged(hyper =>
{
@ -54,8 +47,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
protected override void Update()
{
if (BorderPiece != null && DrawableHitObject?.HitObject != null)
BorderPiece.Alpha = (float)Math.Clamp((DrawableHitObject.HitObject.StartTime - Time.Current) / 500, 0, 1);
if (BorderPiece != null)
BorderPiece.Alpha = (float)Math.Clamp((ObjectState.HitObject.StartTime - Time.Current) / 500, 0, 1);
}
}
}

View File

@ -39,10 +39,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
{
base.LoadComplete();
var fruit = (DrawableFruit)DrawableHitObject;
if (fruit != null)
VisualRepresentation.BindTo(fruit.VisualRepresentation);
var fruitState = (IHasFruitState)ObjectState;
VisualRepresentation.BindTo(fruitState.VisualRepresentation);
}
}
}

View File

@ -19,4 +19,3 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
}
}
}

View File

@ -14,10 +14,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
base.LoadComplete();
var fruit = (DrawableFruit)DrawableHitObject;
if (fruit != null)
VisualRepresentation.BindTo(fruit.VisualRepresentation);
var fruitState = (IHasFruitState)ObjectState;
VisualRepresentation.BindTo(fruitState.VisualRepresentation);
VisualRepresentation.BindValueChanged(visual => setTexture(visual.NewValue), true);
}

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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -10,7 +9,6 @@ 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.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@ -29,9 +27,8 @@ namespace osu.Game.Rulesets.Catch.Skinning
[Resolved]
protected ISkinSource Skin { get; private set; }
[Resolved(canBeNull: true)]
[CanBeNull]
protected DrawableHitObject DrawableHitObject { get; private set; }
[Resolved]
protected IHasCatchObjectState ObjectState { get; private set; }
protected LegacyCatchHitObjectPiece()
{
@ -65,13 +62,8 @@ namespace osu.Game.Rulesets.Catch.Skinning
{
base.LoadComplete();
var hitObject = (DrawablePalpableCatchHitObject)DrawableHitObject;
if (hitObject != null)
{
AccentColour.BindTo(hitObject.AccentColour);
HyperDash.BindTo(hitObject.HyperDash);
}
AccentColour.BindTo(ObjectState.AccentColour);
HyperDash.BindTo(ObjectState.HyperDash);
hyperSprite.Colour = Skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value ??
Skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ??

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,
};

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

@ -53,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; }
@ -108,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;
@ -124,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,
@ -172,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.
@ -202,7 +216,7 @@ 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;
return catchObjectPosition >= catcherPosition - halfCatchWidth &&
@ -215,10 +229,19 @@ namespace osu.Game.Rulesets.Catch.UI
catchResult.CatcherAnimationState = CurrentState;
catchResult.CatcherHyperDash = HyperDashing;
if (!(drawableObject.HitObject is PalpableCatchHitObject hitObject)) return;
if (!(drawableObject is DrawablePalpableCatchHitObject palpableObject)) return;
var hitObject = palpableObject.HitObject;
if (result.IsHit)
placeCaughtObject(hitObject);
{
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 (hitObject is TinyDroplet) return;
@ -227,10 +250,10 @@ namespace osu.Game.Rulesets.Catch.UI
{
var target = hitObject.HyperDashTarget;
var timeDifference = target.StartTime - hitObject.StartTime;
double positionDifference = target.X - X;
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();
@ -256,8 +279,8 @@ namespace osu.Game.Rulesets.Catch.UI
SetHyperDashState();
}
caughtFruitContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject);
droppedObjectTarget.RemoveAll(d => (d as DrawableCatchHitObject)?.HitObject == drawableObject.HitObject);
caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject);
droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject);
hitExplosionContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject);
}
@ -441,138 +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.HitObject = caughtObject.HitObject;
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

@ -21,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[]

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

@ -5,6 +5,13 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Description>catch the fruit. to the beat.</Description>
</PropertyGroup>
<PropertyGroup Label="Nuget">
<Title>osu!catch (ruleset)</Title>
<PackageId>ppy.osu.Game.Rulesets.Catch</PackageId>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup Label="Project References">
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
</ItemGroup>