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.

234 lines
8.6 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.Allocation;
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().TimeFadeIn * 2;
// 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"
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;
ApplyTransformsAt(float.MinValue, true);
ClearTransforms(true);
Position = entry.Position;
bubbleDrawable.Animate(entry);
LifetimeEnd = bubbleDrawable.LatestTransformEndTime;
}
}
private partial class BubbleDrawable : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new Circle
{
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Colour = Colour4.Black.Opacity(0.05f),
Type = EdgeEffectType.Shadow,
Radius = 5
}
};
}
public void Animate(BubbleLifeTimeEntry entry)
{
Size = entry.InitialSize;
this
.ScaleTo(entry.MaxSize, entry.FadeTime * 6, Easing.OutSine)
.FadeColour(entry.IsHit ? entry.Colour : Colour4.Black, entry.FadeTime, Easing.OutSine)
.Delay(entry.FadeTime)
.FadeColour(entry.IsHit ? entry.Colour.Darken(.4f) : Colour4.Black, entry.FadeTime * 5, Easing.OutSine)
.Then()
.ScaleTo(entry.MaxSize * 1.5f, entry.FadeTime, Easing.OutSine)
.FadeTo(0, entry.FadeTime, Easing.OutQuint);
}
}
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
}
}