2019-01-24 08:43:03 +00:00
|
|
|
|
// 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.
|
2018-04-13 09:19:50 +00:00
|
|
|
|
|
2017-12-01 06:08:12 +00:00
|
|
|
|
using System;
|
2017-12-01 11:39:58 +00:00
|
|
|
|
using System.Linq;
|
2017-09-18 03:48:33 +00:00
|
|
|
|
using osu.Game.Beatmaps;
|
2017-09-15 11:54:34 +00:00
|
|
|
|
using osu.Game.Rulesets.Catch.Objects;
|
2017-12-01 06:08:12 +00:00
|
|
|
|
using osu.Game.Rulesets.Catch.UI;
|
2020-11-24 10:57:37 +00:00
|
|
|
|
using osu.Game.Rulesets.Objects.Types;
|
2022-04-28 08:46:00 +00:00
|
|
|
|
using osu.Game.Utils;
|
2018-04-13 09:19:50 +00:00
|
|
|
|
|
2017-09-15 11:54:34 +00:00
|
|
|
|
namespace osu.Game.Rulesets.Catch.Beatmaps
|
|
|
|
|
{
|
2018-04-19 13:04:12 +00:00
|
|
|
|
public class CatchBeatmapProcessor : BeatmapProcessor
|
2017-09-15 11:54:34 +00:00
|
|
|
|
{
|
2018-06-29 03:45:48 +00:00
|
|
|
|
public const int RNG_SEED = 1337;
|
|
|
|
|
|
2021-06-23 05:11:25 +00:00
|
|
|
|
public bool HardRockOffsets { get; set; }
|
|
|
|
|
|
2018-04-19 13:04:12 +00:00
|
|
|
|
public CatchBeatmapProcessor(IBeatmap beatmap)
|
|
|
|
|
: base(beatmap)
|
2017-09-15 11:54:34 +00:00
|
|
|
|
{
|
2018-04-19 13:04:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-11-23 04:41:01 +00:00
|
|
|
|
public override void PreProcess()
|
|
|
|
|
{
|
|
|
|
|
IHasComboInformation? lastObj = null;
|
|
|
|
|
|
|
|
|
|
// For sanity, ensures that both the first hitobject and the first hitobject after a banana shower start a new combo.
|
|
|
|
|
// This is normally enforced by the legacy decoder, but is not enforced by the editor.
|
|
|
|
|
foreach (var obj in Beatmap.HitObjects.OfType<IHasComboInformation>())
|
|
|
|
|
{
|
|
|
|
|
if (obj is not BananaShower && (lastObj == null || lastObj is BananaShower))
|
|
|
|
|
obj.NewCombo = true;
|
|
|
|
|
lastObj = obj;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
base.PreProcess();
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-29 03:45:48 +00:00
|
|
|
|
public override void PostProcess()
|
|
|
|
|
{
|
2018-04-19 13:04:12 +00:00
|
|
|
|
base.PostProcess();
|
2018-04-13 09:19:50 +00:00
|
|
|
|
|
2019-07-31 09:44:01 +00:00
|
|
|
|
ApplyPositionOffsets(Beatmap);
|
2018-06-29 03:45:48 +00:00
|
|
|
|
|
2018-03-20 06:45:40 +00:00
|
|
|
|
int index = 0;
|
2019-04-01 03:16:05 +00:00
|
|
|
|
|
2018-04-19 13:04:12 +00:00
|
|
|
|
foreach (var obj in Beatmap.HitObjects.OfType<CatchHitObject>())
|
2018-06-26 11:13:55 +00:00
|
|
|
|
{
|
2020-02-19 06:37:12 +00:00
|
|
|
|
obj.IndexInBeatmap = index;
|
|
|
|
|
foreach (var nested in obj.NestedHitObjects.OfType<CatchHitObject>())
|
|
|
|
|
nested.IndexInBeatmap = index;
|
|
|
|
|
|
2018-06-26 11:13:55 +00:00
|
|
|
|
if (obj.LastInCombo && obj.NestedHitObjects.LastOrDefault() is IHasComboInformation lastNested)
|
|
|
|
|
lastNested.LastInCombo = true;
|
2020-02-19 06:37:12 +00:00
|
|
|
|
|
|
|
|
|
index++;
|
2018-06-26 11:13:55 +00:00
|
|
|
|
}
|
2017-09-15 11:54:34 +00:00
|
|
|
|
}
|
2018-04-13 09:19:50 +00:00
|
|
|
|
|
2021-06-23 05:11:25 +00:00
|
|
|
|
public void ApplyPositionOffsets(IBeatmap beatmap)
|
2018-05-25 10:11:29 +00:00
|
|
|
|
{
|
2022-04-28 08:46:00 +00:00
|
|
|
|
var rng = new LegacyRandom(RNG_SEED);
|
2018-05-25 10:11:29 +00:00
|
|
|
|
|
2019-07-31 09:44:01 +00:00
|
|
|
|
float? lastPosition = null;
|
|
|
|
|
double lastStartTime = 0;
|
|
|
|
|
|
2019-08-01 04:33:00 +00:00
|
|
|
|
foreach (var obj in beatmap.HitObjects.OfType<CatchHitObject>())
|
2018-05-25 10:11:29 +00:00
|
|
|
|
{
|
2019-08-01 04:33:00 +00:00
|
|
|
|
obj.XOffset = 0;
|
|
|
|
|
|
2018-05-25 10:11:29 +00:00
|
|
|
|
switch (obj)
|
|
|
|
|
{
|
2019-07-31 09:44:01 +00:00
|
|
|
|
case Fruit fruit:
|
2021-06-23 05:11:25 +00:00
|
|
|
|
if (HardRockOffsets)
|
2019-07-31 09:44:01 +00:00
|
|
|
|
applyHardRockOffset(fruit, ref lastPosition, ref lastStartTime, rng);
|
|
|
|
|
break;
|
|
|
|
|
|
2018-05-25 10:11:29 +00:00
|
|
|
|
case BananaShower bananaShower:
|
2018-06-29 06:01:33 +00:00
|
|
|
|
foreach (var banana in bananaShower.NestedHitObjects.OfType<Banana>())
|
2018-05-25 10:11:29 +00:00
|
|
|
|
{
|
2020-07-01 15:21:45 +00:00
|
|
|
|
banana.XOffset = (float)(rng.NextDouble() * CatchPlayfield.WIDTH);
|
2018-06-13 09:39:26 +00:00
|
|
|
|
rng.Next(); // osu!stable retrieved a random banana type
|
|
|
|
|
rng.Next(); // osu!stable retrieved a random banana rotation
|
2018-06-13 12:10:54 +00:00
|
|
|
|
rng.Next(); // osu!stable retrieved a random banana colour
|
2018-05-25 10:11:29 +00:00
|
|
|
|
}
|
2019-02-28 04:31:40 +00:00
|
|
|
|
|
2018-05-25 10:11:29 +00:00
|
|
|
|
break;
|
2019-04-01 03:16:05 +00:00
|
|
|
|
|
2018-05-25 10:11:29 +00:00
|
|
|
|
case JuiceStream juiceStream:
|
2020-03-11 09:37:58 +00:00
|
|
|
|
// Todo: BUG!! Stable used the last control point as the final position of the path, but it should use the computed path instead.
|
2021-08-25 16:42:57 +00:00
|
|
|
|
lastPosition = juiceStream.OriginalX + juiceStream.Path.ControlPoints[^1].Position.X;
|
2020-03-11 09:37:58 +00:00
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
2018-05-25 10:11:29 +00:00
|
|
|
|
foreach (var nested in juiceStream.NestedHitObjects)
|
|
|
|
|
{
|
2019-08-01 05:57:17 +00:00
|
|
|
|
var catchObject = (CatchHitObject)nested;
|
|
|
|
|
catchObject.XOffset = 0;
|
|
|
|
|
|
|
|
|
|
if (catchObject is TinyDroplet)
|
2020-12-09 08:58:53 +00:00
|
|
|
|
catchObject.XOffset = Math.Clamp(rng.Next(-20, 20), -catchObject.OriginalX, CatchPlayfield.WIDTH - catchObject.OriginalX);
|
2019-08-01 05:57:17 +00:00
|
|
|
|
else if (catchObject is Droplet)
|
2018-06-13 10:52:04 +00:00
|
|
|
|
rng.Next(); // osu!stable retrieved a random droplet rotation
|
2018-05-25 10:11:29 +00:00
|
|
|
|
}
|
2019-02-28 04:31:40 +00:00
|
|
|
|
|
2018-05-25 10:11:29 +00:00
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-03-11 09:36:37 +00:00
|
|
|
|
|
|
|
|
|
initialiseHyperDash(beatmap);
|
2018-05-25 10:11:29 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-04-28 08:46:00 +00:00
|
|
|
|
private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, LegacyRandom rng)
|
2019-07-31 09:44:01 +00:00
|
|
|
|
{
|
2020-12-09 08:58:53 +00:00
|
|
|
|
float offsetPosition = hitObject.OriginalX;
|
2019-07-31 09:44:01 +00:00
|
|
|
|
double startTime = hitObject.StartTime;
|
|
|
|
|
|
|
|
|
|
if (lastPosition == null)
|
|
|
|
|
{
|
2019-08-01 04:33:00 +00:00
|
|
|
|
lastPosition = offsetPosition;
|
2019-07-31 09:44:01 +00:00
|
|
|
|
lastStartTime = startTime;
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-01 04:33:00 +00:00
|
|
|
|
float positionDiff = offsetPosition - lastPosition.Value;
|
2020-03-11 09:43:08 +00:00
|
|
|
|
|
|
|
|
|
// Todo: BUG!! Stable calculated time deltas as ints, which affects randomisation. This should be changed to a double.
|
|
|
|
|
int timeDiff = (int)(startTime - lastStartTime);
|
2019-07-31 09:44:01 +00:00
|
|
|
|
|
|
|
|
|
if (timeDiff > 1000)
|
|
|
|
|
{
|
2019-08-01 04:33:00 +00:00
|
|
|
|
lastPosition = offsetPosition;
|
2019-07-31 09:44:01 +00:00
|
|
|
|
lastStartTime = startTime;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (positionDiff == 0)
|
|
|
|
|
{
|
2019-08-01 04:33:00 +00:00
|
|
|
|
applyRandomOffset(ref offsetPosition, timeDiff / 4d, rng);
|
2020-12-09 08:58:53 +00:00
|
|
|
|
hitObject.XOffset = offsetPosition - hitObject.OriginalX;
|
2019-07-31 09:44:01 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-11 09:43:08 +00:00
|
|
|
|
// ReSharper disable once PossibleLossOfFraction
|
2020-07-01 15:21:45 +00:00
|
|
|
|
if (Math.Abs(positionDiff) < timeDiff / 3)
|
2019-08-01 04:33:00 +00:00
|
|
|
|
applyOffset(ref offsetPosition, positionDiff);
|
2019-07-31 09:44:01 +00:00
|
|
|
|
|
2020-12-09 08:58:53 +00:00
|
|
|
|
hitObject.XOffset = offsetPosition - hitObject.OriginalX;
|
2019-07-31 09:44:01 +00:00
|
|
|
|
|
2019-08-01 04:33:00 +00:00
|
|
|
|
lastPosition = offsetPosition;
|
2019-07-31 09:44:01 +00:00
|
|
|
|
lastStartTime = startTime;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Applies a random offset in a random direction to a position, ensuring that the final position remains within the boundary of the playfield.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="position">The position which the offset should be applied to.</param>
|
|
|
|
|
/// <param name="maxOffset">The maximum offset, cannot exceed 20px.</param>
|
|
|
|
|
/// <param name="rng">The random number generator.</param>
|
2022-04-28 08:46:00 +00:00
|
|
|
|
private static void applyRandomOffset(ref float position, double maxOffset, LegacyRandom rng)
|
2019-07-31 09:44:01 +00:00
|
|
|
|
{
|
|
|
|
|
bool right = rng.NextBool();
|
2020-07-01 15:21:45 +00:00
|
|
|
|
float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset)));
|
2019-07-31 09:44:01 +00:00
|
|
|
|
|
|
|
|
|
if (right)
|
|
|
|
|
{
|
|
|
|
|
// Clamp to the right bound
|
2020-07-01 15:21:45 +00:00
|
|
|
|
if (position + rand <= CatchPlayfield.WIDTH)
|
2019-07-31 09:44:01 +00:00
|
|
|
|
position += rand;
|
|
|
|
|
else
|
|
|
|
|
position -= rand;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Clamp to the left bound
|
|
|
|
|
if (position - rand >= 0)
|
|
|
|
|
position -= rand;
|
|
|
|
|
else
|
|
|
|
|
position += rand;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Applies an offset to a position, ensuring that the final position remains within the boundary of the playfield.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="position">The position which the offset should be applied to.</param>
|
|
|
|
|
/// <param name="amount">The amount to offset by.</param>
|
|
|
|
|
private static void applyOffset(ref float position, float amount)
|
|
|
|
|
{
|
|
|
|
|
if (amount > 0)
|
|
|
|
|
{
|
|
|
|
|
// Clamp to the right bound
|
2020-08-20 11:25:40 +00:00
|
|
|
|
if (position + amount < CatchPlayfield.WIDTH)
|
2019-07-31 09:44:01 +00:00
|
|
|
|
position += amount;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Clamp to the left bound
|
|
|
|
|
if (position + amount > 0)
|
|
|
|
|
position += amount;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-11 09:36:37 +00:00
|
|
|
|
private static void initialiseHyperDash(IBeatmap beatmap)
|
2017-11-28 12:22:57 +00:00
|
|
|
|
{
|
2023-02-03 05:07:21 +00:00
|
|
|
|
var palpableObjects = CatchBeatmap.GetPalpableObjects(beatmap.HitObjects)
|
|
|
|
|
.Where(h => h is Fruit || (h is Droplet && h is not TinyDroplet))
|
|
|
|
|
.ToArray();
|
2018-09-13 15:15:46 +00:00
|
|
|
|
|
2021-10-02 03:34:29 +00:00
|
|
|
|
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) / 2;
|
2020-08-20 17:21:16 +00:00
|
|
|
|
|
|
|
|
|
// Todo: This is wrong. osu!stable calculated hyperdashes using the full catcher size, excluding the margins.
|
|
|
|
|
// This should theoretically cause impossible scenarios, but practically, likely due to the size of the playfield, it doesn't seem possible.
|
|
|
|
|
// For now, to bring gameplay (and diffcalc!) completely in-line with stable, this code also uses the full catcher size.
|
|
|
|
|
halfCatcherWidth /= Catcher.ALLOWED_CATCH_RANGE;
|
|
|
|
|
|
2018-09-12 17:48:35 +00:00
|
|
|
|
int lastDirection = 0;
|
|
|
|
|
double lastExcess = halfCatcherWidth;
|
2018-04-13 09:19:50 +00:00
|
|
|
|
|
2023-02-03 05:07:21 +00:00
|
|
|
|
for (int i = 0; i < palpableObjects.Length - 1; i++)
|
2018-09-12 17:48:35 +00:00
|
|
|
|
{
|
2020-11-24 10:57:37 +00:00
|
|
|
|
var currentObject = palpableObjects[i];
|
|
|
|
|
var nextObject = palpableObjects[i + 1];
|
2018-04-13 09:19:50 +00:00
|
|
|
|
|
2020-03-11 09:36:37 +00:00
|
|
|
|
// Reset variables in-case values have changed (e.g. after applying HR)
|
|
|
|
|
currentObject.HyperDashTarget = null;
|
|
|
|
|
currentObject.DistanceToHyperDash = 0;
|
|
|
|
|
|
2020-12-09 08:58:53 +00:00
|
|
|
|
int thisDirection = nextObject.EffectiveX > currentObject.EffectiveX ? 1 : -1;
|
2023-12-06 05:50:03 +00:00
|
|
|
|
|
|
|
|
|
// Int truncation added to match osu!stable.
|
2023-12-04 05:32:14 +00:00
|
|
|
|
double timeToNext = (int)nextObject.StartTime - (int)currentObject.StartTime - 1000f / 60f / 4; // 1/4th of a frame of grace time, taken from osu-stable
|
2020-12-09 08:58:53 +00:00
|
|
|
|
double distanceToNext = Math.Abs(nextObject.EffectiveX - currentObject.EffectiveX) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth);
|
2021-10-26 11:09:48 +00:00
|
|
|
|
float distanceToHyper = (float)(timeToNext * Catcher.BASE_DASH_SPEED - distanceToNext);
|
2019-04-01 03:16:05 +00:00
|
|
|
|
|
2018-09-12 17:48:35 +00:00
|
|
|
|
if (distanceToHyper < 0)
|
2017-12-01 06:08:12 +00:00
|
|
|
|
{
|
2017-12-01 10:24:48 +00:00
|
|
|
|
currentObject.HyperDashTarget = nextObject;
|
2017-12-01 11:39:58 +00:00
|
|
|
|
lastExcess = halfCatcherWidth;
|
2017-12-01 06:08:12 +00:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2018-09-12 17:48:35 +00:00
|
|
|
|
currentObject.DistanceToHyperDash = distanceToHyper;
|
2019-11-20 12:19:49 +00:00
|
|
|
|
lastExcess = Math.Clamp(distanceToHyper, 0, halfCatcherWidth);
|
2017-12-01 06:08:12 +00:00
|
|
|
|
}
|
2018-04-13 09:19:50 +00:00
|
|
|
|
|
2017-12-01 06:08:12 +00:00
|
|
|
|
lastDirection = thisDirection;
|
|
|
|
|
}
|
2017-11-28 12:22:57 +00:00
|
|
|
|
}
|
2017-09-15 11:54:34 +00:00
|
|
|
|
}
|
|
|
|
|
}
|