2020-06-15 12:48:59 +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 ;
2020-06-15 13:45:18 +00:00
using System.Collections.Generic ;
2020-06-15 12:48:59 +00:00
using System.Diagnostics ;
using osu.Framework.Allocation ;
using osu.Framework.Extensions.Color4Extensions ;
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
using osu.Framework.Graphics.Shapes ;
2020-06-16 07:31:02 +00:00
using osu.Framework.Layout ;
2020-06-18 13:11:03 +00:00
using osu.Game.Beatmaps ;
using osu.Game.Rulesets.Osu.Objects ;
2020-06-15 13:45:18 +00:00
using osu.Game.Rulesets.Osu.Scoring ;
2020-06-15 12:48:59 +00:00
using osuTK ;
using osuTK.Graphics ;
namespace osu.Game.Rulesets.Osu.Statistics
{
public class Heatmap : CompositeDrawable
{
/// <summary>
2020-06-16 07:31:02 +00:00
/// Size of the inner circle containing the "hit" points, relative to the size of this <see cref="Heatmap"/>.
2020-06-15 12:48:59 +00:00
/// All other points outside of the inner circle are "miss" points.
/// </summary>
private const float inner_portion = 0.8f ;
2020-06-19 10:08:36 +00:00
/// <summary>
/// Number of rows/columns of points.
/// 4px per point @ 128x128 size (the contents of the <see cref="Heatmap"/> are always square). 1024 total points.
/// </summary>
private const int points_per_dimension = 32 ;
2020-06-15 12:48:59 +00:00
private const float rotation = 45 ;
2020-06-19 10:08:36 +00:00
private GridContainer pointGrid ;
2020-06-15 12:48:59 +00:00
2020-06-18 13:11:03 +00:00
private readonly BeatmapInfo beatmap ;
private readonly IReadOnlyList < HitEvent > hitEvents ;
2020-06-16 07:31:02 +00:00
private readonly LayoutValue sizeLayout = new LayoutValue ( Invalidation . DrawSize ) ;
2020-06-18 13:11:03 +00:00
public Heatmap ( BeatmapInfo beatmap , IReadOnlyList < HitEvent > hitEvents )
2020-06-15 12:48:59 +00:00
{
2020-06-18 13:11:03 +00:00
this . beatmap = beatmap ;
this . hitEvents = hitEvents ;
2020-06-15 13:45:18 +00:00
2020-06-16 07:31:02 +00:00
AddLayout ( sizeLayout ) ;
2020-06-15 12:48:59 +00:00
}
[BackgroundDependencyLoader]
private void load ( )
{
2020-06-16 07:31:02 +00:00
InternalChild = new Container
2020-06-15 12:48:59 +00:00
{
2020-06-16 07:31:02 +00:00
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
RelativeSizeAxes = Axes . Both ,
FillMode = FillMode . Fit ,
Children = new Drawable [ ]
2020-06-15 12:48:59 +00:00
{
2020-06-16 07:31:02 +00:00
new CircularContainer
2020-06-15 12:48:59 +00:00
{
2020-06-16 07:31:02 +00:00
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
2020-06-15 12:48:59 +00:00
RelativeSizeAxes = Axes . Both ,
2020-06-16 07:31:02 +00:00
Size = new Vector2 ( inner_portion ) ,
Masking = true ,
BorderThickness = 2f ,
BorderColour = Color4 . White ,
Child = new Box
2020-06-15 12:48:59 +00:00
{
2020-06-16 07:31:02 +00:00
RelativeSizeAxes = Axes . Both ,
Colour = Color4Extensions . FromHex ( "#202624" )
}
} ,
new Container
{
RelativeSizeAxes = Axes . Both ,
Masking = true ,
Children = new Drawable [ ]
2020-06-15 12:48:59 +00:00
{
2020-06-16 07:31:02 +00:00
new Box
{
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
RelativeSizeAxes = Axes . Y ,
Height = 2 , // We're rotating along a diagonal - we don't really care how big this is.
Width = 1f ,
Rotation = - rotation ,
Alpha = 0.3f ,
} ,
new Box
{
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
RelativeSizeAxes = Axes . Y ,
Height = 2 , // We're rotating along a diagonal - we don't really care how big this is.
Width = 1f ,
Rotation = rotation
} ,
new Box
{
Anchor = Anchor . TopRight ,
Origin = Anchor . TopRight ,
Width = 10 ,
Height = 2f ,
} ,
new Box
{
Anchor = Anchor . TopRight ,
Origin = Anchor . TopRight ,
Y = - 1 ,
Width = 2f ,
Height = 10 ,
}
2020-06-15 12:48:59 +00:00
}
2020-06-16 07:31:02 +00:00
} ,
2020-06-19 10:08:36 +00:00
pointGrid = new GridContainer
2020-06-16 07:31:02 +00:00
{
RelativeSizeAxes = Axes . Both
2020-06-15 12:48:59 +00:00
}
2020-06-16 07:31:02 +00:00
}
2020-06-15 12:48:59 +00:00
} ;
2020-06-16 07:31:02 +00:00
2020-06-19 10:08:36 +00:00
Vector2 centre = new Vector2 ( points_per_dimension ) / 2 ;
float innerRadius = centre . X * inner_portion ;
2020-06-15 12:48:59 +00:00
2020-06-19 10:08:36 +00:00
Drawable [ ] [ ] points = new Drawable [ points_per_dimension ] [ ] ;
2020-06-15 12:48:59 +00:00
2020-06-19 10:08:36 +00:00
for ( int r = 0 ; r < points_per_dimension ; r + + )
2020-06-15 12:48:59 +00:00
{
2020-06-19 10:08:36 +00:00
points [ r ] = new Drawable [ points_per_dimension ] ;
2020-06-15 12:48:59 +00:00
2020-06-19 10:08:36 +00:00
for ( int c = 0 ; c < points_per_dimension ; c + + )
{
HitPointType pointType = Vector2 . Distance ( new Vector2 ( c , r ) , centre ) < = innerRadius
? HitPointType . Hit
: HitPointType . Miss ;
2020-06-15 12:48:59 +00:00
2020-06-19 10:08:36 +00:00
var point = new HitPoint ( pointType )
2020-06-15 12:48:59 +00:00
{
Colour = pointType = = HitPointType . Hit ? new Color4 ( 102 , 255 , 204 , 255 ) : new Color4 ( 255 , 102 , 102 , 255 )
2020-06-19 10:08:36 +00:00
} ;
points [ r ] [ c ] = point ;
2020-06-15 12:48:59 +00:00
}
}
2020-06-15 13:45:18 +00:00
2020-06-19 10:08:36 +00:00
pointGrid . Content = points ;
2020-06-18 13:11:03 +00:00
if ( hitEvents . Count > 0 )
2020-06-16 08:20:38 +00:00
{
2020-06-18 13:11:03 +00:00
// Todo: This should probably not be done like this.
float radius = OsuHitObject . OBJECT_RADIUS * ( 1.0f - 0.7f * ( beatmap . BaseDifficulty . CircleSize - 5 ) / 5 ) / 2 ;
foreach ( var e in hitEvents )
{
if ( e . LastHitObject = = null | | e . CursorPosition = = null )
continue ;
AddPoint ( ( ( OsuHitObject ) e . LastHitObject ) . StackedEndPosition , ( ( OsuHitObject ) e . HitObject ) . StackedEndPosition , e . CursorPosition . Value , radius ) ;
}
2020-06-16 08:20:38 +00:00
}
2020-06-15 12:48:59 +00:00
}
2020-06-16 07:31:02 +00:00
protected void AddPoint ( Vector2 start , Vector2 end , Vector2 hitPoint , float radius )
2020-06-15 12:48:59 +00:00
{
2020-06-19 10:08:36 +00:00
if ( pointGrid . Content . Length = = 0 )
2020-06-16 07:31:02 +00:00
return ;
2020-06-15 12:48:59 +00:00
double angle1 = Math . Atan2 ( end . Y - hitPoint . Y , hitPoint . X - end . X ) ; // Angle between the end point and the hit point.
double angle2 = Math . Atan2 ( end . Y - start . Y , start . X - end . X ) ; // Angle between the end point and the start point.
double finalAngle = angle2 - angle1 ; // Angle between start, end, and hit points.
float normalisedDistance = Vector2 . Distance ( hitPoint , end ) / radius ;
2020-06-19 10:08:36 +00:00
// Convert the above into the local search space.
Vector2 localCentre = new Vector2 ( points_per_dimension ) / 2 ;
float localRadius = localCentre . X * inner_portion * normalisedDistance ; // The radius inside the inner portion which of the heatmap which the closest point lies.
double localAngle = finalAngle + 3 * Math . PI / 4 ; // The angle inside the heatmap on which the closest point lies.
Vector2 localPoint = localCentre + localRadius * new Vector2 ( ( float ) Math . Cos ( localAngle ) , ( float ) Math . Sin ( localAngle ) ) ;
2020-06-16 07:31:02 +00:00
2020-06-15 12:48:59 +00:00
// Find the most relevant hit point.
double minDist = double . PositiveInfinity ;
HitPoint point = null ;
2020-06-19 10:08:36 +00:00
for ( int r = 0 ; r < points_per_dimension ; r + + )
2020-06-15 12:48:59 +00:00
{
2020-06-19 10:08:36 +00:00
for ( int c = 0 ; c < points_per_dimension ; c + + )
2020-06-15 12:48:59 +00:00
{
2020-06-19 10:08:36 +00:00
float dist = Vector2 . Distance ( new Vector2 ( c , r ) , localPoint ) ;
if ( dist < minDist )
{
minDist = dist ;
point = ( HitPoint ) pointGrid . Content [ r ] [ c ] ;
}
2020-06-15 12:48:59 +00:00
}
}
Debug . Assert ( point ! = null ) ;
point . Increment ( ) ;
}
private class HitPoint : Circle
{
private readonly HitPointType pointType ;
2020-06-19 10:08:36 +00:00
public HitPoint ( HitPointType pointType )
2020-06-15 12:48:59 +00:00
{
this . pointType = pointType ;
2020-06-19 10:08:36 +00:00
RelativeSizeAxes = Axes . Both ;
2020-06-15 12:48:59 +00:00
Alpha = 0 ;
}
public void Increment ( )
{
if ( Alpha < 1 )
Alpha + = 0.1f ;
else if ( pointType = = HitPointType . Hit )
Colour = ( ( Color4 ) Colour ) . Lighten ( 0.1f ) ;
}
}
private enum HitPointType
{
Hit ,
Miss
}
}
}