// Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using OpenTK; using OpenTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Colour; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using System; using System.Linq; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Mania.UI { public class Column : ScrollingPlayfield, IKeyBindingHandler, IHasAccentColour { private const float key_icon_size = 10; private const float key_icon_corner_radius = 3; private const float key_icon_border_radius = 2; private const float hit_target_height = 10; private const float hit_target_bar_height = 2; private const float column_width = 45; private const float special_column_width = 70; public ManiaAction Action; private readonly Box background; private readonly Container hitTargetBar; private readonly Container keyIcon; internal readonly Container TopLevelContainer; private readonly Container explosionContainer; protected override Container Content => content; private readonly Container content; private const float opacity_released = 0.1f; private const float opacity_pressed = 0.25f; public Column() : base(ScrollingDirection.Up) { RelativeSizeAxes = Axes.Y; Width = column_width; InternalChildren = new Drawable[] { background = new Box { Name = "Background", RelativeSizeAxes = Axes.Both, Alpha = opacity_released }, new Container { Name = "Hit target + hit objects", RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Top = ManiaStage.HIT_TARGET_POSITION }, Children = new Drawable[] { new Container { Name = "Hit target", RelativeSizeAxes = Axes.X, Height = hit_target_height, Children = new Drawable[] { new Box { Name = "Background", RelativeSizeAxes = Axes.Both, Colour = Color4.Black }, hitTargetBar = new Container { Name = "Bar", RelativeSizeAxes = Axes.X, Height = hit_target_bar_height, Masking = true, Children = new[] { new Box { RelativeSizeAxes = Axes.Both } } } } }, content = new Container { Name = "Hit objects", RelativeSizeAxes = Axes.Both, }, // For column lighting, we need to capture input events before the notes new InputTarget { Pressed = onPressed, Released = onReleased }, explosionContainer = new Container { Name = "Hit explosions", RelativeSizeAxes = Axes.Both } } }, new Container { Name = "Key", RelativeSizeAxes = Axes.X, Height = ManiaStage.HIT_TARGET_POSITION, Children = new Drawable[] { new Box { Name = "Key gradient", RelativeSizeAxes = Axes.Both, Colour = ColourInfo.GradientVertical(Color4.Black, Color4.Black.Opacity(0)), Alpha = 0.5f }, keyIcon = new Container { Name = "Key icon", Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(key_icon_size), Masking = true, CornerRadius = key_icon_corner_radius, BorderThickness = 2, BorderColour = Color4.White, // Not true Children = new[] { new Box { RelativeSizeAxes = Axes.Both, Alpha = 0, AlwaysPresent = true } } } } }, TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both } }; TopLevelContainer.Add(explosionContainer.CreateProxy()); } public override Axes RelativeSizeAxes => Axes.Y; private bool isSpecial; public bool IsSpecial { get { return isSpecial; } set { if (isSpecial == value) return; isSpecial = value; Width = isSpecial ? special_column_width : column_width; } } private Color4 accentColour; public Color4 AccentColour { get { return accentColour; } set { if (accentColour == value) return; accentColour = value; background.Colour = accentColour; hitTargetBar.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Radius = 5, Colour = accentColour.Opacity(0.5f), }; keyIcon.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Radius = 5, Colour = accentColour.Opacity(0.5f), }; } } /// /// Adds a DrawableHitObject to this Playfield. /// /// The DrawableHitObject to add. public override void Add(DrawableHitObject hitObject) { hitObject.AccentColour = AccentColour; hitObject.OnJudgement += OnJudgement; HitObjects.Add(hitObject); } internal void OnJudgement(DrawableHitObject judgedObject, Judgement judgement) { if (!judgement.IsHit) return; explosionContainer.Add(new HitExplosion(judgedObject)); } private bool onPressed(ManiaAction action) { if (action == Action) { background.FadeTo(opacity_pressed, 50, Easing.OutQuint); keyIcon.ScaleTo(1.4f, 50, Easing.OutQuint); } return false; } private bool onReleased(ManiaAction action) { if (action == Action) { background.FadeTo(opacity_released, 800, Easing.OutQuart); keyIcon.ScaleTo(1f, 400, Easing.OutQuart); } return false; } /// /// This is a simple container which delegates various input events that have to be captured before the notes. /// private class InputTarget : Container, IKeyBindingHandler { public Func Pressed; public Func Released; public InputTarget() { RelativeSizeAxes = Axes.Both; AlwaysPresent = true; Alpha = 0; } public bool OnPressed(ManiaAction action) => Pressed?.Invoke(action) ?? false; public bool OnReleased(ManiaAction action) => Released?.Invoke(action) ?? false; } public bool OnPressed(ManiaAction action) { // Play the sounds of the next hitobject if (HitObjects.AliveObjects.Any()) { // If there are alive hitobjects, we can abuse the fact that AliveObjects are sorted by time (see: Add()) HitObjects.AliveObjects.First().PlaySamples(); } else { // If not, we do a slow search - we might want to do a BinarySearch here if this becomes problematic // We fallback to LastOrDefault() if we're beyond the last note in the map var hitObject = HitObjects.Objects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ?? HitObjects.Objects.LastOrDefault(); hitObject?.PlaySamples(); } return false; } public bool OnReleased(ManiaAction action) => false; } }