osu/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

257 lines
9.7 KiB
C#
Raw Normal View History

2022-12-25 20:32:47 +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;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Performance;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Pooling;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
public partial class OsuModBubbles : ModWithVisibilityAdjustment, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToScoreProcessor
{
public override string Name => "Bubbles";
public override string Acronym => "BB";
public override LocalisableString Description => "Dont let their popping distract you!";
public override double ScoreMultiplier => 1;
public override ModType Type => ModType.Fun;
// Compatibility with these seems potentially feasible in the future, blocked for now because they dont work as one would expect
public override Type[] IncompatibleMods => new[] { typeof(OsuModBarrelRoll), typeof(OsuModMagnetised), typeof(OsuModRepel) };
private PlayfieldAdjustmentContainer adjustmentContainer = null!;
private BubbleContainer bubbleContainer = null!;
private readonly Bindable<int> currentCombo = new BindableInt();
private float maxSize;
private float bubbleRadius;
private double bubbleFade;
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
{
currentCombo.BindTo(scoreProcessor.Combo);
currentCombo.BindValueChanged(combo =>
maxSize = Math.Min(1.75f, (float)(1.25 + 0.005 * combo.NewValue)), true);
}
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
// Multiplying by 2 results in an initial size that is too large, hence 1.85 has been chosen
bubbleRadius = (float)(drawableRuleset.Beatmap.HitObjects.OfType<HitCircle>().First().Radius * 1.85f);
bubbleFade = drawableRuleset.Beatmap.HitObjects.OfType<HitCircle>().First().TimePreempt * 2;
2022-12-25 20:32:47 +00:00
// We want to hide the judgements since they are obscured by the BubbleDrawable (due to layering)
drawableRuleset.Playfield.DisplayJudgements.Value = false;
adjustmentContainer = drawableRuleset.CreatePlayfieldAdjustmentContainer();
adjustmentContainer.Add(bubbleContainer = new BubbleContainer());
drawableRuleset.KeyBindingInputManager.Add(adjustmentContainer);
}
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) => applyBubbleState(hitObject);
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) => applyBubbleState(hitObject);
private void applyBubbleState(DrawableHitObject drawableObject)
{
if (drawableObject is not DrawableOsuHitObject drawableOsuObject || !drawableObject.Judged) return;
OsuHitObject hitObject = drawableOsuObject.HitObject;
switch (drawableOsuObject)
{
//Needs to be done explicitly to avoid being handled by DrawableHitCircle below
case DrawableSliderHead:
2022-12-25 20:33:10 +00:00
addBubbleContainer(hitObject.Position, drawableOsuObject);
2022-12-25 20:32:47 +00:00
break;
//Stack leniency causes placement issues if this isn't handled as such.
case DrawableHitCircle hitCircle:
2022-12-25 20:33:10 +00:00
addBubbleContainer(hitCircle.Position, drawableOsuObject);
2022-12-25 20:32:47 +00:00
break;
case DrawableSpinnerTick:
case DrawableSlider:
return;
default:
2022-12-25 20:33:10 +00:00
addBubbleContainer(hitObject.Position, drawableOsuObject);
2022-12-25 20:32:47 +00:00
break;
}
2022-12-25 20:33:10 +00:00
}
2022-12-25 20:32:47 +00:00
2022-12-25 20:33:10 +00:00
private void addBubbleContainer(Vector2 position, DrawableHitObject hitObject)
{
bubbleContainer.Add
(
new BubbleLifeTimeEntry
2022-12-25 20:32:47 +00:00
{
LifetimeStart = bubbleContainer.Time.Current,
2022-12-25 20:33:10 +00:00
Colour = hitObject.AccentColour.Value,
2022-12-25 20:32:47 +00:00
Position = position,
InitialSize = new Vector2(bubbleRadius),
MaxSize = maxSize,
FadeTime = bubbleFade,
2022-12-25 20:33:10 +00:00
IsHit = hitObject.IsHit
2022-12-25 20:32:47 +00:00
}
);
}
#region Pooled Bubble drawable
// LifetimeEntry flow is necessary to allow for correct rewind behaviour, can probably be made generic later if more mods are made requiring it
// Todo: find solution to bubbles rewinding in "groups"
2022-12-25 20:32:47 +00:00
private sealed partial class BubbleContainer : PooledDrawableWithLifetimeContainer<BubbleLifeTimeEntry, BubbleObject>
{
protected override bool RemoveRewoundEntry => true;
private readonly DrawablePool<BubbleObject> pool;
public BubbleContainer()
{
RelativeSizeAxes = Axes.Both;
AddInternal(pool = new DrawablePool<BubbleObject>(10, 1000));
}
protected override BubbleObject GetDrawable(BubbleLifeTimeEntry entry) => pool.Get(d => d.Apply(entry));
}
private sealed partial class BubbleObject : PoolableDrawableWithLifetime<BubbleLifeTimeEntry>
{
private readonly BubbleDrawable bubbleDrawable;
public BubbleObject()
{
InternalChild = bubbleDrawable = new BubbleDrawable();
}
protected override void OnApply(BubbleLifeTimeEntry entry)
{
base.OnApply(entry);
if (IsLoaded)
apply(entry);
}
protected override void LoadComplete()
{
base.LoadComplete();
apply(Entry);
}
private void apply(BubbleLifeTimeEntry? entry)
{
if (entry == null) return;
2022-12-25 20:32:47 +00:00
ApplyTransformsAt(float.MinValue, true);
ClearTransforms(true);
Position = entry.Position;
bubbleDrawable.Animate(entry);
LifetimeEnd = bubbleDrawable.LatestTransformEndTime;
}
}
private partial class BubbleDrawable : CircularContainer
2022-12-25 20:32:47 +00:00
{
private readonly Circle innerCircle;
private readonly Box colourBox;
2023-01-11 16:51:41 +00:00
public BubbleDrawable()
2022-12-25 20:32:47 +00:00
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Masking = true;
MaskingSmoothness = 2;
BorderThickness = 0;
BorderColour = Colour4.Transparent;
EdgeEffect = new EdgeEffectParameters
2022-12-25 20:32:47 +00:00
{
Type = EdgeEffectType.Shadow,
Radius = 3,
Colour = Colour4.Black.Opacity(0.05f)
};
Children = new Drawable[]
{
colourBox = new Box { RelativeSizeAxes = Axes.Both, },
2023-01-11 16:51:41 +00:00
innerCircle = new Circle
{
Anchor = Anchor.Centre,
2023-01-11 16:51:41 +00:00
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.5f),
2022-12-25 20:32:47 +00:00
}
};
}
public void Animate(BubbleLifeTimeEntry entry)
{
Size = entry.InitialSize;
this.ScaleTo(entry.MaxSize, getAnimationTime() * 0.8f, Easing.OutSine)
2022-12-25 20:32:47 +00:00
.Then()
.ScaleTo(entry.MaxSize * 1.5f, getAnimationTime() * 0.2f, Easing.OutSine)
.FadeTo(0, getAnimationTime() * 0.2f, Easing.OutExpo);
2023-01-11 16:51:41 +00:00
colourBox.FadeColour(entry.IsHit ? entry.Colour : Colour4.Black, getAnimationTime() * 0.1f, Easing.OutSine)
.Then()
.FadeColour(entry.IsHit ? entry.Colour.Darken(0.4f) : Colour4.Black, getAnimationTime() * 0.9f, Easing.OutSine);
2023-01-11 16:51:41 +00:00
innerCircle.FadeColour(entry.IsHit ? entry.Colour.Darken(0.2f) : Colour4.Black)
.FadeColour(entry.IsHit ? entry.Colour.Lighten(0.2f) : Colour4.Black, getAnimationTime());
2023-01-11 16:51:41 +00:00
innerCircle.FadeTo(0.5f, getAnimationTime(), Easing.InExpo)
.ScaleTo(2.2f, getAnimationTime(), Easing.InSine);
2023-01-11 16:51:41 +00:00
// The absolute length of the bubble's animation, can be used in fractions for animations of partial length
double getAnimationTime() => 2000 + 450 / (450 / entry.FadeTime);
2022-12-25 20:32:47 +00:00
}
}
private class BubbleLifeTimeEntry : LifetimeEntry
{
public Vector2 InitialSize { get; set; }
public float MaxSize { get; set; }
public Vector2 Position { get; set; }
public Colour4 Colour { get; set; }
// FadeTime is based on the approach rate of the beatmap.
public double FadeTime { get; set; }
// Whether the corresponding HitObject was hit
public bool IsHit { get; set; }
}
#endregion
}
}