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-05-01 10:01:43 +08:00
using osu.Framework.Graphics ;
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>
/// Number of previous hit circles to be shifted together when a slider needs to be moved.
/// </summary>
private const int shift_object_count = 10 ;
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-06-29 12:33:40 +08:00
int cntSinceNewCombo = 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
if ( hitObject . NewCombo )
cntSinceNewCombo = 0 ;
else
cntSinceNewCombo + + ;
if ( cntSinceNewCombo % 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-05-26 15:44:44 +08:00
hitObject . Position = current . PositionRandomised ;
2021-05-15 08:07:24 +08:00
2021-05-24 13:33:07 +08:00
// update end position as it may have changed as a result of the position update.
2021-05-26 15:44:44 +08:00
current . EndPositionRandomised = current . PositionRandomised ;
2021-05-15 05:04:09 +08:00
2021-06-04 22:17:54 +08:00
if ( hitObject is Slider slider )
2021-06-29 12:33:40 +08:00
{
Vector2 shift = moveSliderIntoPlayfield ( slider , current ) ;
if ( shift ! = Vector2 . Zero )
{
var toBeShifted = new List < OsuHitObject > ( ) ;
for ( int j = i - 1 ; j > = i - shift_object_count & & j > = 0 ; j - - )
{
2021-06-29 12:56:05 +08:00
// only shift hit circles
2021-06-29 12:33:40 +08:00
if ( ! ( hitObjects [ j ] is HitCircle ) ) break ;
toBeShifted . Add ( hitObjects [ j ] ) ;
}
if ( toBeShifted . Count > 0 )
applyDecreasingShift ( toBeShifted , shift ) ;
}
}
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-04 22:23:03 +08:00
var position = previous . EndPositionRandomised + posRelativeToPrev ;
2021-04-26 05:57:01 +08:00
// Move hit objects back into the playfield if they are outside of it,
// which would sometimes happen during big jumps otherwise.
2021-05-24 13:28:07 +08:00
position . X = MathHelper . Clamp ( position . X , 0 , OsuPlayfield . BASE_SIZE . X ) ;
position . Y = MathHelper . Clamp ( position . Y , 0 , OsuPlayfield . BASE_SIZE . Y ) ;
2021-04-26 05:57:01 +08:00
2021-05-26 15:44:05 +08:00
current . PositionRandomised = position ;
2021-05-15 05:04:09 +08:00
}
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-06-29 12:33:40 +08:00
/// <returns>The <see cref="Vector2"/> that this slider has been shifted by.</returns>
private Vector2 moveSliderIntoPlayfield ( Slider slider , RandomObjectInfo currentObjectInfo )
2021-05-15 05:04:09 +08:00
{
2021-06-04 22:17:54 +08:00
var minMargin = getMinSliderMargin ( slider ) ;
2021-06-29 12:33:40 +08:00
var prevPosition = slider . Position ;
2021-06-04 22:17:54 +08:00
slider . Position = new Vector2 (
Math . Clamp ( slider . Position . X , minMargin . Left , OsuPlayfield . BASE_SIZE . X - minMargin . Right ) ,
Math . Clamp ( slider . Position . Y , minMargin . Top , OsuPlayfield . BASE_SIZE . Y - minMargin . Bottom )
) ;
currentObjectInfo . PositionRandomised = slider . Position ;
currentObjectInfo . EndPositionRandomised = slider . EndPosition ;
2021-06-04 22:23:03 +08:00
shiftNestedObjects ( slider , currentObjectInfo . PositionRandomised - currentObjectInfo . PositionOriginal ) ;
2021-06-29 12:33:40 +08:00
return slider . Position - prevPosition ;
}
/// <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 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 12:33:40 +08:00
Vector2 position = hitObjects [ i ] . Position + shift * ( ( hitObjects . Count - i ) / ( float ) ( hitObjects . Count + 1 ) ) ;
position . X = MathHelper . Clamp ( position . X , 0 , OsuPlayfield . BASE_SIZE . X ) ;
position . Y = MathHelper . Clamp ( position . Y , 0 , OsuPlayfield . BASE_SIZE . Y ) ;
hitObjects [ i ] . Position = position ;
}
2021-06-04 22:17:54 +08:00
}
/// <summary>
/// Calculates the min. distances from the <see cref="Slider"/>'s position to the playfield border for the slider to be fully inside of the playfield.
/// </summary>
private MarginPadding getMinSliderMargin ( Slider slider )
{
2021-06-05 23:04:58 +08:00
var pathPositions = new List < Vector2 > ( ) ;
slider . Path . GetPathToProgress ( pathPositions , 0 , 1 ) ;
2021-06-05 23:13:08 +08:00
var minMargin = new MarginPadding ( ) ;
2021-05-26 03:32:18 +08:00
2021-06-05 23:13:08 +08:00
foreach ( var pos in pathPositions )
2021-06-04 22:17:54 +08:00
{
minMargin . Left = Math . Max ( minMargin . Left , - pos . X ) ;
minMargin . Right = Math . Max ( minMargin . Right , pos . X ) ;
minMargin . Top = Math . Max ( minMargin . Top , - pos . Y ) ;
minMargin . Bottom = Math . Max ( minMargin . Bottom , pos . Y ) ;
}
2021-06-05 23:13:08 +08:00
minMargin . Left = Math . Min ( minMargin . Left , OsuPlayfield . BASE_SIZE . X - minMargin . Right ) ;
minMargin . Top = Math . Min ( minMargin . Top , OsuPlayfield . BASE_SIZE . Y - minMargin . Bottom ) ;
return minMargin ;
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-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
}