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-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-04-26 05:57:01 +08:00
// The distances from the hit objects to the borders of the playfield they start to "turn around" and curve towards the middle.
// The closer the hit objects draw to the border, the sharper the turn
2021-05-01 10:01:43 +08:00
private const byte border_distance_x = 192 ;
private const byte border_distance_y = 144 ;
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 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-13 00:11:50 +08:00
var prevObjectInfo = new HitObjectInfo
{
AngleRad = 0 ,
PosUnchanged = osuBeatmap . HitObjects [ 0 ] . Position ,
PosChanged = osuBeatmap . HitObjects [ 0 ] . Position
} ;
2021-04-26 05:57:01 +08:00
// rateOfChangeMultiplier changes every i iterations to prevent shaky-line-shaped streams
2021-05-01 10:01:43 +08:00
byte i = 3 ;
2021-04-26 05:57:01 +08:00
float rateOfChangeMultiplier = 0 ;
2021-05-13 00:11:50 +08:00
foreach ( var currentHitObject in osuBeatmap . HitObjects )
2021-04-25 06:39:36 +08:00
{
2021-05-13 00:11:50 +08:00
var currentObjectInfo = new HitObjectInfo
{
AngleRad = 0 ,
PosUnchanged = currentHitObject . EndPosition ,
PosChanged = Vector2 . Zero
} ;
2021-04-26 05:57:01 +08:00
2021-05-01 10:01:43 +08:00
if ( i > = 3 )
2021-04-25 06:39:36 +08:00
{
2021-04-26 05:57:01 +08:00
i = 0 ;
rateOfChangeMultiplier = ( float ) rng . NextDouble ( ) * 2 - 1 ;
2021-04-25 06:39:36 +08:00
}
2021-04-26 05:57:01 +08:00
2021-05-13 00:11:50 +08:00
if ( currentHitObject is HitCircle circle )
2021-04-25 06:39:36 +08:00
{
2021-05-13 00:11:50 +08:00
var distanceToPrev = Vector2 . Distance ( currentObjectInfo . PosUnchanged , prevObjectInfo . PosUnchanged ) ;
2021-04-26 05:57:01 +08:00
2021-05-13 00:11:50 +08:00
getObjectInfo (
2021-04-26 05:57:01 +08:00
rateOfChangeMultiplier ,
2021-05-13 00:11:50 +08:00
prevObjectInfo ,
2021-04-26 05:57:01 +08:00
distanceToPrev ,
2021-05-13 00:11:50 +08:00
ref currentObjectInfo
2021-04-25 06:39:36 +08:00
) ;
2021-05-13 00:11:50 +08:00
circle . Position = currentObjectInfo . PosChanged ;
2021-04-25 06:39:36 +08:00
}
2021-04-26 05:57:01 +08:00
// TODO: Implement slider position randomisation
2021-05-13 00:11:50 +08:00
prevObjectInfo = currentObjectInfo ;
2021-04-26 05:57:01 +08:00
i + + ;
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-13 00:11:50 +08:00
private void getObjectInfo (
2021-04-26 05:57:01 +08:00
float rateOfChangeMultiplier ,
2021-05-13 00:11:50 +08:00
HitObjectInfo prevObjectInfo ,
2021-04-26 05:57:01 +08:00
float distanceToPrev ,
2021-05-13 00:11:50 +08:00
ref HitObjectInfo 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.
var maxDistance = OsuPlayfield . BASE_SIZE . LengthFast ;
var randomAngleRad = rateOfChangeMultiplier * 2 * Math . PI * distanceToPrev / maxDistance ;
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-13 00:11:50 +08:00
posRelativeToPrev = getRotatedVector ( prevObjectInfo . PosChanged , 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 ) ;
var position = Vector2 . Add ( prevObjectInfo . PosChanged , 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.
if ( position . X < 0 )
position . X = 0 ;
else if ( position . X > OsuPlayfield . BASE_SIZE . X )
position . X = OsuPlayfield . BASE_SIZE . X ;
if ( position . Y < 0 )
position . Y = 0 ;
else if ( position . Y > OsuPlayfield . BASE_SIZE . Y )
position . Y = OsuPlayfield . BASE_SIZE . Y ;
2021-05-13 00:11:50 +08:00
currentObjectInfo . PosChanged = 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 ;
var playfieldMiddle = Vector2 . Divide ( OsuPlayfield . BASE_SIZE , 2 ) ;
if ( prevPosChanged . X < playfieldMiddle . X )
{
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
) ;
}
if ( prevPosChanged . Y < playfieldMiddle . Y )
{
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
) ;
}
return rotateVectorTowardsVector (
posRelativeToPrev ,
Vector2 . Subtract ( playfieldMiddle , prevPosChanged ) ,
2021-05-13 00:11:50 +08:00
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 ;
while ( diff < - Math . PI )
{
diff + = 2 * Math . PI ;
}
while ( diff > Math . PI )
{
diff - = 2 * Math . PI ;
}
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
private struct HitObjectInfo
{
internal float AngleRad { get ; set ; }
internal Vector2 PosUnchanged { get ; set ; }
internal Vector2 PosChanged { get ; set ; }
}
2021-04-25 06:39:36 +08:00
}
2021-05-01 10:01:43 +08:00
2021-05-13 00:11:50 +08:00
public class OsuModRandomSettingsControl : SettingsItem < int? >
2021-05-01 10:01:43 +08:00
{
protected override Drawable CreateControl ( ) = > new SeedControl
{
RelativeSizeAxes = Axes . X ,
Margin = new MarginPadding { Top = 5 }
} ;
2021-05-13 00:11:50 +08:00
private sealed class SeedControl : CompositeDrawable , IHasCurrentValue < int? >
2021-05-01 10:01:43 +08:00
{
2021-05-13 00:11:50 +08:00
private readonly BindableWithCurrent < int? > current = new BindableWithCurrent < int? > ( ) ;
public Bindable < int? > Current
2021-05-01 10:01:43 +08:00
{
get = > current ;
2021-05-13 00:11:50 +08:00
set
{
current . Current = value ;
seedNumberBox . Text = value . Value . ToString ( ) ;
}
2021-05-01 10:01:43 +08:00
}
private readonly OsuNumberBox seedNumberBox ;
public SeedControl ( )
{
AutoSizeAxes = Axes . Y ;
InternalChildren = new [ ]
{
new GridContainer
{
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 [ ]
{
new Drawable [ ]
{
seedNumberBox = new OsuNumberBox
{
RelativeSizeAxes = Axes . X ,
CommitOnFocusLost = true
}
}
}
}
} ;
2021-05-13 00:11:50 +08:00
seedNumberBox . Current . BindValueChanged ( e = >
{
int? value = null ;
2021-05-01 10:01:43 +08:00
2021-05-13 00:11:50 +08:00
if ( int . TryParse ( e . NewValue , out var intVal ) )
value = intVal ;
2021-05-01 10:01:43 +08:00
2021-05-13 00:11:50 +08:00
current . Value = value ;
} ) ;
2021-05-01 10:01:43 +08:00
}
protected override void Update ( )
{
2021-05-13 00:11:50 +08:00
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
}