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-05-15 08:07:24 +08:00
using System.Linq ;
2021-04-28 02:44:36 +08:00
using osu.Framework.Bindables ;
2021-05-01 10:01:43 +08:00
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
using osu.Framework.Graphics.UserInterface ;
2021-04-28 04:19:04 +08:00
using osu.Framework.Utils ;
2021-04-25 06:39:36 +08:00
using osu.Game.Beatmaps ;
2021-04-28 02:44:36 +08:00
using osu.Game.Configuration ;
2021-05-01 10:01:43 +08:00
using osu.Game.Graphics.UserInterface ;
using osu.Game.Overlays.Settings ;
2021-04-25 06:39:36 +08:00
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 ;
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
public override bool Ranked = > false ;
2021-05-24 13:24:56 +08: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-26 05:57:01 +08:00
// The closer the hit objects draw to the border, the sharper the turn
2021-05-24 13:24:56 +08: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 ;
private static readonly Vector2 playfield_middle = Vector2 . Divide ( OsuPlayfield . BASE_SIZE , 2 ) ;
private static readonly float playfield_diagonal = OsuPlayfield . BASE_SIZE . LengthFast ;
2021-04-25 07:34:39 +08:00
2021-05-14 07:50:11 +08:00
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(OsuModRandomSettingsControl))]
public Bindable < int? > Seed { get ; } = new Bindable < int? >
2021-04-28 02:44:36 +08:00
{
2021-05-13 00:11:50 +08:00
Default = null ,
Value = null
2021-04-28 02:44:36 +08:00
} ;
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-14 07:50:11 +08:00
var rng = new Random ( ( int ) Seed . Value ) ;
2021-04-26 05:57:01 +08:00
2021-05-24 13:19:10 +08:00
var prevObjectInfo = new RandomObjectInfo
2021-05-13 00:11:50 +08:00
{
2021-05-24 13:19:10 +08:00
PositionOriginal = hitObjects [ 0 ] . Position ,
EndPositionOriginal = hitObjects [ 0 ] . EndPosition ,
PositionRandomised = hitObjects [ 0 ] . Position ,
EndPositionRandomised = hitObjects [ 0 ] . EndPosition
2021-05-13 00:11:50 +08:00
} ;
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-24 13:19:10 +08:00
var currentObjectInfo = new RandomObjectInfo ( hitObject ) ;
if ( i = = 0 )
prevObjectInfo = currentObjectInfo ;
2021-04-26 05:57:01 +08:00
2021-05-14 13:13:35 +08:00
// rateOfChangeMultiplier only changes every i iterations to prevent shaky-line-shaped streams
if ( i % 3 = = 0 )
2021-04-26 05:57:01 +08:00
rateOfChangeMultiplier = ( float ) rng . NextDouble ( ) * 2 - 1 ;
2021-05-24 13:19:10 +08:00
var distanceToPrev = Vector2 . Distance ( prevObjectInfo . EndPositionOriginal , currentObjectInfo . PositionOriginal ) ;
2021-05-13 00:11:50 +08:00
2021-05-24 13:33:07 +08:00
if ( hitObject is Spinner )
continue ;
applyRandomisation (
rateOfChangeMultiplier ,
prevObjectInfo ,
distanceToPrev ,
ref currentObjectInfo
) ;
2021-05-15 05:04:09 +08:00
2021-05-24 13:33:07 +08:00
hitObject . Position = currentObjectInfo . 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.
currentObjectInfo . EndPositionRandomised = currentObjectInfo . PositionRandomised ;
2021-05-15 05:04:09 +08:00
2021-05-24 13:33:07 +08:00
switch ( hitObject )
{
2021-05-15 05:04:09 +08:00
case Slider slider :
moveSliderIntoPlayfield ( ref slider , ref currentObjectInfo ) ;
2021-05-15 08:07:24 +08:00
2021-05-24 13:19:10 +08:00
var sliderShift = Vector2 . Subtract ( slider . Position , currentObjectInfo . PositionOriginal ) ;
2021-05-15 08:07:24 +08:00
foreach ( var tick in slider . NestedHitObjects . OfType < SliderTick > ( ) )
tick . Position = Vector2 . Add ( tick . Position , sliderShift ) ;
2021-05-15 05:04:09 +08:00
break ;
2021-04-25 06:39:36 +08:00
}
2021-04-26 05:57:01 +08:00
2021-05-13 00:11:50 +08:00
prevObjectInfo = currentObjectInfo ;
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-24 13:28:07 +08:00
private void applyRandomisation ( float rateOfChangeMultiplier , RandomObjectInfo prevObjectInfo , float distanceToPrev , ref RandomObjectInfo currentObjectInfo )
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-05-24 13:24:56 +08:00
var randomAngleRad = rateOfChangeMultiplier * 2 * Math . PI * distanceToPrev / playfield_diagonal ;
2021-04-26 05:57:01 +08:00
2021-05-13 00:11:50 +08:00
currentObjectInfo . AngleRad = ( float ) randomAngleRad + prevObjectInfo . AngleRad ;
if ( currentObjectInfo . AngleRad < 0 )
currentObjectInfo . AngleRad + = 2 * ( float ) Math . PI ;
2021-04-26 05:57:01 +08:00
var posRelativeToPrev = new Vector2 (
2021-05-13 00:11:50 +08:00
distanceToPrev * ( float ) Math . Cos ( currentObjectInfo . AngleRad ) ,
distanceToPrev * ( float ) Math . Sin ( currentObjectInfo . AngleRad )
2021-04-26 05:57:01 +08:00
) ;
2021-05-24 13:19:10 +08:00
posRelativeToPrev = getRotatedVector ( prevObjectInfo . EndPositionRandomised , posRelativeToPrev ) ;
2021-04-26 05:57:01 +08:00
2021-05-13 00:11:50 +08:00
currentObjectInfo . AngleRad = ( float ) Math . Atan2 ( posRelativeToPrev . Y , posRelativeToPrev . X ) ;
2021-05-24 13:24:56 +08:00
2021-05-24 13:19:10 +08:00
var position = Vector2 . Add ( prevObjectInfo . 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-24 13:19:10 +08:00
currentObjectInfo . PositionRandomised = position ;
2021-05-15 05:04:09 +08:00
}
2021-05-24 13:19:10 +08:00
private void moveSliderIntoPlayfield ( ref Slider slider , ref RandomObjectInfo currentObjectInfo )
2021-05-15 05:04:09 +08:00
{
foreach ( var controlPoint in slider . Path . ControlPoints )
{
// Position of controlPoint relative to slider.Position
var pos = controlPoint . Position . Value ;
var playfieldSize = OsuPlayfield . BASE_SIZE ;
if ( pos . X + slider . Position . X < 0 )
slider . Position = new Vector2 ( - pos . X , slider . Position . Y ) ;
else if ( pos . X + slider . Position . X > playfieldSize . X )
slider . Position = new Vector2 ( playfieldSize . X - pos . X , slider . Position . Y ) ;
if ( pos . Y + slider . Position . Y < 0 )
slider . Position = new Vector2 ( slider . Position . X , - pos . Y ) ;
else if ( pos . Y + slider . Position . Y > playfieldSize . Y )
slider . Position = new Vector2 ( slider . Position . X , playfieldSize . Y - pos . Y ) ;
}
2021-05-24 13:19:10 +08:00
currentObjectInfo . EndPositionRandomised = slider . TailCircle . Position ;
2021-04-26 05:57:01 +08: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 13:24:56 +08:00
if ( prevPosChanged . X < playfield_middle . X )
2021-04-26 05:57:01 +08: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 13:24:56 +08:00
if ( prevPosChanged . Y < playfield_middle . Y )
2021-04-26 05:57:01 +08: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 13:24:56 +08:00
return rotateVectorTowardsVector ( posRelativeToPrev , playfield_middle - prevPosChanged , relativeRotationDistance / 2 ) ;
2021-04-26 05:57:01 +08: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 13:33:07 +08:00
while ( diff < - Math . PI ) diff + = 2 * Math . PI ;
2021-04-26 05:57:01 +08:00
2021-05-24 13:33:07 +08:00
while ( diff > Math . PI ) diff - = 2 * Math . PI ;
2021-04-26 05:57:01 +08:00
2021-05-13 00:11:50 +08:00
var finalAngleRad = initialAngleRad + relativeDistance * diff ;
2021-04-26 05:57:01 +08:00
return new Vector2 (
2021-05-13 00:11:50 +08:00
initial . Length * ( float ) Math . Cos ( finalAngleRad ) ,
initial . Length * ( float ) Math . Sin ( finalAngleRad )
2021-04-26 05:57:01 +08:00
) ;
}
2021-05-13 00:11:50 +08:00
2021-05-24 13:19:10 +08:00
private struct RandomObjectInfo
2021-05-13 00:11:50 +08:00
{
internal float AngleRad { get ; set ; }
2021-05-01 10:01:43 +08:00
2021-05-24 13:19:10 +08:00
internal Vector2 PositionOriginal { get ; set ; }
internal Vector2 PositionRandomised { get ; set ; }
2021-05-01 10:01:43 +08:00
2021-05-24 13:19:10 +08:00
internal Vector2 EndPositionOriginal { get ; set ; }
internal 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-05-24 13:19:10 +08:00
public class OsuModRandomSettingsControl : SettingsItem < int? >
{
protected override Drawable CreateControl ( ) = > new SeedControl
{
RelativeSizeAxes = Axes . X ,
Margin = new MarginPadding { Top = 5 }
} ;
2021-05-01 10:01:43 +08:00
2021-05-24 13:19:10 +08:00
private sealed class SeedControl : CompositeDrawable , IHasCurrentValue < int? >
2021-05-01 10:01:43 +08:00
{
2021-05-24 13:19:10 +08:00
private readonly BindableWithCurrent < int? > current = new BindableWithCurrent < int? > ( ) ;
2021-05-01 10:01:43 +08:00
2021-05-24 13:19:10 +08:00
public Bindable < int? > Current
2021-05-01 10:01:43 +08:00
{
2021-05-24 13:19:10 +08:00
get = > current ;
set
2021-05-01 10:01:43 +08:00
{
2021-05-24 13:19:10 +08:00
current . Current = value ;
seedNumberBox . Text = value . Value . ToString ( ) ;
}
}
private readonly OsuNumberBox seedNumberBox ;
public SeedControl ( )
{
AutoSizeAxes = Axes . Y ;
InternalChildren = new [ ]
{
new GridContainer
2021-05-01 10:01:43 +08:00
{
2021-05-24 13:19:10 +08:00
RelativeSizeAxes = Axes . X ,
AutoSizeAxes = Axes . Y ,
ColumnDimensions = new [ ]
{
new Dimension ( ) ,
new Dimension ( GridSizeMode . Absolute , 2 ) ,
new Dimension ( GridSizeMode . Relative , 0.25f )
} ,
RowDimensions = new [ ]
{
new Dimension ( GridSizeMode . AutoSize )
} ,
Content = new [ ]
2021-05-01 10:01:43 +08:00
{
2021-05-24 13:19:10 +08:00
new Drawable [ ]
2021-05-01 10:01:43 +08:00
{
2021-05-24 13:19:10 +08:00
seedNumberBox = new OsuNumberBox
{
RelativeSizeAxes = Axes . X ,
CommitOnFocusLost = true
}
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-05-24 13:19:10 +08:00
seedNumberBox . Current . BindValueChanged ( e = >
{
int? value = null ;
2021-05-01 10:01:43 +08:00
2021-05-24 13:19:10 +08:00
if ( int . TryParse ( e . NewValue , out var intVal ) )
value = intVal ;
2021-05-01 10:01:43 +08:00
2021-05-24 13:19:10 +08:00
current . Value = value ;
} ) ;
}
2021-05-01 10:01:43 +08:00
2021-05-24 13:19:10 +08:00
protected override void Update ( )
{
if ( current . Value = = null )
seedNumberBox . Text = current . Current . Value . ToString ( ) ;
}
2021-05-01 10:01:43 +08:00
}
}
}
2021-04-25 06:39:36 +08:00
}