diff --git a/fluXis.Game/Graphics/UserInterface/Color/FluXisColors.cs b/fluXis.Game/Graphics/UserInterface/Color/FluXisColors.cs index 209d33f8..03c81a89 100644 --- a/fluXis.Game/Graphics/UserInterface/Color/FluXisColors.cs +++ b/fluXis.Game/Graphics/UserInterface/Color/FluXisColors.cs @@ -47,6 +47,7 @@ public static class FluXisColors public static Colour4 PlayfieldRotate => Colour4.FromHex("#8AF7A2"); public static Colour4 PlayfieldFade => Colour4.FromHex("#0180FE"); public static Colour4 HitObjectFade => Colour4.FromHex("#8AF3F7"); + public static Colour4 HitObjectEase => Colour4.FromHex("#5B92FF"); public static Colour4 BeatPulse => Colour4.FromHex("#FF6666"); public static Colour4 Note => Colour4.FromHex("#FFFFFF"); public static Colour4 Shader => Colour4.FromHex("#D65C5C"); diff --git a/fluXis.Game/Map/MapEvents.cs b/fluXis.Game/Map/MapEvents.cs index b292411c..445a8bf1 100644 --- a/fluXis.Game/Map/MapEvents.cs +++ b/fluXis.Game/Map/MapEvents.cs @@ -36,6 +36,9 @@ public class MapEvents : IDeepCloneable [JsonProperty("hitfade")] public List HitObjectFadeEvents { get; private set; } = new(); + [JsonProperty("hitease")] + public List HitObjectEaseEvents { get; private set; } = new(); + [JsonProperty("shake")] public List ShakeEvents { get; private set; } = new(); @@ -57,6 +60,7 @@ public class MapEvents : IDeepCloneable && PlayfieldRotateEvents.Count == 0 && PlayfieldFadeEvents.Count == 0 && HitObjectFadeEvents.Count == 0 + && HitObjectEaseEvents.Count == 0 && ShakeEvents.Count == 0 && ShaderEvents.Count == 0 && BeatPulseEvents.Count == 0 @@ -222,6 +226,7 @@ public MapEvents Sort() PlayfieldScaleEvents.Sort(compare); PlayfieldFadeEvents.Sort(compare); HitObjectFadeEvents.Sort(compare); + HitObjectEaseEvents.Sort(compare); ShakeEvents.Sort(compare); ShaderEvents.Sort(compare); BeatPulseEvents.Sort(compare); @@ -252,6 +257,7 @@ public MapEvents DeepClone() clone.PlayfieldRotateEvents = new List(PlayfieldRotateEvents); clone.PlayfieldFadeEvents = new List(PlayfieldFadeEvents); clone.HitObjectFadeEvents = new List(HitObjectFadeEvents); + clone.HitObjectEaseEvents = new List(HitObjectEaseEvents); clone.ShakeEvents = new List(ShakeEvents); clone.ShaderEvents = new List(ShaderEvents); clone.BeatPulseEvents = new List(BeatPulseEvents); diff --git a/fluXis.Game/Map/Structures/Events/HitObjectEaseEvent.cs b/fluXis.Game/Map/Structures/Events/HitObjectEaseEvent.cs new file mode 100644 index 00000000..14f62e7a --- /dev/null +++ b/fluXis.Game/Map/Structures/Events/HitObjectEaseEvent.cs @@ -0,0 +1,14 @@ +using fluXis.Game.Map.Structures.Bases; +using Newtonsoft.Json; +using osu.Framework.Graphics; + +namespace fluXis.Game.Map.Structures.Events; + +public class HitObjectEaseEvent : IMapEvent, IHasEasing +{ + [JsonProperty("time")] + public double Time { get; set; } + + [JsonProperty("ease")] + public Easing Easing { get; set; } +} diff --git a/fluXis.Game/Screens/Edit/EditorMap.cs b/fluXis.Game/Screens/Edit/EditorMap.cs index 6e6a31dc..79090ff7 100644 --- a/fluXis.Game/Screens/Edit/EditorMap.cs +++ b/fluXis.Game/Screens/Edit/EditorMap.cs @@ -80,6 +80,10 @@ public class EditorMap public event Action HitObjectFadeEventRemoved; public event Action HitObjectFadeEventUpdated; + public event Action HitObjectEaseEventAdded; + public event Action HitObjectEaseEventRemoved; + public event Action HitObjectEaseEventUpdated; + public event Action ShaderEventAdded; public event Action ShaderEventRemoved; public event Action ShaderEventUpdated; @@ -242,6 +246,11 @@ public void Add(ITimedObject obj) HitObjectFadeEventAdded?.Invoke(hitFade); break; + case HitObjectEaseEvent hitEase: + MapEvents.HitObjectEaseEvents.Add(hitEase); + HitObjectEaseEventAdded?.Invoke(hitEase); + break; + case ShaderEvent shaderEvent: MapEvents.ShaderEvents.Add(shaderEvent); ShaderEventAdded?.Invoke(shaderEvent); @@ -311,6 +320,10 @@ public void Update(ITimedObject obj) HitObjectFadeEventUpdated?.Invoke(hitFade); break; + case HitObjectEaseEvent hitEase: + HitObjectEaseEventUpdated?.Invoke(hitEase); + break; + case ShaderEvent shaderEvent: ShaderEventUpdated?.Invoke(shaderEvent); break; @@ -388,6 +401,11 @@ public void Remove(ITimedObject obj) HitObjectFadeEventRemoved?.Invoke(hitFade); break; + case HitObjectEaseEvent hitEase: + MapEvents.HitObjectEaseEvents.Remove(hitEase); + HitObjectEaseEventRemoved?.Invoke(hitEase); + break; + case ShaderEvent shaderEvent: MapEvents.ShaderEvents.Remove(shaderEvent); ShaderEventRemoved?.Invoke(shaderEvent); @@ -475,6 +493,12 @@ public void ApplyOffsetToAll(float offset) Update(hitFade); } + foreach (var hitEase in MapEvents.HitObjectEaseEvents) + { + hitEase.Time += offset; + Update(hitEase); + } + foreach (var shaderEvent in MapEvents.ShaderEvents) { shaderEvent.Time += offset; diff --git a/fluXis.Game/Screens/Edit/Tabs/Design/Points/DesignPointsList.cs b/fluXis.Game/Screens/Edit/Tabs/Design/Points/DesignPointsList.cs index 19070331..3483f9ab 100644 --- a/fluXis.Game/Screens/Edit/Tabs/Design/Points/DesignPointsList.cs +++ b/fluXis.Game/Screens/Edit/Tabs/Design/Points/DesignPointsList.cs @@ -41,6 +41,11 @@ protected override void RegisterEvents() Map.HitObjectFadeEventRemoved += RemovePoint; Map.MapEvents.HitObjectFadeEvents.ForEach(AddPoint); + Map.HitObjectEaseEventAdded += AddPoint; + Map.HitObjectEaseEventUpdated += UpdatePoint; + Map.HitObjectEaseEventRemoved += RemovePoint; + Map.MapEvents.HitObjectEaseEvents.ForEach(AddPoint); + Map.ShaderEventAdded += AddPoint; Map.ShaderEventUpdated += UpdatePoint; Map.ShaderEventRemoved += RemovePoint; @@ -71,7 +76,8 @@ protected override PointListEntry CreateEntryFor(ITimedObject obj) PlayfieldMoveEvent move => new PlayfieldMoveEntry(move), PlayfieldFadeEvent fade => new PlayfieldFadeEntry(fade), PlayfieldScaleEvent scale => new PlayfieldScaleEntry(scale), - HitObjectFadeEvent scale => new HitObjectFadeEntry(scale), + HitObjectFadeEvent fade => new HitObjectFadeEntry(fade), + HitObjectEaseEvent ease => new HitObjectEaseEntry(ease), BeatPulseEvent pulse => new BeatPulseEntry(pulse), PlayfieldRotateEvent rotate => new PlayfieldRotateEntry(rotate), ShaderEvent shader => new ShaderEntry(shader), @@ -91,6 +97,7 @@ protected override IEnumerable CreateAddEntries() new("Playfield Rotate", FluXisColors.PlayfieldRotate, () => Create(new PlayfieldRotateEvent())), new("Playfield Fade", FluXisColors.PlayfieldFade, () => Create(new PlayfieldFadeEvent())), new("HitObject Fade", FluXisColors.HitObjectFade, () => Create(new HitObjectFadeEvent())), + new("HitObject Ease", FluXisColors.HitObjectEase, () => Create(new HitObjectEaseEvent())), new("Beat Pulse", FluXisColors.BeatPulse, () => Create(new BeatPulseEvent())), new("Note", FluXisColors.Note, () => Create(new NoteEvent())), new("Shader", FluXisColors.Shader, () => Create(new ShaderEvent { ShaderName = "Bloom" })) diff --git a/fluXis.Game/Screens/Edit/Tabs/Design/Points/Entries/HitObjectEaseEntry.cs b/fluXis.Game/Screens/Edit/Tabs/Design/Points/Entries/HitObjectEaseEntry.cs new file mode 100644 index 00000000..1871db76 --- /dev/null +++ b/fluXis.Game/Screens/Edit/Tabs/Design/Points/Entries/HitObjectEaseEntry.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using fluXis.Game.Graphics.Sprites; +using fluXis.Game.Graphics.UserInterface.Color; +using fluXis.Game.Map.Structures.Bases; +using fluXis.Game.Map.Structures.Events; +using fluXis.Game.Screens.Edit.Tabs.Shared.Points.List; +using fluXis.Game.Screens.Edit.Tabs.Shared.Points.Settings.Preset; +using osu.Framework.Graphics; + +namespace fluXis.Game.Screens.Edit.Tabs.Design.Points.Entries; + +public partial class HitObjectEaseEntry : PointListEntry +{ + protected override string Text => "HitObject Ease"; + protected override Colour4 Color => FluXisColors.HitObjectEase; + + private HitObjectEaseEvent ease => Object as HitObjectEaseEvent; + + public HitObjectEaseEntry(HitObjectEaseEvent obj) + : base(obj) + { + } + + public override ITimedObject CreateClone() => new HitObjectEaseEvent + { + Time = Object.Time, + Easing = ease.Easing + }; + + protected override Drawable[] CreateValueContent() + { + return new Drawable[] + { + new FluXisSpriteText + { + Text = $"{ease.Easing}", + Colour = Color + } + }; + } + + protected override IEnumerable CreateSettings() + { + return base.CreateSettings().Concat(new Drawable[] + { + new PointSettingsEasing(Map, ease) + }); + } +} diff --git a/fluXis.Game/Screens/Gameplay/Ruleset/HitObjects/DrawableHitObject.cs b/fluXis.Game/Screens/Gameplay/Ruleset/HitObjects/DrawableHitObject.cs index c1cae11a..026bff3c 100644 --- a/fluXis.Game/Screens/Gameplay/Ruleset/HitObjects/DrawableHitObject.cs +++ b/fluXis.Game/Screens/Gameplay/Ruleset/HitObjects/DrawableHitObject.cs @@ -28,6 +28,8 @@ public partial class DrawableHitObject : CompositeDrawable protected double ScrollVelocityTime { get; private set; } protected double ScrollVelocityEndTime { get; private set; } + private Easing easing = Easing.None; + public FluXisGameplayKeybind Keybind { get; set; } public virtual bool CanBeRemoved => false; @@ -49,6 +51,7 @@ private void load() ScrollVelocityTime = ObjectManager.ScrollVelocityPositionFromTime(Data.Time); ScrollVelocityEndTime = ObjectManager.ScrollVelocityPositionFromTime(Data.EndTime); + easing = ObjectManager.EasingAtTime(Data.Time); } protected override void LoadComplete() @@ -64,7 +67,7 @@ protected override void Update() base.Update(); X = ObjectManager.PositionAtLane(Data.Lane); - Y = ObjectManager.PositionAtTime(ScrollVelocityTime); + Y = ObjectManager.PositionAtTime(ScrollVelocityTime, easing); Width = ObjectManager.WidthOfLane(Data.Lane); } diff --git a/fluXis.Game/Screens/Gameplay/Ruleset/HitObjects/DrawableLongNote.cs b/fluXis.Game/Screens/Gameplay/Ruleset/HitObjects/DrawableLongNote.cs index b709ec01..74f2df41 100644 --- a/fluXis.Game/Screens/Gameplay/Ruleset/HitObjects/DrawableLongNote.cs +++ b/fluXis.Game/Screens/Gameplay/Ruleset/HitObjects/DrawableLongNote.cs @@ -28,6 +28,8 @@ public override bool CanBeRemoved private DrawableLongNoteTail tailPiece; private DrawableLongNoteHead headPiece; + private Easing endEasing = Easing.None; + private bool missed; private readonly Colour4 missTint = new(.4f, .4f, .4f, 1); @@ -51,6 +53,8 @@ private void load() headPiece = new DrawableLongNoteHead(Data) }; + endEasing = ObjectManager.EasingAtTime(Data.EndTime); + if (ObjectManager.UseSnapColors) { var startIdx = ObjectManager.GetSnapIndex(Data.Time); @@ -102,7 +106,7 @@ protected override void Update() if (IsBeingHeld.Value) Y = ObjectManager.HitPosition; - var endY = ObjectManager.PositionAtTime(ScrollVelocityEndTime); + var endY = ObjectManager.PositionAtTime(ScrollVelocityEndTime, endEasing); var height = Y - endY; bodyPiece.Height = height; diff --git a/fluXis.Game/Screens/Gameplay/Ruleset/HitObjects/HitObjectManager.cs b/fluXis.Game/Screens/Gameplay/Ruleset/HitObjects/HitObjectManager.cs index c7f336f3..c743bcae 100644 --- a/fluXis.Game/Screens/Gameplay/Ruleset/HitObjects/HitObjectManager.cs +++ b/fluXis.Game/Screens/Gameplay/Ruleset/HitObjects/HitObjectManager.cs @@ -15,6 +15,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; namespace fluXis.Game.Screens.Gameplay.Ruleset.HitObjects; @@ -220,7 +221,19 @@ protected override void Update() public float HitPosition => DrawHeight - laneSwitchManager.HitPosition; public bool ShouldDisplay(double time) => ScrollVelocityPositionFromTime(time) <= ScrollVelocityPositionFromTime(Clock.CurrentTime) + DrawHeight * screen.Rate; - public float PositionAtTime(double time) => (float)(HitPosition - .5f * ((time - (float)CurrentTime) * ScrollSpeed)); + + public float PositionAtTime(double time, Easing ease = Easing.None) + { + var pos = HitPosition; + var y = (float)(pos - .5f * ((time - (float)CurrentTime) * ScrollSpeed)); + + if (ease <= Easing.None || y < 0 || y > pos) + return y; + + var progress = y / pos; + y = Interpolation.ValueAt(progress, 0, pos, 0, 1, ease); + return float.IsFinite(y) ? y : 0; + } public float PositionAtLane(int lane) { @@ -238,6 +251,17 @@ public float PositionAtLane(int lane) return x; } + public Easing EasingAtTime(double time) + { + var events = screen.MapEvents.HitObjectEaseEvents; + + if (events.Count == 0) + return Easing.None; + + var first = events.LastOrDefault(e => e.Time <= time); + return first?.Easing ?? Easing.None; + } + public float WidthOfLane(int lane) => laneSwitchManager.WidthFor(lane); public bool IsFirstInColumn(DrawableHitObject hitObject) => HitObjects.FirstOrDefault(h => h.Data.Lane == hitObject.Data.Lane && h.Data.Time < hitObject.Data.Time) == null; diff --git a/fluXis.Game/Screens/Gameplay/Ruleset/TimingLines/TimingLine.cs b/fluXis.Game/Screens/Gameplay/Ruleset/TimingLines/TimingLine.cs index 7ec50d2c..ac639554 100644 --- a/fluXis.Game/Screens/Gameplay/Ruleset/TimingLines/TimingLine.cs +++ b/fluXis.Game/Screens/Gameplay/Ruleset/TimingLines/TimingLine.cs @@ -11,6 +11,7 @@ public partial class TimingLine : Box public double OriginalTime { get; } private double scrollVelocityTime; + private Easing easing = Easing.None; public TimingLine(double time) { @@ -25,10 +26,11 @@ private void load() Origin = Anchor.BottomLeft; scrollVelocityTime = playfield.Manager.ScrollVelocityPositionFromTime(OriginalTime); + easing = playfield.Manager.EasingAtTime(OriginalTime); } protected override void Update() { - Y = playfield.Manager.PositionAtTime(scrollVelocityTime); + Y = playfield.Manager.PositionAtTime(scrollVelocityTime, easing); } }