2021-04-24 22:39:36 +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.
using System ;
2021-05-15 00:07:24 +00:00
using System.Linq ;
2021-05-01 02:01:43 +00:00
using osu.Framework.Graphics ;
2021-04-27 20:19:04 +00:00
using osu.Framework.Utils ;
2021-04-24 22:39:36 +00:00
using osu.Game.Beatmaps ;
using osu.Game.Rulesets.Mods ;
2021-04-24 23:34:39 +00:00
using osu.Game.Rulesets.Objects ;
2021-05-01 02:01:43 +00:00
using osu.Game.Rulesets.Osu.Beatmaps ;
2021-04-24 22:39:36 +00:00
using osu.Game.Rulesets.Osu.Objects ;
using osu.Game.Rulesets.Osu.UI ;
using osuTK ;
namespace osu.Game.Rulesets.Osu.Mods
{
2021-04-24 23:34:39 +00:00
/// <summary>
/// Mod that randomises the positions of the <see cref="HitObject"/>s
/// </summary>
2021-04-27 17:39:58 +00:00
public class OsuModRandom : ModRandom , IApplicableToBeatmap
2021-04-24 22:39:36 +00:00
{
2021-04-24 23:43:32 +00:00
public override string Description = > "It never gets boring!" ;
2021-04-24 23:34:39 +00:00
public override bool Ranked = > false ;
2021-06-04 14:26:40 +00:00
// How often per second getMinSliderMargin() checks if the slider is outside of the playfield
2021-06-04 14:17:54 +00:00
private const float slider_path_checking_rate = 10 ;
2021-05-24 05:24:56 +00:00
// The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
2021-04-25 21:57:01 +00:00
// The closer the hit objects draw to the border, the sharper the turn
2021-05-24 05:24:56 +00:00
private const float playfield_edge_ratio = 0.375f ;
private static readonly float border_distance_x = OsuPlayfield . BASE_SIZE . X * playfield_edge_ratio ;
private static readonly float border_distance_y = OsuPlayfield . BASE_SIZE . Y * playfield_edge_ratio ;
2021-06-04 14:23:03 +00:00
private static readonly Vector2 playfield_middle = OsuPlayfield . BASE_SIZE / 2 ;
2021-05-24 05:24:56 +00:00
private static readonly float playfield_diagonal = OsuPlayfield . BASE_SIZE . LengthFast ;
2021-04-24 23:34:39 +00:00
2021-05-26 07:37:30 +00:00
private Random rng ;
2021-05-12 16:11:50 +00:00
public void ApplyToBeatmap ( IBeatmap beatmap )
2021-04-24 22:39:36 +00:00
{
2021-05-12 16:11:50 +00:00
if ( ! ( beatmap is OsuBeatmap osuBeatmap ) )
2021-05-01 02:01:43 +00:00
return ;
2021-05-14 05:13:35 +00:00
var hitObjects = osuBeatmap . HitObjects ;
2021-05-13 23:50:11 +00:00
Seed . Value ? ? = RNG . Next ( ) ;
2021-04-27 18:44:36 +00:00
2021-05-26 07:37:30 +00:00
rng = new Random ( ( int ) Seed . Value ) ;
2021-04-25 21:57:01 +00:00
2021-05-26 07:44:44 +00:00
RandomObjectInfo previous = null ;
2021-04-25 21:57:01 +00:00
float rateOfChangeMultiplier = 0 ;
2021-05-14 05:13:35 +00:00
for ( int i = 0 ; i < hitObjects . Count ; i + + )
2021-04-24 22:39:36 +00:00
{
2021-05-24 05:19:10 +00:00
var hitObject = hitObjects [ i ] ;
2021-05-14 05:13:35 +00:00
2021-05-26 07:44:44 +00:00
var current = new RandomObjectInfo ( hitObject ) ;
2021-05-24 05:19:10 +00:00
2021-05-14 05:13:35 +00:00
// rateOfChangeMultiplier only changes every i iterations to prevent shaky-line-shaped streams
if ( i % 3 = = 0 )
2021-04-25 21:57:01 +00:00
rateOfChangeMultiplier = ( float ) rng . NextDouble ( ) * 2 - 1 ;
2021-05-24 05:33:07 +00:00
if ( hitObject is Spinner )
2021-05-26 07:36:14 +00:00
{
2021-05-26 07:44:44 +00:00
previous = null ;
2021-05-24 05:33:07 +00:00
continue ;
2021-05-26 07:36:14 +00:00
}
2021-05-24 05:33:07 +00:00
2021-05-26 07:44:44 +00:00
applyRandomisation ( rateOfChangeMultiplier , previous , current ) ;
2021-05-14 21:04:09 +00:00
2021-05-26 07:44:44 +00:00
hitObject . Position = current . PositionRandomised ;
2021-05-15 00:07:24 +00:00
2021-05-24 05:33:07 +00:00
// update end position as it may have changed as a result of the position update.
2021-05-26 07:44:44 +00:00
current . EndPositionRandomised = current . PositionRandomised ;
2021-05-14 21:04:09 +00:00
2021-06-04 14:17:54 +00:00
if ( hitObject is Slider slider )
moveSliderIntoPlayfield ( slider , current ) ;
2021-04-25 21:57:01 +00:00
2021-05-26 07:44:44 +00:00
previous = current ;
2021-04-24 22:39:36 +00:00
}
}
2021-04-25 21:57:01 +00:00
/// <summary>
/// Returns the final position of the hit object
/// </summary>
/// <returns>Final position of the hit object</returns>
2021-05-26 07:44:05 +00:00
private void applyRandomisation ( float rateOfChangeMultiplier , RandomObjectInfo previous , RandomObjectInfo current )
2021-04-25 21:57:01 +00:00
{
2021-05-26 07:44:05 +00:00
if ( previous = = null )
2021-05-24 13:13:31 +00:00
{
var playfieldSize = OsuPlayfield . BASE_SIZE ;
2021-05-26 07:44:05 +00: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 13:13:31 +00:00
return ;
}
2021-05-26 07:44:05 +00:00
float distanceToPrev = Vector2 . Distance ( previous . EndPositionOriginal , current . PositionOriginal ) ;
2021-04-25 21:57:01 +00: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-05-24 05:24:56 +00:00
var randomAngleRad = rateOfChangeMultiplier * 2 * Math . PI * distanceToPrev / playfield_diagonal ;
2021-04-25 21:57:01 +00:00
2021-05-26 07:44:05 +00:00
current . AngleRad = ( float ) randomAngleRad + previous . AngleRad ;
if ( current . AngleRad < 0 )
current . AngleRad + = 2 * ( float ) Math . PI ;
2021-04-25 21:57:01 +00:00
var posRelativeToPrev = new Vector2 (
2021-05-26 07:44:05 +00:00
distanceToPrev * ( float ) Math . Cos ( current . AngleRad ) ,
distanceToPrev * ( float ) Math . Sin ( current . AngleRad )
2021-04-25 21:57:01 +00:00
) ;
2021-05-26 07:44:05 +00:00
posRelativeToPrev = getRotatedVector ( previous . EndPositionRandomised , posRelativeToPrev ) ;
2021-04-25 21:57:01 +00:00
2021-05-26 07:44:05 +00:00
current . AngleRad = ( float ) Math . Atan2 ( posRelativeToPrev . Y , posRelativeToPrev . X ) ;
2021-05-24 05:24:56 +00:00
2021-06-04 14:23:03 +00:00
var position = previous . EndPositionRandomised + posRelativeToPrev ;
2021-04-25 21:57:01 +00: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 05:28:07 +00: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-25 21:57:01 +00:00
2021-05-26 07:44:05 +00:00
current . PositionRandomised = position ;
2021-05-14 21:04:09 +00:00
}
2021-05-25 19:32:18 +00: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-05-26 07:36:14 +00:00
private void moveSliderIntoPlayfield ( Slider slider , RandomObjectInfo currentObjectInfo )
2021-05-14 21:04:09 +00:00
{
2021-06-04 14:17:54 +00:00
var minMargin = getMinSliderMargin ( slider ) ;
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 14:23:03 +00:00
shiftNestedObjects ( slider , currentObjectInfo . PositionRandomised - currentObjectInfo . PositionOriginal ) ;
2021-06-04 14:17:54 +00: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-05-25 19:42:26 +00:00
var minMargin = new MarginPadding ( ) ;
2021-06-04 14:17:54 +00:00
Vector2 pos ;
2021-05-14 21:04:09 +00:00
2021-06-04 15:22:36 +00:00
for ( double i = 0 ; i < = 1 ; i + = 1 / ( slider_path_checking_rate / 1000 * ( slider . EndTime - slider . StartTime ) ) )
2021-05-25 19:32:18 +00:00
{
2021-06-04 15:22:36 +00:00
pos = slider . Path . PositionAt ( i ) ;
2021-06-04 14:17:54 +00:00
updateMargin ( ) ;
}
2021-05-25 19:32:18 +00:00
2021-06-04 14:17:54 +00:00
var repeat = ( SliderRepeat ) slider . NestedHitObjects . FirstOrDefault ( o = > o is SliderRepeat ) ;
2021-05-14 21:04:09 +00:00
2021-06-04 14:17:54 +00:00
if ( repeat ! = null )
{
pos = repeat . Position - slider . Position ;
updateMargin ( ) ;
2021-05-14 21:04:09 +00:00
}
2021-06-04 14:17:54 +00:00
pos = slider . Path . PositionAt ( 1 ) ;
updateMargin ( ) ;
2021-05-25 19:32:18 +00:00
2021-06-04 14:50:27 +00: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 ) ;
2021-06-04 14:17:54 +00:00
return minMargin ;
2021-05-25 19:32:18 +00:00
2021-06-04 14:17:54 +00:00
void updateMargin ( )
{
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-05-25 19:32:18 +00: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 14:23:03 +00:00
osuHitObject . Position + = shift ;
2021-05-25 19:32:18 +00:00
}
2021-04-25 21:57:01 +00:00
}
/// <summary>
/// Determines the position of the current hit object relative to the previous one.
/// </summary>
/// <returns>The position of the current hit object relative to the previous one</returns>
private Vector2 getRotatedVector ( Vector2 prevPosChanged , Vector2 posRelativeToPrev )
{
var relativeRotationDistance = 0f ;
2021-05-24 05:24:56 +00:00
if ( prevPosChanged . X < playfield_middle . X )
2021-04-25 21:57:01 +00:00
{
relativeRotationDistance = Math . Max (
( border_distance_x - prevPosChanged . X ) / border_distance_x ,
relativeRotationDistance
) ;
}
else
{
relativeRotationDistance = Math . Max (
( prevPosChanged . X - ( OsuPlayfield . BASE_SIZE . X - border_distance_x ) ) / border_distance_x ,
relativeRotationDistance
) ;
}
2021-05-24 05:24:56 +00:00
if ( prevPosChanged . Y < playfield_middle . Y )
2021-04-25 21:57:01 +00:00
{
relativeRotationDistance = Math . Max (
( border_distance_y - prevPosChanged . Y ) / border_distance_y ,
relativeRotationDistance
) ;
}
else
{
relativeRotationDistance = Math . Max (
( prevPosChanged . Y - ( OsuPlayfield . BASE_SIZE . Y - border_distance_y ) ) / border_distance_y ,
relativeRotationDistance
) ;
}
2021-05-24 05:24:56 +00:00
return rotateVectorTowardsVector ( posRelativeToPrev , playfield_middle - prevPosChanged , relativeRotationDistance / 2 ) ;
2021-04-25 21:57:01 +00:00
}
/// <summary>
/// Rotates vector "initial" towards vector "destinantion"
/// </summary>
/// <param name="initial">Vector to rotate to "destination"</param>
/// <param name="destination">Vector "initial" should be rotated to</param>
/// <param name="relativeDistance">The angle the vector should be rotated relative to the difference between the angles of the the two vectors.</param>
/// <returns>Resulting vector</returns>
private Vector2 rotateVectorTowardsVector ( Vector2 initial , Vector2 destination , float relativeDistance )
{
var initialAngleRad = Math . Atan2 ( initial . Y , initial . X ) ;
var destAngleRad = Math . Atan2 ( destination . Y , destination . X ) ;
var diff = destAngleRad - initialAngleRad ;
2021-05-24 05:33:07 +00:00
while ( diff < - Math . PI ) diff + = 2 * Math . PI ;
2021-04-25 21:57:01 +00:00
2021-05-24 05:33:07 +00:00
while ( diff > Math . PI ) diff - = 2 * Math . PI ;
2021-04-25 21:57:01 +00:00
2021-05-12 16:11:50 +00:00
var finalAngleRad = initialAngleRad + relativeDistance * diff ;
2021-04-25 21:57:01 +00:00
return new Vector2 (
2021-05-12 16:11:50 +00:00
initial . Length * ( float ) Math . Cos ( finalAngleRad ) ,
initial . Length * ( float ) Math . Sin ( finalAngleRad )
2021-04-25 21:57:01 +00:00
) ;
}
2021-05-12 16:11:50 +00:00
2021-05-26 07:36:14 +00:00
private class RandomObjectInfo
2021-05-12 16:11:50 +00:00
{
2021-05-26 07:31:25 +00:00
public float AngleRad { get ; set ; }
2021-05-01 02:01:43 +00:00
2021-05-26 07:31:25 +00:00
public Vector2 PositionOriginal { get ; }
public Vector2 PositionRandomised { get ; set ; }
2021-05-01 02:01:43 +00:00
2021-05-26 07:31:25 +00:00
public Vector2 EndPositionOriginal { get ; }
public Vector2 EndPositionRandomised { get ; set ; }
2021-05-12 16:11:50 +00:00
2021-05-24 05:19:10 +00:00
public RandomObjectInfo ( OsuHitObject hitObject )
2021-05-01 02:01:43 +00:00
{
2021-05-24 05:19:10 +00:00
PositionRandomised = PositionOriginal = hitObject . Position ;
EndPositionRandomised = EndPositionOriginal = hitObject . EndPosition ;
AngleRad = 0 ;
2021-05-01 02:01:43 +00:00
}
2021-05-24 05:19:10 +00:00
}
2021-05-01 02:01:43 +00:00
}
2021-04-24 22:39:36 +00:00
}