1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-14 21:17:33 +08:00
osu-lazer/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs

254 lines
10 KiB
C#
Raw Normal View History

// 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 17:19:50 +08:00
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.MathUtils;
2018-04-13 17:19:50 +08:00
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
2019-07-31 17:44:01 +08:00
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Types;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Rulesets.Catch.Beatmaps
{
public class CatchBeatmapProcessor : BeatmapProcessor
2018-04-13 17:19:50 +08:00
{
public const int RNG_SEED = 1337;
public CatchBeatmapProcessor(IBeatmap beatmap)
: base(beatmap)
2018-04-13 17:19:50 +08:00
{
}
public override void PostProcess()
{
base.PostProcess();
2018-04-13 17:19:50 +08:00
2019-07-31 17:44:01 +08:00
ApplyPositionOffsets(Beatmap);
2018-04-13 17:19:50 +08:00
int index = 0;
2019-04-01 11:16:05 +08:00
foreach (var obj in Beatmap.HitObjects.OfType<CatchHitObject>())
{
obj.IndexInBeatmap = index;
foreach (var nested in obj.NestedHitObjects.OfType<CatchHitObject>())
nested.IndexInBeatmap = index;
if (obj.LastInCombo && obj.NestedHitObjects.LastOrDefault() is IHasComboInformation lastNested)
lastNested.LastInCombo = true;
index++;
}
2018-04-13 17:19:50 +08:00
}
2019-07-31 17:44:01 +08:00
public static void ApplyPositionOffsets(IBeatmap beatmap, params Mod[] mods)
{
var rng = new FastRandom(RNG_SEED);
2019-07-31 17:44:01 +08:00
bool shouldApplyHardRockOffset = mods.Any(m => m is ModHardRock);
float? lastPosition = null;
double lastStartTime = 0;
foreach (var obj in beatmap.HitObjects.OfType<CatchHitObject>())
{
obj.XOffset = 0;
switch (obj)
{
2019-07-31 17:44:01 +08:00
case Fruit fruit:
if (shouldApplyHardRockOffset)
applyHardRockOffset(fruit, ref lastPosition, ref lastStartTime, rng);
break;
case BananaShower bananaShower:
2018-06-29 14:01:33 +08:00
foreach (var banana in bananaShower.NestedHitObjects.OfType<Banana>())
{
banana.XOffset = (float)(rng.NextDouble() * CatchPlayfield.WIDTH);
rng.Next(); // osu!stable retrieved a random banana type
rng.Next(); // osu!stable retrieved a random banana rotation
2018-06-13 20:10:54 +08:00
rng.Next(); // osu!stable retrieved a random banana colour
}
2019-02-28 12:31:40 +08:00
break;
2019-04-01 11:16:05 +08:00
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.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;
foreach (var nested in juiceStream.NestedHitObjects)
{
2019-08-01 13:57:17 +08:00
var catchObject = (CatchHitObject)nested;
catchObject.XOffset = 0;
if (catchObject is TinyDroplet)
catchObject.XOffset = Math.Clamp(rng.Next(-20, 20), -catchObject.OriginalX, CatchPlayfield.WIDTH - catchObject.OriginalX);
2019-08-01 13:57:17 +08:00
else if (catchObject is Droplet)
2018-06-13 18:52:04 +08:00
rng.Next(); // osu!stable retrieved a random droplet rotation
}
2019-02-28 12:31:40 +08:00
break;
}
}
initialiseHyperDash(beatmap);
}
private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, FastRandom rng)
2019-07-31 17:44:01 +08:00
{
float offsetPosition = hitObject.OriginalX;
2019-07-31 17:44:01 +08:00
double startTime = hitObject.StartTime;
if (lastPosition == null)
{
lastPosition = offsetPosition;
2019-07-31 17:44:01 +08:00
lastStartTime = startTime;
return;
}
float positionDiff = offsetPosition - lastPosition.Value;
// 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 17:44:01 +08:00
if (timeDiff > 1000)
{
lastPosition = offsetPosition;
2019-07-31 17:44:01 +08:00
lastStartTime = startTime;
return;
}
if (positionDiff == 0)
{
applyRandomOffset(ref offsetPosition, timeDiff / 4d, rng);
hitObject.XOffset = offsetPosition - hitObject.OriginalX;
2019-07-31 17:44:01 +08:00
return;
}
// ReSharper disable once PossibleLossOfFraction
if (Math.Abs(positionDiff) < timeDiff / 3)
applyOffset(ref offsetPosition, positionDiff);
2019-07-31 17:44:01 +08:00
hitObject.XOffset = offsetPosition - hitObject.OriginalX;
2019-07-31 17:44:01 +08:00
lastPosition = offsetPosition;
2019-07-31 17:44:01 +08: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>
private static void applyRandomOffset(ref float position, double maxOffset, FastRandom rng)
{
bool right = rng.NextBool();
float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset)));
2019-07-31 17:44:01 +08:00
if (right)
{
// Clamp to the right bound
if (position + rand <= CatchPlayfield.WIDTH)
2019-07-31 17:44:01 +08: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
if (position + amount < CatchPlayfield.WIDTH)
2019-07-31 17:44:01 +08:00
position += amount;
}
else
{
// Clamp to the left bound
if (position + amount > 0)
position += amount;
}
}
private static void initialiseHyperDash(IBeatmap beatmap)
2018-04-13 17:19:50 +08:00
{
List<PalpableCatchHitObject> palpableObjects = new List<PalpableCatchHitObject>();
2018-04-13 17:19:50 +08:00
foreach (var currentObject in beatmap.HitObjects)
2018-04-13 17:19:50 +08:00
{
if (currentObject is Fruit fruitObject)
palpableObjects.Add(fruitObject);
2019-11-11 20:05:36 +08:00
if (currentObject is JuiceStream)
2019-11-11 19:53:22 +08:00
{
foreach (var juice in currentObject.NestedHitObjects)
2019-11-11 19:53:22 +08:00
{
if (juice is PalpableCatchHitObject palpableObject && !(juice is TinyDroplet))
palpableObjects.Add(palpableObject);
2019-11-11 19:53:22 +08:00
}
}
}
2018-04-13 17:19:50 +08:00
palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2;
// 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;
int lastDirection = 0;
double lastExcess = halfCatcherWidth;
2018-04-13 17:19:50 +08:00
for (int i = 0; i < palpableObjects.Count - 1; i++)
{
var currentObject = palpableObjects[i];
var nextObject = palpableObjects[i + 1];
2018-04-13 17:19:50 +08:00
// Reset variables in-case values have changed (e.g. after applying HR)
currentObject.HyperDashTarget = null;
currentObject.DistanceToHyperDash = 0;
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.EffectiveX - currentObject.EffectiveX) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth);
float distanceToHyper = (float)(timeToNext * Catcher.BASE_SPEED - distanceToNext);
2019-04-01 11:16:05 +08:00
if (distanceToHyper < 0)
2018-04-13 17:19:50 +08:00
{
currentObject.HyperDashTarget = nextObject;
lastExcess = halfCatcherWidth;
}
else
{
currentObject.DistanceToHyperDash = distanceToHyper;
lastExcess = Math.Clamp(distanceToHyper, 0, halfCatcherWidth);
2018-04-13 17:19:50 +08:00
}
lastDirection = thisDirection;
}
}
}
}