2021-04-25 06:39:36 +08: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.
using System ;
2021-06-05 23:04:58 +08:00
using System.Collections.Generic ;
2021-05-15 08:07:24 +08:00
using System.Linq ;
2021-07-01 10:06:14 +08:00
using osu.Framework.Graphics.Primitives ;
2021-04-28 04:19:04 +08:00
using osu.Framework.Utils ;
2021-04-25 06:39:36 +08:00
using osu.Game.Beatmaps ;
using osu.Game.Rulesets.Mods ;
2021-04-25 07:34:39 +08:00
using osu.Game.Rulesets.Objects ;
2021-05-01 10:01:43 +08:00
using osu.Game.Rulesets.Osu.Beatmaps ;
2021-04-25 06:39:36 +08:00
using osu.Game.Rulesets.Osu.Objects ;
using osu.Game.Rulesets.Osu.UI ;
2021-06-24 13:19:42 +08:00
using osu.Game.Rulesets.Osu.Utils ;
2021-04-25 06:39:36 +08:00
using osuTK ;
namespace osu.Game.Rulesets.Osu.Mods
{
2021-04-25 07:34:39 +08:00
/// <summary>
/// Mod that randomises the positions of the <see cref="HitObject"/>s
/// </summary>
2021-04-28 01:39:58 +08:00
public class OsuModRandom : ModRandom , IApplicableToBeatmap
2021-04-25 06:39:36 +08:00
{
2021-04-25 07:43:32 +08:00
public override string Description = > "It never gets boring!" ;
2021-04-25 07:34:39 +08:00
2021-05-24 13:24:56 +08:00
private static readonly float playfield_diagonal = OsuPlayfield . BASE_SIZE . LengthFast ;
2021-04-25 07:34:39 +08:00
2021-06-29 12:33:40 +08:00
/// <summary>
2021-07-11 22:46:30 +08:00
/// Number of previous hitobjects to be shifted together when another object is being moved.
2021-06-29 12:33:40 +08:00
/// </summary>
2021-07-11 22:46:30 +08:00
private const int preceding_hitobjects_to_shift = 10 ;
2021-06-29 12:33:40 +08:00
2021-05-26 15:37:30 +08:00
private Random rng ;
2021-05-13 00:11:50 +08:00
public void ApplyToBeatmap ( IBeatmap beatmap )
2021-04-25 06:39:36 +08:00
{
2021-05-13 00:11:50 +08:00
if ( ! ( beatmap is OsuBeatmap osuBeatmap ) )
2021-05-01 10:01:43 +08:00
return ;
2021-05-14 13:13:35 +08:00
var hitObjects = osuBeatmap . HitObjects ;
2021-05-14 07:50:11 +08:00
Seed . Value ? ? = RNG . Next ( ) ;
2021-04-28 02:44:36 +08:00
2021-05-26 15:37:30 +08:00
rng = new Random ( ( int ) Seed . Value ) ;
2021-04-26 05:57:01 +08:00
2021-05-26 15:44:44 +08:00
RandomObjectInfo previous = null ;
2021-04-26 05:57:01 +08:00
float rateOfChangeMultiplier = 0 ;
2021-05-14 13:13:35 +08:00
for ( int i = 0 ; i < hitObjects . Count ; i + + )
2021-04-25 06:39:36 +08:00
{
2021-05-24 13:19:10 +08:00
var hitObject = hitObjects [ i ] ;
2021-05-14 13:13:35 +08:00
2021-05-26 15:44:44 +08:00
var current = new RandomObjectInfo ( hitObject ) ;
2021-05-24 13:19:10 +08:00
2021-06-29 12:33:40 +08:00
// rateOfChangeMultiplier only changes every 5 iterations in a combo
// to prevent shaky-line-shaped streams
2021-06-29 14:25:45 +08:00
if ( hitObject . IndexInCurrentCombo % 5 = = 0 )
2021-04-26 05:57:01 +08:00
rateOfChangeMultiplier = ( float ) rng . NextDouble ( ) * 2 - 1 ;
2021-05-24 13:33:07 +08:00
if ( hitObject is Spinner )
2021-05-26 15:36:14 +08:00
{
2021-05-26 15:44:44 +08:00
previous = null ;
2021-05-24 13:33:07 +08:00
continue ;
2021-05-26 15:36:14 +08:00
}
2021-05-24 13:33:07 +08:00
2021-05-26 15:44:44 +08:00
applyRandomisation ( rateOfChangeMultiplier , previous , current ) ;
2021-05-15 05:04:09 +08:00
2021-07-11 22:01:28 +08:00
// Move hit objects back into the playfield if they are outside of it
Vector2 shift = Vector2 . Zero ;
2021-06-29 13:36:30 +08:00
2021-07-13 18:37:02 +08:00
switch ( hitObject )
2021-07-11 22:01:28 +08:00
{
2021-07-13 18:37:02 +08:00
case HitCircle circle :
shift = clampHitCircleToPlayfield ( circle , current ) ;
break ;
case Slider slider :
shift = clampSliderToPlayfield ( slider , current ) ;
break ;
2021-07-11 22:01:28 +08:00
}
2021-05-15 05:04:09 +08:00
2021-07-11 22:01:28 +08:00
if ( shift ! = Vector2 . Zero )
2021-06-29 12:33:40 +08:00
{
2021-07-11 22:01:28 +08:00
var toBeShifted = new List < OsuHitObject > ( ) ;
2021-06-29 12:33:40 +08:00
2021-07-11 22:46:30 +08:00
for ( int j = i - 1 ; j > = i - preceding_hitobjects_to_shift & & j > = 0 ; j - - )
2021-06-29 12:33:40 +08:00
{
2021-07-11 22:01:28 +08:00
// only shift hit circles
if ( ! ( hitObjects [ j ] is HitCircle ) ) break ;
2021-06-29 12:33:40 +08:00
2021-07-11 22:01:28 +08:00
toBeShifted . Add ( hitObjects [ j ] ) ;
2021-06-29 12:33:40 +08:00
}
2021-07-11 22:01:28 +08:00
if ( toBeShifted . Count > 0 )
applyDecreasingShift ( toBeShifted , shift ) ;
2021-06-29 12:33:40 +08:00
}
2021-04-26 05:57:01 +08:00
2021-05-26 15:44:44 +08:00
previous = current ;
2021-04-25 06:39:36 +08:00
}
}
2021-04-26 05:57:01 +08:00
/// <summary>
/// Returns the final position of the hit object
/// </summary>
/// <returns>Final position of the hit object</returns>
2021-05-26 15:44:05 +08:00
private void applyRandomisation ( float rateOfChangeMultiplier , RandomObjectInfo previous , RandomObjectInfo current )
2021-04-26 05:57:01 +08:00
{
2021-05-26 15:44:05 +08:00
if ( previous = = null )
2021-05-24 21:13:31 +08:00
{
var playfieldSize = OsuPlayfield . BASE_SIZE ;
2021-05-26 15:44:05 +08:00
current . AngleRad = ( float ) ( rng . NextDouble ( ) * 2 * Math . PI - Math . PI ) ;
current . PositionRandomised = new Vector2 ( ( float ) rng . NextDouble ( ) * playfieldSize . X , ( float ) rng . NextDouble ( ) * playfieldSize . Y ) ;
2021-05-24 21:13:31 +08:00
return ;
}
2021-05-26 15:44:05 +08:00
float distanceToPrev = Vector2 . Distance ( previous . EndPositionOriginal , current . PositionOriginal ) ;
2021-04-26 05:57:01 +08:00
// The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object)
// is proportional to the distance between the last and the current hit object
// to allow jumps and prevent too sharp turns during streams.
2021-06-29 12:33:40 +08:00
// Allow maximum jump angle when jump distance is more than half of playfield diagonal length
var randomAngleRad = rateOfChangeMultiplier * 2 * Math . PI * Math . Min ( 1f , distanceToPrev / ( playfield_diagonal * 0.5f ) ) ;
2021-04-26 05:57:01 +08:00
2021-05-26 15:44:05 +08:00
current . AngleRad = ( float ) randomAngleRad + previous . AngleRad ;
if ( current . AngleRad < 0 )
current . AngleRad + = 2 * ( float ) Math . PI ;
2021-04-26 05:57:01 +08:00
var posRelativeToPrev = new Vector2 (
2021-05-26 15:44:05 +08:00
distanceToPrev * ( float ) Math . Cos ( current . AngleRad ) ,
distanceToPrev * ( float ) Math . Sin ( current . AngleRad )
2021-04-26 05:57:01 +08:00
) ;
2021-06-25 09:43:14 +08:00
posRelativeToPrev = OsuHitObjectGenerationUtils . RotateAwayFromEdge ( previous . EndPositionRandomised , posRelativeToPrev ) ;
2021-04-26 05:57:01 +08:00
2021-05-26 15:44:05 +08:00
current . AngleRad = ( float ) Math . Atan2 ( posRelativeToPrev . Y , posRelativeToPrev . X ) ;
2021-05-24 13:24:56 +08:00
2021-06-29 13:36:30 +08:00
current . PositionRandomised = previous . EndPositionRandomised + posRelativeToPrev ;
2021-05-15 05:04:09 +08:00
}
2021-07-13 18:37:17 +08:00
/// <summary>
/// Move the randomised position of a hit circle so that it fits inside the playfield.
/// </summary>
/// <returns>The deviation from the original randomised position in order to fit within the playfield.</returns>
private Vector2 clampHitCircleToPlayfield ( HitCircle circle , RandomObjectInfo objectInfo )
{
var previousPosition = objectInfo . PositionRandomised ;
objectInfo . EndPositionRandomised = objectInfo . PositionRandomised = clampToPlayfieldWithPadding (
objectInfo . PositionRandomised ,
( float ) circle . Radius
) ;
circle . Position = objectInfo . PositionRandomised ;
return objectInfo . PositionRandomised - previousPosition ;
}
2021-05-26 03:32:18 +08:00
/// <summary>
/// Moves the <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already.
/// </summary>
2021-07-11 22:01:28 +08:00
/// <returns>The deviation from the original randomised position in order to fit within the playfield.</returns>
private Vector2 clampSliderToPlayfield ( Slider slider , RandomObjectInfo objectInfo )
2021-05-15 05:04:09 +08:00
{
2021-07-11 22:49:23 +08:00
var possibleMovementBounds = calculatePossibleMovementBounds ( slider ) ;
2021-06-04 22:17:54 +08:00
2021-07-11 22:01:28 +08:00
var previousPosition = objectInfo . PositionRandomised ;
2021-06-29 12:33:40 +08:00
2021-07-05 09:16:01 +08:00
// Clamp slider position to the placement area
2021-07-01 10:59:06 +08:00
// If the slider is larger than the playfield, force it to stay at the original position
2021-07-11 22:49:23 +08:00
var newX = possibleMovementBounds . Width < 0
2021-07-11 22:01:28 +08:00
? objectInfo . PositionOriginal . X
2021-07-11 22:49:23 +08:00
: Math . Clamp ( previousPosition . X , possibleMovementBounds . Left , possibleMovementBounds . Right ) ;
2021-07-01 10:59:06 +08:00
2021-07-11 22:49:23 +08:00
var newY = possibleMovementBounds . Height < 0
2021-07-11 22:01:28 +08:00
? objectInfo . PositionOriginal . Y
2021-07-11 22:49:23 +08:00
: Math . Clamp ( previousPosition . Y , possibleMovementBounds . Top , possibleMovementBounds . Bottom ) ;
2021-07-01 10:59:06 +08:00
2021-07-11 22:01:28 +08:00
slider . Position = objectInfo . PositionRandomised = new Vector2 ( newX , newY ) ;
objectInfo . EndPositionRandomised = slider . EndPosition ;
2021-06-04 22:17:54 +08:00
2021-07-11 22:01:28 +08:00
shiftNestedObjects ( slider , objectInfo . PositionRandomised - objectInfo . PositionOriginal ) ;
2021-06-04 22:17:54 +08:00
2021-07-11 22:01:28 +08:00
return objectInfo . PositionRandomised - previousPosition ;
2021-06-29 12:33:40 +08:00
}
/// <summary>
/// Decreasingly shift a list of <see cref="OsuHitObject"/>s by a specified amount.
/// The first item in the list is shifted by the largest amount, while the last item is shifted by the smallest amount.
/// </summary>
/// <param name="hitObjects">The list of hit objects to be shifted.</param>
/// <param name="shift">The amount to be shifted.</param>
private void applyDecreasingShift ( IList < OsuHitObject > hitObjects , Vector2 shift )
{
for ( int i = 0 ; i < hitObjects . Count ; i + + )
{
2021-06-29 13:36:30 +08:00
var hitObject = hitObjects [ i ] ;
2021-06-29 12:56:05 +08:00
// The first object is shifted by a vector slightly smaller than shift
// The last object is shifted by a vector slightly larger than zero
2021-06-29 13:36:30 +08:00
Vector2 position = hitObject . Position + shift * ( ( hitObjects . Count - i ) / ( float ) ( hitObjects . Count + 1 ) ) ;
2021-06-29 12:33:40 +08:00
2021-07-11 22:01:28 +08:00
hitObject . Position = clampToPlayfieldWithPadding ( position , ( float ) hitObject . Radius ) ;
2021-06-29 12:33:40 +08:00
}
2021-06-04 22:17:54 +08:00
}
/// <summary>
2021-07-10 18:13:36 +08:00
/// Calculates a <see cref="RectangleF"/> which contains all of the possible movements of the slider (in relative X/Y coordinates)
/// such that the entire slider is inside the playfield.
2021-06-04 22:17:54 +08:00
/// </summary>
2021-07-05 09:16:01 +08:00
/// <remarks>
/// If the slider is larger than the playfield, the returned <see cref="RectangleF"/> may have negative width/height.
/// </remarks>
2021-07-10 18:13:36 +08:00
private RectangleF calculatePossibleMovementBounds ( Slider slider )
2021-06-04 22:17:54 +08:00
{
2021-06-05 23:04:58 +08:00
var pathPositions = new List < Vector2 > ( ) ;
slider . Path . GetPathToProgress ( pathPositions , 0 , 1 ) ;
2021-07-10 18:13:36 +08:00
float minX = float . PositiveInfinity ;
float maxX = float . NegativeInfinity ;
2021-05-26 03:32:18 +08:00
2021-07-10 18:13:36 +08:00
float minY = float . PositiveInfinity ;
float maxY = float . NegativeInfinity ;
// Compute the bounding box of the slider.
2021-06-05 23:13:08 +08:00
foreach ( var pos in pathPositions )
2021-06-04 22:17:54 +08:00
{
2021-07-10 18:13:36 +08:00
minX = MathF . Min ( minX , pos . X ) ;
maxX = MathF . Max ( maxX , pos . X ) ;
minY = MathF . Min ( minY , pos . Y ) ;
maxY = MathF . Max ( maxY , pos . Y ) ;
2021-06-04 22:17:54 +08:00
}
2021-06-05 23:13:08 +08:00
2021-07-10 18:13:36 +08:00
// Take the circle radius into account.
2021-06-29 13:36:30 +08:00
var radius = ( float ) slider . Radius ;
2021-07-10 18:13:36 +08:00
minX - = radius ;
minY - = radius ;
maxX + = radius ;
maxY + = radius ;
// Given the bounding box of the slider (via min/max X/Y),
// the amount that the slider can move to the left is minX (with the sign flipped, since positive X is to the right),
// and the amount that it can move to the right is WIDTH - maxX.
// Same calculation applies for the Y axis.
float left = - minX ;
float right = OsuPlayfield . BASE_SIZE . X - maxX ;
float top = - minY ;
float bottom = OsuPlayfield . BASE_SIZE . Y - maxY ;
2021-07-01 10:06:14 +08:00
2021-07-10 18:13:36 +08:00
return new RectangleF ( left , top , right - left , bottom - top ) ;
2021-05-26 03:32:18 +08:00
}
/// <summary>
/// Shifts all nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s by the specified shift.
/// </summary>
/// <param name="slider"><see cref="Slider"/> whose nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted</param>
/// <param name="shift">The <see cref="Vector2"/> the <see cref="Slider"/>'s nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted by</param>
private void shiftNestedObjects ( Slider slider , Vector2 shift )
{
foreach ( var hitObject in slider . NestedHitObjects . Where ( o = > o is SliderTick | | o is SliderRepeat ) )
{
if ( ! ( hitObject is OsuHitObject osuHitObject ) )
continue ;
2021-06-04 22:23:03 +08:00
osuHitObject . Position + = shift ;
2021-05-26 03:32:18 +08:00
}
2021-04-26 05:57:01 +08:00
}
2021-07-11 22:01:28 +08:00
/// <summary>
/// Clamp a position to playfield, keeping a specified distance from the edges.
/// </summary>
/// <param name="position">The position to be clamped.</param>
/// <param name="padding">The minimum distance allowed from playfield edges.</param>
/// <returns>The clamped position.</returns>
private Vector2 clampToPlayfieldWithPadding ( Vector2 position , float padding )
{
return new Vector2 (
Math . Clamp ( position . X , padding , OsuPlayfield . BASE_SIZE . X - padding ) ,
Math . Clamp ( position . Y , padding , OsuPlayfield . BASE_SIZE . Y - padding )
) ;
2021-06-29 13:36:30 +08:00
}
2021-05-26 15:36:14 +08:00
private class RandomObjectInfo
2021-05-13 00:11:50 +08:00
{
2021-05-26 15:31:25 +08:00
public float AngleRad { get ; set ; }
2021-05-01 10:01:43 +08:00
2021-05-26 15:31:25 +08:00
public Vector2 PositionOriginal { get ; }
public Vector2 PositionRandomised { get ; set ; }
2021-05-01 10:01:43 +08:00
2021-05-26 15:31:25 +08:00
public Vector2 EndPositionOriginal { get ; }
public Vector2 EndPositionRandomised { get ; set ; }
2021-05-13 00:11:50 +08:00
2021-05-24 13:19:10 +08:00
public RandomObjectInfo ( OsuHitObject hitObject )
2021-05-01 10:01:43 +08:00
{
2021-05-24 13:19:10 +08:00
PositionRandomised = PositionOriginal = hitObject . Position ;
EndPositionRandomised = EndPositionOriginal = hitObject . EndPosition ;
AngleRad = 0 ;
2021-05-01 10:01:43 +08:00
}
2021-05-24 13:19:10 +08:00
}
2021-05-01 10:01:43 +08:00
}
2021-04-25 06:39:36 +08:00
}