2019-01-24 16:43:03 +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.
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
|
|
|
|
using System;
|
2023-09-29 17:23:22 +08:00
|
|
|
|
using System.Collections.Generic;
|
2019-09-02 17:31:33 +08:00
|
|
|
|
using System.Diagnostics;
|
2018-11-09 12:58:46 +08:00
|
|
|
|
using osu.Framework.Allocation;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
using osu.Framework.Graphics;
|
2019-02-14 17:47:05 +08:00
|
|
|
|
using osu.Framework.Graphics.Containers;
|
2019-07-24 17:50:57 +08:00
|
|
|
|
using osu.Framework.Input.Bindings;
|
2021-09-16 17:26:12 +08:00
|
|
|
|
using osu.Framework.Input.Events;
|
2022-09-22 13:30:01 +08:00
|
|
|
|
using osu.Game.Graphics.Containers;
|
2020-06-15 21:44:55 +08:00
|
|
|
|
using osu.Game.Rulesets.Judgements;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
using osu.Game.Rulesets.Objects.Drawables;
|
2020-06-15 21:44:55 +08:00
|
|
|
|
using osu.Game.Rulesets.Osu.Judgements;
|
2021-06-21 12:35:07 +08:00
|
|
|
|
using osu.Game.Rulesets.Osu.Skinning;
|
2020-12-04 19:21:53 +08:00
|
|
|
|
using osu.Game.Rulesets.Osu.Skinning.Default;
|
2023-07-18 11:31:21 +08:00
|
|
|
|
using osu.Game.Rulesets.Osu.UI;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
using osu.Game.Rulesets.Scoring;
|
2019-07-24 17:50:57 +08:00
|
|
|
|
using osu.Game.Skinning;
|
2019-10-01 13:15:48 +08:00
|
|
|
|
using osuTK;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
|
|
|
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|
|
|
|
{
|
2022-11-24 13:32:20 +08:00
|
|
|
|
public partial class DrawableHitCircle : DrawableOsuHitObject, IHasApproachCircle
|
2018-04-13 17:19:50 +08:00
|
|
|
|
{
|
2024-02-07 02:38:07 +08:00
|
|
|
|
public OsuAction? HitAction => HitArea.HitAction;
|
2020-03-28 12:39:08 +08:00
|
|
|
|
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
|
|
|
|
|
|
2024-02-07 02:38:07 +08:00
|
|
|
|
public SkinnableDrawable ApproachCircle { get; private set; } = null!;
|
|
|
|
|
public HitReceptor HitArea { get; private set; } = null!;
|
|
|
|
|
public SkinnableDrawable CirclePiece { get; private set; } = null!;
|
2020-11-05 12:51:46 +08:00
|
|
|
|
|
2024-02-07 02:38:07 +08:00
|
|
|
|
protected override IEnumerable<Drawable> DimmablePieces => new[] { CirclePiece };
|
2023-09-29 17:23:22 +08:00
|
|
|
|
|
2021-06-21 12:35:07 +08:00
|
|
|
|
Drawable IHasApproachCircle.ApproachCircle => ApproachCircle;
|
|
|
|
|
|
2024-02-07 02:38:07 +08:00
|
|
|
|
private Container scaleContainer = null!;
|
|
|
|
|
private ShakeContainer shakeContainer = null!;
|
2020-06-15 21:44:55 +08:00
|
|
|
|
|
2020-11-10 23:22:06 +08:00
|
|
|
|
public DrawableHitCircle()
|
|
|
|
|
: this(null)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-07 02:38:07 +08:00
|
|
|
|
public DrawableHitCircle(HitCircle? h = null)
|
2018-04-13 17:19:50 +08:00
|
|
|
|
: base(h)
|
|
|
|
|
{
|
2020-11-05 12:51:46 +08:00
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2020-11-05 12:51:46 +08:00
|
|
|
|
[BackgroundDependencyLoader]
|
|
|
|
|
private void load()
|
|
|
|
|
{
|
|
|
|
|
Origin = Anchor.Centre;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2022-09-22 13:46:37 +08:00
|
|
|
|
AddRangeInternal(new Drawable[]
|
2018-04-13 17:19:50 +08:00
|
|
|
|
{
|
2019-02-14 17:47:05 +08:00
|
|
|
|
scaleContainer = new Container
|
2018-04-13 17:19:50 +08:00
|
|
|
|
{
|
2019-02-14 17:47:05 +08:00
|
|
|
|
RelativeSizeAxes = Axes.Both,
|
|
|
|
|
Origin = Anchor.Centre,
|
|
|
|
|
Anchor = Anchor.Centre,
|
2019-07-24 17:50:57 +08:00
|
|
|
|
Children = new Drawable[]
|
2018-04-13 17:19:50 +08:00
|
|
|
|
{
|
2019-10-21 16:14:08 +08:00
|
|
|
|
HitArea = new HitReceptor
|
2019-02-14 17:47:05 +08:00
|
|
|
|
{
|
2024-02-06 19:06:51 +08:00
|
|
|
|
CanBeHit = () => !AllJudged,
|
|
|
|
|
Hit = () => UpdateResult(true)
|
2019-07-24 17:50:57 +08:00
|
|
|
|
},
|
2022-09-22 13:30:01 +08:00
|
|
|
|
shakeContainer = new ShakeContainer
|
2019-07-24 17:50:57 +08:00
|
|
|
|
{
|
2022-09-22 13:30:01 +08:00
|
|
|
|
ShakeDuration = 30,
|
2021-09-16 18:35:15 +08:00
|
|
|
|
RelativeSizeAxes = Axes.Both,
|
2022-09-22 13:30:01 +08:00
|
|
|
|
Children = new Drawable[]
|
|
|
|
|
{
|
2022-11-09 15:04:56 +08:00
|
|
|
|
CirclePiece = new SkinnableDrawable(new OsuSkinComponentLookup(CirclePieceComponent), _ => new MainCirclePiece())
|
2022-09-22 13:30:01 +08:00
|
|
|
|
{
|
|
|
|
|
Anchor = Anchor.Centre,
|
|
|
|
|
Origin = Anchor.Centre,
|
|
|
|
|
},
|
2022-11-09 15:04:56 +08:00
|
|
|
|
ApproachCircle = new ProxyableSkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.ApproachCircle), _ => new DefaultApproachCircle())
|
2022-09-22 13:30:01 +08:00
|
|
|
|
{
|
|
|
|
|
Anchor = Anchor.Centre,
|
|
|
|
|
Origin = Anchor.Centre,
|
|
|
|
|
RelativeSizeAxes = Axes.Both,
|
|
|
|
|
Alpha = 0,
|
|
|
|
|
Scale = new Vector2(4),
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-02-14 17:47:05 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
},
|
2022-09-22 13:46:37 +08:00
|
|
|
|
});
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2019-10-21 16:14:08 +08:00
|
|
|
|
Size = HitArea.DrawSize;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2022-10-26 14:26:20 +08:00
|
|
|
|
PositionBindable.BindValueChanged(_ => UpdatePosition());
|
|
|
|
|
StackHeightBindable.BindValueChanged(_ => UpdatePosition());
|
2020-11-06 22:35:47 +08:00
|
|
|
|
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
|
2018-04-13 17:19:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-12 18:29:08 +08:00
|
|
|
|
public override double LifetimeStart
|
|
|
|
|
{
|
|
|
|
|
get => base.LifetimeStart;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
base.LifetimeStart = value;
|
|
|
|
|
ApproachCircle.LifetimeStart = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override double LifetimeEnd
|
|
|
|
|
{
|
|
|
|
|
get => base.LifetimeEnd;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
base.LifetimeEnd = value;
|
|
|
|
|
ApproachCircle.LifetimeEnd = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-26 14:26:20 +08:00
|
|
|
|
protected virtual void UpdatePosition()
|
|
|
|
|
{
|
|
|
|
|
Position = HitObject.StackedPosition;
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-22 13:30:01 +08:00
|
|
|
|
public override void Shake() => shakeContainer.Shake();
|
|
|
|
|
|
2018-08-06 10:31:46 +08:00
|
|
|
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
2018-04-13 17:19:50 +08:00
|
|
|
|
{
|
2019-09-02 17:31:33 +08:00
|
|
|
|
Debug.Assert(HitObject.HitWindows != null);
|
|
|
|
|
|
2018-04-13 17:19:50 +08:00
|
|
|
|
if (!userTriggered)
|
|
|
|
|
{
|
|
|
|
|
if (!HitObject.HitWindows.CanBeHit(timeOffset))
|
2024-02-06 19:06:51 +08:00
|
|
|
|
{
|
|
|
|
|
ApplyResult((r, position) =>
|
|
|
|
|
{
|
|
|
|
|
var circleResult = (OsuHitCircleJudgementResult)r;
|
|
|
|
|
|
|
|
|
|
circleResult.Type = r.Judgement.MinResult;
|
|
|
|
|
circleResult.CursorPositionAtHit = position;
|
|
|
|
|
}, computeHitPosition());
|
|
|
|
|
}
|
2018-08-01 20:46:22 +08:00
|
|
|
|
|
2018-04-13 17:19:50 +08:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-05 20:35:41 +08:00
|
|
|
|
var result = ResultFor(timeOffset);
|
|
|
|
|
var clickAction = CheckHittable?.Invoke(this, Time.Current, result);
|
2019-04-01 11:16:05 +08:00
|
|
|
|
|
2023-08-23 19:12:18 +08:00
|
|
|
|
if (clickAction == ClickAction.Shake)
|
2022-09-22 13:30:01 +08:00
|
|
|
|
Shake();
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2024-02-05 20:35:41 +08:00
|
|
|
|
if (result == HitResult.None || clickAction != ClickAction.Hit)
|
2018-04-13 17:19:50 +08:00
|
|
|
|
return;
|
|
|
|
|
|
2024-02-05 20:21:01 +08:00
|
|
|
|
ApplyResult<(HitResult result, Vector2? position)>((r, state) =>
|
|
|
|
|
{
|
|
|
|
|
var circleResult = (OsuHitCircleJudgementResult)r;
|
2020-06-15 21:44:55 +08:00
|
|
|
|
|
2024-02-05 20:21:01 +08:00
|
|
|
|
circleResult.Type = state.result;
|
|
|
|
|
circleResult.CursorPositionAtHit = state.position;
|
2024-02-06 19:06:51 +08:00
|
|
|
|
}, (result, computeHitPosition()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Vector2? computeHitPosition()
|
|
|
|
|
{
|
|
|
|
|
if (HitArea.ClosestPressPosition is Vector2 screenSpaceHitPosition)
|
|
|
|
|
return HitObject.StackedPosition + (ToLocalSpace(screenSpaceHitPosition) - DrawSize / 2);
|
|
|
|
|
|
|
|
|
|
return null;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-02-03 21:12:20 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Retrieves the <see cref="HitResult"/> for a time offset.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="timeOffset">The time offset.</param>
|
|
|
|
|
/// <returns>The hit result, or <see cref="HitResult.None"/> if <paramref name="timeOffset"/> doesn't result in a judgement.</returns>
|
|
|
|
|
protected virtual HitResult ResultFor(double timeOffset) => HitObject.HitWindows.ResultFor(timeOffset);
|
|
|
|
|
|
2019-07-22 14:33:12 +08:00
|
|
|
|
protected override void UpdateInitialTransforms()
|
2018-04-13 17:19:50 +08:00
|
|
|
|
{
|
2019-07-22 14:33:12 +08:00
|
|
|
|
base.UpdateInitialTransforms();
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2019-09-18 19:04:49 +08:00
|
|
|
|
CirclePiece.FadeInFromZero(HitObject.TimeFadeIn);
|
2019-08-28 17:10:58 +08:00
|
|
|
|
|
2023-09-29 11:44:26 +08:00
|
|
|
|
ApproachCircle.FadeTo(0.9f, Math.Min(HitObject.TimeFadeIn * 2, HitObject.TimePreempt));
|
2019-07-24 18:32:24 +08:00
|
|
|
|
ApproachCircle.ScaleTo(1f, HitObject.TimePreempt);
|
2018-12-13 13:55:28 +08:00
|
|
|
|
ApproachCircle.Expire(true);
|
2018-04-13 17:19:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-23 18:19:07 +08:00
|
|
|
|
protected override void UpdateStartTimeStateTransforms()
|
|
|
|
|
{
|
|
|
|
|
base.UpdateStartTimeStateTransforms();
|
|
|
|
|
|
2021-06-17 14:08:22 +08:00
|
|
|
|
// always fade out at the circle's start time (to match user expectations).
|
2021-03-23 18:19:07 +08:00
|
|
|
|
ApproachCircle.FadeOut(50);
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-04 15:19:07 +08:00
|
|
|
|
protected override void UpdateHitStateTransforms(ArmedState state)
|
2018-04-13 17:19:50 +08:00
|
|
|
|
{
|
2019-09-02 17:31:33 +08:00
|
|
|
|
Debug.Assert(HitObject.HitWindows != null);
|
|
|
|
|
|
2021-03-23 18:19:07 +08:00
|
|
|
|
// todo: temporary / arbitrary, used for lifetime optimisation.
|
|
|
|
|
this.Delay(800).FadeOut();
|
|
|
|
|
|
2018-04-13 17:19:50 +08:00
|
|
|
|
switch (state)
|
|
|
|
|
{
|
2022-10-05 12:42:04 +08:00
|
|
|
|
default:
|
|
|
|
|
ApproachCircle.FadeOut();
|
|
|
|
|
break;
|
|
|
|
|
|
2018-04-13 17:19:50 +08:00
|
|
|
|
case ArmedState.Idle:
|
2024-02-07 02:38:07 +08:00
|
|
|
|
HitArea.Reset();
|
2018-04-13 17:19:50 +08:00
|
|
|
|
break;
|
2019-04-01 11:16:05 +08:00
|
|
|
|
|
2018-04-13 17:19:50 +08:00
|
|
|
|
case ArmedState.Miss:
|
|
|
|
|
this.FadeOut(100);
|
|
|
|
|
break;
|
2019-07-24 17:50:57 +08:00
|
|
|
|
}
|
2020-11-17 22:19:59 +08:00
|
|
|
|
|
|
|
|
|
Expire();
|
2019-07-24 17:50:57 +08:00
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2019-07-24 17:50:57 +08:00
|
|
|
|
public Drawable ProxiedLayer => ApproachCircle;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2020-06-15 21:44:55 +08:00
|
|
|
|
protected override JudgementResult CreateResult(Judgement judgement) => new OsuHitCircleJudgementResult(HitObject, judgement);
|
|
|
|
|
|
2022-11-24 13:32:20 +08:00
|
|
|
|
public partial class HitReceptor : CompositeDrawable, IKeyBindingHandler<OsuAction>
|
2019-07-24 17:50:57 +08:00
|
|
|
|
{
|
|
|
|
|
// IsHovered is used
|
|
|
|
|
public override bool HandlePositionalInput => true;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2024-02-07 02:47:36 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Whether the hitobject can still be hit at the current point in time.
|
|
|
|
|
/// </summary>
|
2024-02-07 02:38:07 +08:00
|
|
|
|
public required Func<bool> CanBeHit { get; set; }
|
2024-02-07 02:47:36 +08:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// An action that's invoked to perform the hit.
|
|
|
|
|
/// </summary>
|
2024-02-07 02:38:07 +08:00
|
|
|
|
public required Action Hit { get; set; }
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2024-02-07 02:47:36 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// The <see cref="OsuAction"/> with which the hit was attempted.
|
|
|
|
|
/// </summary>
|
2024-02-07 02:38:07 +08:00
|
|
|
|
public OsuAction? HitAction { get; private set; }
|
2024-02-07 02:47:36 +08:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The closest position to the hit receptor at the point where the hit was attempted.
|
|
|
|
|
/// </summary>
|
2024-02-07 02:38:07 +08:00
|
|
|
|
public Vector2? ClosestPressPosition { get; private set; }
|
2019-07-24 17:50:57 +08:00
|
|
|
|
|
2019-10-21 16:14:08 +08:00
|
|
|
|
public HitReceptor()
|
2019-07-24 17:50:57 +08:00
|
|
|
|
{
|
2023-09-20 11:48:15 +08:00
|
|
|
|
Size = OsuHitObject.OBJECT_DIMENSIONS;
|
2019-07-24 17:50:57 +08:00
|
|
|
|
|
|
|
|
|
Anchor = Anchor.Centre;
|
|
|
|
|
Origin = Anchor.Centre;
|
2020-03-07 04:21:20 +08:00
|
|
|
|
|
|
|
|
|
CornerRadius = OsuHitObject.OBJECT_RADIUS;
|
|
|
|
|
CornerExponent = 2;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-09-16 17:26:12 +08:00
|
|
|
|
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
2019-07-24 17:50:57 +08:00
|
|
|
|
{
|
2024-02-07 02:50:56 +08:00
|
|
|
|
if (!CanBeHit())
|
2024-02-06 19:06:51 +08:00
|
|
|
|
return false;
|
|
|
|
|
|
2021-09-16 17:26:12 +08:00
|
|
|
|
switch (e.Action)
|
2019-07-24 17:50:57 +08:00
|
|
|
|
{
|
|
|
|
|
case OsuAction.LeftButton:
|
|
|
|
|
case OsuAction.RightButton:
|
2024-02-07 02:52:51 +08:00
|
|
|
|
if (ClosestPressPosition is Vector2 curClosest)
|
2024-02-06 19:06:51 +08:00
|
|
|
|
{
|
2024-02-07 02:52:51 +08:00
|
|
|
|
float oldDist = Vector2.DistanceSquared(curClosest, ScreenSpaceDrawQuad.Centre);
|
|
|
|
|
float newDist = Vector2.DistanceSquared(e.ScreenSpaceMousePosition, ScreenSpaceDrawQuad.Centre);
|
2024-02-06 19:06:51 +08:00
|
|
|
|
|
2024-02-07 02:52:51 +08:00
|
|
|
|
if (newDist < oldDist)
|
2024-02-06 19:06:51 +08:00
|
|
|
|
ClosestPressPosition = e.ScreenSpaceMousePosition;
|
|
|
|
|
}
|
2024-02-07 02:52:51 +08:00
|
|
|
|
else
|
|
|
|
|
ClosestPressPosition = e.ScreenSpaceMousePosition;
|
2024-02-06 19:06:51 +08:00
|
|
|
|
|
|
|
|
|
if (IsHovered)
|
2019-07-24 17:50:57 +08:00
|
|
|
|
{
|
2024-02-07 02:38:07 +08:00
|
|
|
|
Hit();
|
2023-09-08 17:08:25 +08:00
|
|
|
|
HitAction ??= e.Action;
|
2019-07-24 17:50:57 +08:00
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-16 17:26:12 +08:00
|
|
|
|
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
|
2020-01-22 12:22:34 +08:00
|
|
|
|
{
|
|
|
|
|
}
|
2024-02-07 02:38:07 +08:00
|
|
|
|
|
2024-02-07 02:47:36 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Resets to a fresh state.
|
|
|
|
|
/// </summary>
|
2024-02-07 02:38:07 +08:00
|
|
|
|
public void Reset()
|
|
|
|
|
{
|
|
|
|
|
HitAction = null;
|
|
|
|
|
ClosestPressPosition = null;
|
|
|
|
|
}
|
2019-07-24 17:50:57 +08:00
|
|
|
|
}
|
2021-09-16 18:35:15 +08:00
|
|
|
|
|
2022-11-24 13:32:20 +08:00
|
|
|
|
private partial class ProxyableSkinnableDrawable : SkinnableDrawable
|
2021-09-16 18:35:15 +08:00
|
|
|
|
{
|
|
|
|
|
public override bool RemoveWhenNotAlive => false;
|
|
|
|
|
|
2024-02-07 02:38:07 +08:00
|
|
|
|
public ProxyableSkinnableDrawable(ISkinComponentLookup lookup, Func<ISkinComponentLookup, Drawable>? defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling)
|
2022-11-09 13:11:41 +08:00
|
|
|
|
: base(lookup, defaultImplementation, confineMode)
|
2021-09-16 18:35:15 +08:00
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
}
|
|
|
|
|
}
|