2021-06-24 13:19:42 +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.
2022-06-17 15:37:17 +08:00
#nullable disable
2021-06-24 13:19:42 +08:00
using System ;
2021-07-26 22:46:41 +08:00
using System.Linq ;
using osu.Framework.Extensions.IEnumerableExtensions ;
2021-06-24 13:19:42 +08:00
using osu.Game.Rulesets.Osu.UI ;
2021-07-26 22:46:41 +08:00
using osu.Game.Rulesets.Objects ;
using osu.Game.Rulesets.Osu.Objects ;
2021-06-24 13:19:42 +08:00
using osuTK ;
namespace osu.Game.Rulesets.Osu.Utils
{
2022-03-10 11:53:03 +08:00
public static partial class OsuHitObjectGenerationUtils
2021-06-24 13:19:42 +08:00
{
// The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
// The closer the hit objects draw to the border, the sharper the turn
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 = OsuPlayfield . BASE_SIZE / 2 ;
/// <summary>
/// Rotate a hit object away from the playfield edge, while keeping a constant distance
/// from the previous object.
/// </summary>
/// <remarks>
/// The extent of rotation depends on the position of the hit object. Hit objects
/// closer to the playfield edge will be rotated to a larger extent.
/// </remarks>
/// <param name="prevObjectPos">Position of the previous hit object.</param>
/// <param name="posRelativeToPrev">Position of the hit object to be rotated, relative to the previous hit object.</param>
/// <param name="rotationRatio">
/// The extent of rotation.
/// 0 means the hit object is never rotated.
/// 1 means the hit object will be fully rotated towards playfield center when it is originally at playfield edge.
/// </param>
/// <returns>The new position of the hit object, relative to the previous one.</returns>
public static Vector2 RotateAwayFromEdge ( Vector2 prevObjectPos , Vector2 posRelativeToPrev , float rotationRatio = 0.5f )
{
2021-10-27 12:04:41 +08:00
float relativeRotationDistance = 0f ;
2021-06-24 13:19:42 +08:00
if ( prevObjectPos . X < playfield_middle . X )
{
relativeRotationDistance = Math . Max (
( border_distance_x - prevObjectPos . X ) / border_distance_x ,
relativeRotationDistance
) ;
}
else
{
relativeRotationDistance = Math . Max (
( prevObjectPos . X - ( OsuPlayfield . BASE_SIZE . X - border_distance_x ) ) / border_distance_x ,
relativeRotationDistance
) ;
}
if ( prevObjectPos . Y < playfield_middle . Y )
{
relativeRotationDistance = Math . Max (
( border_distance_y - prevObjectPos . Y ) / border_distance_y ,
relativeRotationDistance
) ;
}
else
{
relativeRotationDistance = Math . Max (
( prevObjectPos . Y - ( OsuPlayfield . BASE_SIZE . Y - border_distance_y ) ) / border_distance_y ,
relativeRotationDistance
) ;
}
2021-06-24 13:22:10 +08:00
return RotateVectorTowardsVector (
posRelativeToPrev ,
playfield_middle - prevObjectPos ,
Math . Min ( 1 , relativeRotationDistance * rotationRatio )
) ;
2021-06-24 13:19:42 +08:00
}
/// <summary>
/// Rotates vector "initial" towards vector "destination".
/// </summary>
/// <param name="initial">The vector to be rotated.</param>
/// <param name="destination">The vector that "initial" should be rotated towards.</param>
/// <param name="rotationRatio">How much "initial" should be rotated. 0 means no rotation. 1 means "initial" is fully rotated to equal "destination".</param>
/// <returns>The rotated vector.</returns>
public static Vector2 RotateVectorTowardsVector ( Vector2 initial , Vector2 destination , float rotationRatio )
{
2021-10-27 12:04:41 +08:00
float initialAngleRad = MathF . Atan2 ( initial . Y , initial . X ) ;
float destAngleRad = MathF . Atan2 ( destination . Y , destination . X ) ;
2021-06-24 13:19:42 +08:00
2021-10-27 12:04:41 +08:00
float diff = destAngleRad - initialAngleRad ;
2021-06-24 13:19:42 +08:00
2021-06-25 09:44:23 +08:00
while ( diff < - MathF . PI ) diff + = 2 * MathF . PI ;
2021-06-24 13:19:42 +08:00
2021-06-25 09:44:23 +08:00
while ( diff > MathF . PI ) diff - = 2 * MathF . PI ;
2021-06-24 13:19:42 +08:00
2021-10-27 12:04:41 +08:00
float finalAngleRad = initialAngleRad + rotationRatio * diff ;
2021-06-24 13:19:42 +08:00
return new Vector2 (
2021-06-25 09:44:23 +08:00
initial . Length * MathF . Cos ( finalAngleRad ) ,
initial . Length * MathF . Sin ( finalAngleRad )
2021-06-24 13:19:42 +08:00
) ;
}
2021-07-26 22:46:41 +08:00
/// <summary>
2021-07-27 23:22:32 +08:00
/// Reflects the position of the <see cref="OsuHitObject"/> in the playfield horizontally.
2021-07-26 22:46:41 +08:00
/// </summary>
2021-07-27 21:01:01 +08:00
/// <param name="osuObject">The object to reflect.</param>
public static void ReflectHorizontally ( OsuHitObject osuObject )
2021-07-26 22:46:41 +08:00
{
osuObject . Position = new Vector2 ( OsuPlayfield . BASE_SIZE . X - osuObject . X , osuObject . Position . Y ) ;
if ( ! ( osuObject is Slider slider ) )
2021-07-27 21:01:01 +08:00
return ;
2021-07-26 22:46:41 +08:00
2022-04-18 09:38:51 +08:00
// No need to update the head and tail circles, since slider handles that when the new slider path is set
2021-07-26 22:46:41 +08:00
slider . NestedHitObjects . OfType < SliderTick > ( ) . ForEach ( h = > h . Position = new Vector2 ( OsuPlayfield . BASE_SIZE . X - h . Position . X , h . Position . Y ) ) ;
slider . NestedHitObjects . OfType < SliderRepeat > ( ) . ForEach ( h = > h . Position = new Vector2 ( OsuPlayfield . BASE_SIZE . X - h . Position . X , h . Position . Y ) ) ;
2021-08-26 00:42:57 +08:00
var controlPoints = slider . Path . ControlPoints . Select ( p = > new PathControlPoint ( p . Position , p . Type ) ) . ToArray ( ) ;
2021-07-26 22:46:41 +08:00
foreach ( var point in controlPoints )
2021-08-26 00:42:57 +08:00
point . Position = new Vector2 ( - point . Position . X , point . Position . Y ) ;
2021-07-26 22:46:41 +08:00
slider . Path = new SliderPath ( controlPoints , slider . Path . ExpectedDistance . Value ) ;
}
/// <summary>
2021-07-27 23:22:32 +08:00
/// Reflects the position of the <see cref="OsuHitObject"/> in the playfield vertically.
2021-07-26 22:46:41 +08:00
/// </summary>
2021-07-27 21:01:01 +08:00
/// <param name="osuObject">The object to reflect.</param>
public static void ReflectVertically ( OsuHitObject osuObject )
2021-07-26 22:46:41 +08:00
{
osuObject . Position = new Vector2 ( osuObject . Position . X , OsuPlayfield . BASE_SIZE . Y - osuObject . Y ) ;
if ( ! ( osuObject is Slider slider ) )
2021-07-27 21:01:01 +08:00
return ;
2021-07-26 22:46:41 +08:00
2022-04-18 09:38:51 +08:00
// No need to update the head and tail circles, since slider handles that when the new slider path is set
2021-07-26 22:46:41 +08:00
slider . NestedHitObjects . OfType < SliderTick > ( ) . ForEach ( h = > h . Position = new Vector2 ( h . Position . X , OsuPlayfield . BASE_SIZE . Y - h . Position . Y ) ) ;
slider . NestedHitObjects . OfType < SliderRepeat > ( ) . ForEach ( h = > h . Position = new Vector2 ( h . Position . X , OsuPlayfield . BASE_SIZE . Y - h . Position . Y ) ) ;
2021-08-26 00:42:57 +08:00
var controlPoints = slider . Path . ControlPoints . Select ( p = > new PathControlPoint ( p . Position , p . Type ) ) . ToArray ( ) ;
2021-07-26 22:46:41 +08:00
foreach ( var point in controlPoints )
2021-08-26 00:42:57 +08:00
point . Position = new Vector2 ( point . Position . X , - point . Position . Y ) ;
2021-07-26 22:46:41 +08:00
slider . Path = new SliderPath ( controlPoints , slider . Path . ExpectedDistance . Value ) ;
}
2022-04-01 11:36:20 +08:00
/// <summary>
/// Rotate a slider about its start position by the specified angle.
/// </summary>
/// <param name="slider">The slider to be rotated.</param>
2022-04-17 10:40:43 +08:00
/// <param name="rotation">The angle, measured in radians, to rotate the slider by.</param>
2022-04-01 11:36:20 +08:00
public static void RotateSlider ( Slider slider , float rotation )
{
void rotateNestedObject ( OsuHitObject nested ) = > nested . Position = rotateVector ( nested . Position - slider . Position , rotation ) + slider . Position ;
2022-04-18 09:38:51 +08:00
// No need to update the head and tail circles, since slider handles that when the new slider path is set
2022-04-01 11:36:20 +08:00
slider . NestedHitObjects . OfType < SliderTick > ( ) . ForEach ( rotateNestedObject ) ;
slider . NestedHitObjects . OfType < SliderRepeat > ( ) . ForEach ( rotateNestedObject ) ;
var controlPoints = slider . Path . ControlPoints . Select ( p = > new PathControlPoint ( p . Position , p . Type ) ) . ToArray ( ) ;
foreach ( var point in controlPoints )
point . Position = rotateVector ( point . Position , rotation ) ;
slider . Path = new SliderPath ( controlPoints , slider . Path . ExpectedDistance . Value ) ;
}
/// <summary>
/// Rotate a vector by the specified angle.
/// </summary>
/// <param name="vector">The vector to be rotated.</param>
2022-04-17 10:40:43 +08:00
/// <param name="rotation">The angle, measured in radians, to rotate the vector by.</param>
2022-04-01 11:36:20 +08:00
/// <returns>The rotated vector.</returns>
private static Vector2 rotateVector ( Vector2 vector , float rotation )
{
2022-04-11 14:15:08 +08:00
float angle = MathF . Atan2 ( vector . Y , vector . X ) + rotation ;
2022-04-01 11:36:20 +08:00
float length = vector . Length ;
return new Vector2 (
2022-04-11 14:15:08 +08:00
length * MathF . Cos ( angle ) ,
length * MathF . Sin ( angle )
2022-04-01 11:36:20 +08:00
) ;
}
2021-06-24 13:19:42 +08:00
}
}