// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.UI { public class TaikoPlayfield : ScrollingPlayfield { private readonly ControlPointInfo controlPoints; /// /// Default height of a when inside a . /// public const float DEFAULT_HEIGHT = 178; private Container hitExplosionContainer; private Container kiaiExplosionContainer; private JudgementContainer judgementContainer; private ScrollingHitObjectContainer drumRollHitContainer; internal Drawable HitTarget; private ProxyContainer topLevelHitContainer; private ProxyContainer barlineContainer; private Container rightArea; private Container leftArea; private Container hitTargetOffsetContent; public TaikoPlayfield(ControlPointInfo controlPoints) { this.controlPoints = controlPoints; } [BackgroundDependencyLoader] private void load(OsuColour colours) { InternalChildren = new[] { new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundRight), _ => new PlayfieldBackgroundRight()), rightArea = new Container { Name = "Right area", RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.Both, Children = new Drawable[] { new Container { Name = "Masked elements before hit objects", RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, Children = new[] { hitExplosionContainer = new Container { RelativeSizeAxes = Axes.Both, Blending = BlendingParameters.Additive, }, HitTarget = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.HitTarget), _ => new TaikoHitTarget()) { RelativeSizeAxes = Axes.Both, } } }, hitTargetOffsetContent = new Container { RelativeSizeAxes = Axes.Both, Children = new Drawable[] { barlineContainer = new ProxyContainer { RelativeSizeAxes = Axes.Both, }, new Container { Name = "Hit objects", RelativeSizeAxes = Axes.Both, Children = new Drawable[] { HitObjectContainer, drumRollHitContainer = new ScrollingHitObjectContainer { Name = "Drumroll hit" } } }, kiaiExplosionContainer = new Container { Name = "Kiai hit explosions", RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, Blending = BlendingParameters.Additive }, judgementContainer = new JudgementContainer { Name = "Judgements", RelativeSizeAxes = Axes.Y, Blending = BlendingParameters.Additive }, } }, } }, leftArea = new Container { Name = "Left overlay", RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, BorderColour = colours.Gray0, Children = new Drawable[] { new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundLeft), _ => new PlayfieldBackgroundLeft()), new InputDrum(controlPoints) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, } }, topLevelHitContainer = new ProxyContainer { Name = "Top level hit objects", RelativeSizeAxes = Axes.Both, }, drumRollHitContainer.CreateProxy() }; } protected override void Update() { base.Update(); // Padding is required to be updated for elements which are based on "absolute" X sized elements. // This is basically allowing for correct alignment as relative pieces move around them. rightArea.Padding = new MarginPadding { Left = leftArea.DrawWidth }; hitTargetOffsetContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 }; // When rewinding, make sure to remove any auxilliary hit notes that were // spawned and played during a drumroll. if (Time.Elapsed < 0) { foreach (var o in drumRollHitContainer.Objects) { if (o.HitObject.StartTime >= Time.Current) drumRollHitContainer.Remove(o); } } } public override void Add(DrawableHitObject h) { h.OnNewResult += OnNewResult; base.Add(h); switch (h) { case DrawableBarLine barline: barlineContainer.Add(barline.CreateProxy()); break; case DrawableTaikoHitObject taikoObject: topLevelHitContainer.Add(taikoObject.CreateProxiedContent()); break; } } internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) { if (!DisplayJudgements.Value) return; if (!judgedObject.DisplayResult) return; switch (result.Judgement) { case TaikoStrongJudgement _: if (result.IsHit) hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).MainObject)?.VisualiseSecondHit(); break; case TaikoDrumRollTickJudgement _: if (!result.IsHit) return; var drawableTick = (DrawableDrumRollTick)judgedObject; addDrumRollHit(drawableTick); addExplosion(drawableTick, drawableTick.JudgementType); break; default: judgementContainer.Add(new DrawableTaikoJudgement(result, judgedObject) { Anchor = result.IsHit ? Anchor.TopLeft : Anchor.CentreLeft, Origin = result.IsHit ? Anchor.BottomCentre : Anchor.Centre, RelativePositionAxes = Axes.X, X = result.IsHit ? judgedObject.Position.X : 0, }); if (!result.IsHit) break; addExplosion(judgedObject, (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre); break; } } private void addDrumRollHit(DrawableDrumRollTick drawableTick) { bool isStrong = drawableTick.HitObject.IsStrong; double time = drawableTick.HitObject.GetEndTime(); DrawableHit drawableHit; if (drawableTick.JudgementType == HitType.Rim) drawableHit = new DrawableFlyingRimHit(time, isStrong); else drawableHit = new DrawableFlyingCentreHit(time, isStrong); drumRollHitContainer.Add(drawableHit); } private void addExplosion(DrawableHitObject drawableObject, HitType type) { hitExplosionContainer.Add(new HitExplosion(drawableObject, type)); if (drawableObject.HitObject.Kiai) kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); } private class ProxyContainer : LifetimeManagementContainer { public new MarginPadding Padding { set => base.Padding = value; } public void Add(Drawable proxy) => AddInternal(proxy); } } }