using RhythmBase.Components; using RhythmBase.Components.Easing; using RhythmBase.Events; using RhythmBase.Exceptions; using RhythmBase.Extensions; using System.Text.RegularExpressions; namespace RhythmBase.Extensions { public static partial class Extensions { /// /// Get the pulse sound effect of row beat event. /// /// The sound effect of row beat event. public static RDAudio BeatSound(this BaseBeat e) { SetBeatSound? setBeatSound = e.Parent?.LastOrDefault((SetBeatSound i) => i.Beat < e.Beat && i.Active); return (setBeatSound?.Sound) ?? e.Parent?.Sound ?? throw new NotImplementedException(); } /// /// Getting controlled events. /// public static IEnumerable> ControllingEvents(this TagAction e) => e.Beat.BaseLevel?.GetTaggedEvents(e.ActionTag, e.Action.HasFlag(TagAction.Actions.All)) ?? []; /// /// Creates a new subordinate to at the specified beat. The new event created will be attempted to be added to the 's source level. /// /// RDLevel /// Specified beat. public static AdvanceText CreateAdvanceText(this FloatingText e, RDBeat beat) { AdvanceText A = new() { Parent = e, Beat = beat.WithoutBinding() }; e.Children.Add(A); return A; } /// /// Get the end beat of the duration. /// /// /// /// /// public static RDBeat DurationOffset(this RDBeat beat, float duration) { SetBeatsPerMinute setBPM = beat.BaseLevel?.First((SetBeatsPerMinute i) => i.Beat > beat) ?? throw new InvalidRDBeatException(); RDBeat DurationOffset = beat.BarBeat.bar == setBPM.Beat.BarBeat.bar ? beat + duration : beat + TimeSpan.FromMinutes(duration / beat.BPM); return DurationOffset; } /// /// Returns the pulse beat of the specified 0-based index. /// /// THIS IS 7TH BEAT GAMES! public static RDBeat GetBeat(this AddClassicBeat e, byte index) { SetRowXs x = e.Parent?.LastOrDefault((SetRowXs i) => i.Active && e.IsBehind(i)) ?? new(); float Synco = 0 <= x.SyncoBeat && x.SyncoBeat < (sbyte)index ? (float)((x.SyncoSwing == 0f) ? 0.5 : ((double)x.SyncoSwing)) : 0f; if (index >= 7) throw new RhythmBaseException("THIS IS 7TH BEAT GAMES!"); return e.Beat.DurationOffset(e.Tick * ((float)index - Synco)); } /// /// Converts Xs patterns to string form. /// public static string GetPatternString(this SetRowXs e) => Utils.Utils.GetPatternString(e.Pattern); /// /// Get the sequence of belonging to this , return all of the from the time the pulse was created to the time it was removed or hit. /// public static IEnumerable GetPulses(this AddFreeTimeBeat e) { List Result = []; byte pulse = e.Pulse; if (e.Parent == null) yield break; foreach (PulseFreeTimeBeat item in e.Parent.Where(i => i.Active && e.IsInFrontOf(i))) { switch (item.Action) { case PulseFreeTimeBeat.ActionType.Increment: pulse += 1; yield return item; break; case PulseFreeTimeBeat.ActionType.Decrement: pulse = (byte)((pulse > 0b1) ? (pulse - 0b1) : 0b1); yield return item; break; case PulseFreeTimeBeat.ActionType.Custom: pulse = (byte)item.CustomPulse; yield return item; break; case PulseFreeTimeBeat.ActionType.Remove: yield return item; break; } if (pulse == 6) break; } } /// /// Get the hit sound effect of row beat event. /// /// The sound effect of row beat event. public static RDAudio HitSound(this BaseBeat e) { RDAudio DefaultAudio = new() { Filename = "sndClapHit", Offset = TimeSpan.Zero, Pan = 100, Pitch = 100, Volume = 100 }; RDAudio? HitSound; switch (e.Player()) { case PlayerType.P1: { SetClapSounds? setClapSounds = e.Beat.BaseLevel?.LastOrDefault((SetClapSounds i) => i.Active && i.P1Sound != null); HitSound = (setClapSounds?.P1Sound) ?? DefaultAudio; break; } case PlayerType.P2: { SetClapSounds? setClapSounds2 = e.Beat.BaseLevel?.LastOrDefault((SetClapSounds i) => i.Active && i.P2Sound != null); HitSound = (setClapSounds2?.P2Sound) ?? DefaultAudio; break; } case PlayerType.CPU: { SetClapSounds? setClapSounds3 = e.Beat.BaseLevel?.LastOrDefault((SetClapSounds i) => i.Active && i.CpuSound != null); HitSound = (setClapSounds3?.CpuSound) ?? DefaultAudio; break; } default: HitSound = DefaultAudio; break; } return HitSound; } /// /// Get all hits. /// public static IEnumerable HitTimes(this AddClassicBeat e) => new List { new(e, e.GetBeat(6), e.Hold) } .AsEnumerable(); /// /// Get all hits. /// public static IEnumerable HitTimes(this AddOneshotBeat e) { e._beat.IfNullThrowException(); List L = []; uint loops = e.Loops; for (uint i = 0U; i <= loops; i += 1U) { sbyte b = (sbyte)(e.Subdivisions - 1); for (sbyte j = 0; j <= b; j += 1) L.Add(new RDHit(e, new RDBeat(e._beat._calculator, e._beat.BeatOnly + i * e.Interval + e.Tick + e.Delay + (float)j * (e.Tick / (float)e.Subdivisions)), 0f)); } return L.AsEnumerable(); } /// /// Get all hits. /// public static IEnumerable HitTimes(this AddFreeTimeBeat e) => e.Pulse == 6 ? [new(e, e.Beat, e.Hold)] : ([]); /// /// Get all hits. /// public static IEnumerable HitTimes(this PulseFreeTimeBeat e) => e.IsHitable() ? [new(e, e.Beat, e.Hold)] : ([]); /// /// Get all hits. /// public static IEnumerable HitTimes(this BaseBeat e) => e.Type switch { EventType.AddClassicBeat => ((AddClassicBeat)e).HitTimes(), EventType.AddFreeTimeBeat => ((AddFreeTimeBeat)e).HitTimes(), EventType.AddOneshotBeat => ((AddOneshotBeat)e).HitTimes(), _ => e.Type != EventType.PulseFreeTimeBeat ? Array.Empty().AsEnumerable() : ((PulseFreeTimeBeat)e).HitTimes(), }; /// /// Determine if is after /// /// /// If is after , /// Else, /// public static bool IsBehind(this OrderedEventCollection e, IBaseEvent item1, IBaseEvent item2) => (item1.Beat > item2.Beat || (item1.Beat.BeatOnly == item2.Beat.BeatOnly && (e.eventsBeatOrder[item1.Beat].BeforeThan(item2, item1)))); /// /// Check if another event is after itself, including events of the same beat but executed after itself. /// public static bool IsBehind(this IBaseEvent e, IBaseEvent item) => e.Beat.BaseLevel?.IsBehind(e, item) ?? throw new InvalidRDBeatException(); /// /// Check if it can be hit by player. /// public static bool IsHitable(this PulseFreeTimeBeat e) { int PulseIndexMin = 6; int PulseIndexMax = 6; if (e.Parent is null) return false; foreach (BaseBeat item in ((IEnumerable)e.Parent .Where(e.IsBehind, new RDRange(e.Beat, null))) .Reverse()) { EventType type = item.Type; switch (type) { case EventType.AddFreeTimeBeat: { AddFreeTimeBeat Temp2 = (AddFreeTimeBeat)item; if (PulseIndexMin <= (int)Temp2.Pulse & (int)Temp2.Pulse <= PulseIndexMax) return true; break; } case EventType.PulseFreeTimeBeat: { PulseFreeTimeBeat Temp = (PulseFreeTimeBeat)item; switch (Temp.Action) { case PulseFreeTimeBeat.ActionType.Increment: if (PulseIndexMin > 0) PulseIndexMin--; if (!(PulseIndexMax > 0)) return false; PulseIndexMax--; break; case PulseFreeTimeBeat.ActionType.Decrement: if (PulseIndexMin > 0) PulseIndexMin++; if (!(PulseIndexMax < 6)) return false; PulseIndexMax++; break; case PulseFreeTimeBeat.ActionType.Custom: if (!(PulseIndexMin <= Temp.CustomPulse & Temp.CustomPulse <= PulseIndexMax)) return false; PulseIndexMin = 0; PulseIndexMax = 5; break; case PulseFreeTimeBeat.ActionType.Remove: return false; } if (PulseIndexMin > PulseIndexMax) return false; break; } } } return false; } /// /// Check if it can be hit by player. /// public static bool IsHitable(this AddFreeTimeBeat e) => e.Pulse == 6; /// /// Check if it can be hit by player. /// public static bool IsHitable(this BaseBeat e) => e.Type switch { EventType.AddClassicBeat or EventType.AddOneshotBeat => true, EventType.AddFreeTimeBeat => ((AddFreeTimeBeat)e).IsHitable(), _ => e.Type == EventType.PulseFreeTimeBeat && ((PulseFreeTimeBeat)e).IsHitable(), }; /// /// Determine if is in front of /// /// /// If is in front of , /// Else, /// public static bool IsInFrontOf(this OrderedEventCollection e, IBaseEvent item1, IBaseEvent item2) => (item1.Beat < item2.Beat || (item1.Beat.BeatOnly == item2.Beat.BeatOnly && (e.eventsBeatOrder[item1.Beat].BeforeThan(item1, item2)))); /// /// Check if another event is in front of itself, including events of the same beat but executed before itself. /// public static bool IsInFrontOf(this IBaseEvent e, IBaseEvent item) => e.Beat.BaseLevel?.IsInFrontOf(e, item) ?? throw new InvalidRDBeatException(); /// /// Get the total length of the oneshot. /// /// public static float Length(this AddOneshotBeat e) => e.Tick * e.Loops + e.Interval * e.Loops - 1f; /// /// Get the total length of the classic beat. /// /// public static float Length(this AddClassicBeat e) { float SyncoSwing = e.Parent?.LastOrDefault((SetRowXs i) => i.Active && e.IsBehind(i))?.SyncoSwing ?? 0; return (float)((double)(e.Tick * 6f) - ((SyncoSwing == 0f) ? 0.5 : ((double)SyncoSwing)) * (double)e.Tick); } /// /// Specifies the position of the image. This method changes both the pivot and the angle to keep the image visually in its original position. /// /// Sprite size. /// RDLevel /// Specified position. public static void MovePositionMaintainVisual(this Move e, RDSizeE spriteSize, RDPointE target) { if (e.Position != null && e.Pivot != null && e.Angle != null && e.Angle.Value.IsNumeric) { e.Position = new RDPointE?(target); e.Pivot = new RDPointE?((e.VisualPosition(spriteSize) - new RDSizeE(target)).Rotate(e.Angle.Value.NumericValue)); } } /// /// Specifies the position of the image. This method changes both the pivot and the angle to keep the image visually in its original position. /// /// RDLevel /// Specified position. public static void MovePositionMaintainVisual(this MoveRoom e, RDSizeE target) { if (e.RoomPosition != null && e.Pivot != null && e.Angle != null && e.Angle.Value.IsNumeric) { e.RoomPosition = new RDPointE?((RDPointE)target); e.Pivot = new RDPointE?((e.VisualPosition() - new RDSizeE((RDPointE)target)).Rotate(e.Angle.Value.NumericValue)); } } /// /// Convert beat pattern to string. /// /// The pattern string. public static string Pattern(this AddClassicBeat e) => Utils.Utils.GetPatternString(e.RowXs()); /// /// Get current player of the beat event. /// /// /// public static PlayerType Player(this BaseBeat e) { ChangePlayersRows? changePlayersRows = e.Beat.BaseLevel?.LastOrDefault((ChangePlayersRows i) => i.Active && i.Players[e.Index] != PlayerType.NoChange); return (changePlayersRows != null) ? changePlayersRows.Players[e.Index] : (PlayerType)(e.Parent?.Player ?? throw new NotImplementedException()); } /// /// Get the actual beat pattern. /// /// The actual beat pattern. public static Patterns[] RowXs(this AddClassicBeat e) { if (e.SetXs == null) { SetRowXs X = e.Parent?.LastOrDefault((SetRowXs i) => i.Active && e.IsBehind(i)) ?? new(); return X.Pattern; } else { Patterns[] T = new Patterns[6]; AddClassicBeat.ClassicBeatPatterns? setXs = e.SetXs; int? num = (setXs != null) ? new int?((int)setXs.GetValueOrDefault()) : null; if (((num != null) ? new bool?(num.GetValueOrDefault() == 0) : null).GetValueOrDefault()) { T[1] = Patterns.X; T[2] = Patterns.X; T[4] = Patterns.X; T[5] = Patterns.X; } else { num = (setXs != null) ? new int?((int)setXs.GetValueOrDefault()) : null; if (!(((num != null) ? new bool?(num.GetValueOrDefault() == 1) : null).GetValueOrDefault())) { throw new RhythmBaseException("How?"); } T[1] = Patterns.X; T[3] = Patterns.X; T[5] = Patterns.X; } return T; } } /// /// Get the special tag of the tag event. /// /// special tags. public static TagAction.SpecialTag[] SpetialTags(this TagAction e) => (TagAction.SpecialTag[])(from i in Enum.GetValues() where e.ActionTag.Contains(string.Format("[{0}]", i)) select i); /// /// Generate split event instances. /// public static IEnumerable Split(this SayReadyGetSetGo e) => e.PhraseToSay switch { SayReadyGetSetGo.Words.SayReaDyGetSetGoNew => [ e.SplitCopy(0f, SayReadyGetSetGo.Words.JustSayRea), e.SplitCopy(e.Tick, SayReadyGetSetGo.Words.JustSayDy), e.SplitCopy(e.Tick * 2f, SayReadyGetSetGo.Words.JustSayGet), e.SplitCopy(e.Tick * 3f, SayReadyGetSetGo.Words.JustSaySet), e.SplitCopy(e.Tick * 4f, SayReadyGetSetGo.Words.JustSayGo) ], SayReadyGetSetGo.Words.SayGetSetGo => [ e.SplitCopy(0f, SayReadyGetSetGo.Words.JustSayGet), e.SplitCopy(e.Tick, SayReadyGetSetGo.Words.JustSaySet), e.SplitCopy(e.Tick * 2f, SayReadyGetSetGo.Words.JustSayGo) ], SayReadyGetSetGo.Words.SayReaDyGetSetOne => [ e.SplitCopy(0f, SayReadyGetSetGo.Words.JustSayRea), e.SplitCopy(e.Tick, SayReadyGetSetGo.Words.JustSayDy), e.SplitCopy(e.Tick * 2f, SayReadyGetSetGo.Words.JustSayGet), e.SplitCopy(e.Tick * 3f, SayReadyGetSetGo.Words.JustSaySet), e.SplitCopy(e.Tick * 4f, SayReadyGetSetGo.Words.Count1) ], SayReadyGetSetGo.Words.SayGetSetOne => [ e.SplitCopy(0f, SayReadyGetSetGo.Words.JustSayGet), e.SplitCopy(e.Tick, SayReadyGetSetGo.Words.JustSaySet), e.SplitCopy(e.Tick * 2f, SayReadyGetSetGo.Words.Count1) ], SayReadyGetSetGo.Words.SayReadyGetSetGo => [ e.SplitCopy(0f, SayReadyGetSetGo.Words.JustSayReady), e.SplitCopy(e.Tick * 2f, SayReadyGetSetGo.Words.JustSayGet), e.SplitCopy(e.Tick * 3f, SayReadyGetSetGo.Words.JustSaySet), e.SplitCopy(e.Tick * 4f, SayReadyGetSetGo.Words.JustSayGo) ], _ => [e], }; /// /// Generate split event instances. /// public static IEnumerable Split(this AddOneshotBeat e) { e._beat.IfNullThrowException(); List L = []; uint loops = e.Loops; for (uint i = 0U; i <= loops; i += 1U) { AddOneshotBeat T = e.MemberwiseClone(); T.Loops = 0U; T.Interval = 0f; T.Beat = new RDBeat(e._beat._calculator, unchecked(e.Beat.BeatOnly + i * e.Interval)); L.Add(T); } return L.AsEnumerable(); } /// /// Generate split event instances. Follow the most recently activated Xs. /// public static IEnumerable Split(this AddClassicBeat e) { SetRowXs x = e.Parent?.LastOrDefault((SetRowXs i) => i.Active && e.IsBehind(i)) ?? new(); return e.Split(x); } /// /// Generate split event instances. /// public static IEnumerable Split(this AddClassicBeat e, SetRowXs Xs) { List L = []; AddFreeTimeBeat Head = e.Clone(); Head.Pulse = 0; Head.Hold = e.Hold; L.Add(Head); int i = 1; do { if (!(i < 6 && Xs.Pattern[i] == Patterns.X)) { PulseFreeTimeBeat Pulse = e.Clone(); PulseFreeTimeBeat pulseFreeTimeBeat; (pulseFreeTimeBeat = Pulse).Beat = pulseFreeTimeBeat.Beat + e.Tick * (float)i; if (i >= (int)Xs.SyncoBeat) (pulseFreeTimeBeat = Pulse).Beat = pulseFreeTimeBeat.Beat - Xs.SyncoSwing; if (i % 2 == 1) (pulseFreeTimeBeat = Pulse).Beat = pulseFreeTimeBeat.Beat + (e.Tick - ((e.Swing == 0f) ? e.Tick : e.Swing)); Pulse.Hold = e.Hold; Pulse.Action = PulseFreeTimeBeat.ActionType.Increment; L.Add(Pulse); } i++; } while (i <= 6); return L.AsEnumerable(); } /// /// Calculates the duration of the VFX effect for the given preset. /// /// The SetVFXPreset event. /// An RDRange representing the duration of the VFX effect. public static RDRange VFXDuration(this SetVFXPreset e) { if (e.Preset != SetVFXPreset.Presets.DisableAll && e.Enable) { SetVFXPreset? close = e.After().FirstOrDefault(i => i.Rooms.Contains(e.Rooms) && ( i.Preset == e.Preset || i.Preset == SetVFXPreset.Presets.DisableAll )); return new(e.Beat, close?.Beat); } return new(e.Beat, e.Beat); } /// /// Remove auxiliary symbols. /// public static string TextOnly(this ShowDialogue e) { string result = e.Text; foreach (string item in new string[] { "shake", "shakeRadius=\\d+", "wave", "waveHeight=\\d+", "waveSpeed=\\d+", "swirl", "swirlRadius=\\d+", "swirlSpeed=\\d+", "static" }) result = Regex.Replace(result, string.Format("\\[{0}\\]", item), ""); return result; } /// /// The visual position of the lower left corner of the image. /// public static RDPointE VisualPosition(this Move e, RDSizeE spriteSize) { RDPointE VisualPosition = default; if (e.Position != null && e.Pivot != null && e.Angle != null && e.Angle.Value.IsNumeric && e.Scale != null) { RDPointE previousPosition = e.Position.Value; RDExpression? x = e.Pivot?.X * (e.Scale?.Width) * spriteSize.Width / 100f; RDPointE previousPivot = new(x, e.Pivot?.Y * (e.Scale?.Height) * spriteSize.Height / 100f); VisualPosition = previousPosition + new RDSizeE(previousPivot.Rotate(e.Angle.Value.NumericValue)); } return VisualPosition; } /// /// The visual position of the lower left corner of the image. /// public static RDPointE VisualPosition(this MoveRoom e) { RDPointE VisualPosition = default; if (e.RoomPosition != null && e.Pivot != null && e.Angle != null) { RDPointE previousPosition = e.RoomPosition.Value; RDPointE previousPivot = new((e.Pivot?.X) * (e.Scale?.Width), (e.Pivot?.Y) * (e.Scale?.Height)); VisualPosition = previousPosition + new RDSizeE(previousPivot.Rotate(e.Angle.Value.NumericValue)); } return VisualPosition; } /// /// Creates a rotated rectangle for the MoveCamera event. /// /// The MoveCamera event. /// A rotated rectangle representing the camera's position, zoom, and angle. public static RDRotatedRectE RotatedRect(this MoveCamera e) => new(e.CameraPosition, new(e.Zoom, e.Zoom), null, e.Angle); /// /// Creates a rotated rectangle for the MoveRow event. /// /// The MoveRow event. /// A rotated rectangle representing the row's position, scale, pivot, and angle. public static RDRotatedRectE RotatedRect(this MoveRow e) => new(e.RowPosition, e.Scale, new(e.Pivot, e.Pivot), e.Angle); /// /// Creates a rotated rectangle for the MoveRoom event. /// /// The MoveRoom event. /// A rotated rectangle representing the room's position, scale, pivot, and angle. public static RDRotatedRectE RotatedRect(this MoveRoom e) => new(e.RoomPosition, e.Scale, e.Pivot, e.Angle); /// /// Creates a rotated rectangle for the Move event. /// /// The Move event. /// A rotated rectangle representing the position, scale, pivot, and angle. public static RDRotatedRectE RotatedRect(this Move e) => new(e.Position, e.Scale, e.Pivot, e.Angle); private static SayReadyGetSetGo SplitCopy(this SayReadyGetSetGo e, float extraBeat, SayReadyGetSetGo.Words word) { SayReadyGetSetGo Temp = e.Clone(); Temp.Beat += extraBeat; Temp.PhraseToSay = word; Temp.Volume = e.Volume; return Temp; } } }