diff --git a/.editorconfig b/.editorconfig index af9feb4..8d85d5f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -86,3 +86,18 @@ dotnet_diagnostic.SA1133.severity = none # IDE0001: Simplify Names dotnet_diagnostic.IDE0001.severity = warning + +# IDE0079: Remove unnecessary suppression +dotnet_diagnostic.IDE0079.severity = none + +# S3903: Types should be defined in named namespaces +dotnet_diagnostic.S3903.severity = none + +# IDE0045: Convert to conditional expression +dotnet_diagnostic.IDE0045.severity = none + +# IDE0046: Convert to conditional expression +dotnet_diagnostic.IDE0046.severity = none + +# S1244: Floating point numbers should not be tested for equality +dotnet_diagnostic.S1244.severity = none diff --git a/MiraAPI.Example/.editorconfig b/MiraAPI.Example/.editorconfig new file mode 100644 index 0000000..171e612 --- /dev/null +++ b/MiraAPI.Example/.editorconfig @@ -0,0 +1,4 @@ +[*.{cs,vb}] + +# SA1602: Enumeration items should be documented. +dotnet_diagnostic.SA1602.severity = none diff --git a/MiraAPI.Example/ExampleColors.cs b/MiraAPI.Example/ExampleColors.cs index cde5aae..297a370 100644 --- a/MiraAPI.Example/ExampleColors.cs +++ b/MiraAPI.Example/ExampleColors.cs @@ -1,4 +1,6 @@ using MiraAPI.Colors; +using MiraAPI.Colors.ColorSpaces; +using MiraAPI.Utilities; using UnityEngine; namespace MiraAPI.Example; @@ -14,4 +16,9 @@ public static class ExampleColors public static CustomColor Rose { get; } = new("color.rose", new Color(0.98f, 0.26f, 0.62f)); public static CustomColor Gold { get; } = new("color.gold", new Color(1.0f, 0.84f, 0.0f)); + + public static VariableColor Rainbow { get; } = new("color.rainbow", t => new HsbColor(MathUtilities.PingPong(t, 1f, 0.3f), 1f, 1f)) + { + ColorBrightness = CustomColorBrightness.Lighter, + }; } diff --git a/MiraAPI.Example/Resources/Locale/en_US.xml b/MiraAPI.Example/Resources/Locale/en_US.xml index e370dc3..8328c69 100644 --- a/MiraAPI.Example/Resources/Locale/en_US.xml +++ b/MiraAPI.Example/Resources/Locale/en_US.xml @@ -16,11 +16,11 @@ Your votes have double the power. You get one extra vote in the meeting! The Mayor is a Crewmate role that gets two votes in the meeting, instead of the singular vote other Crewmates get. - Chamelon - Become one with the environment. - Fade out of everyone's vision by not moving. - Stay invisible while not moving. - The Chamelon is a Crewmate role that slowly becomes invisible while standing still. + Chameleon + Become one with the environment. + Fade out of everyone's vision by not moving. + Stay invisible while not moving. + The Chameleon is a Crewmate role that slowly becomes invisible while standing still. Outcast Killer Kill everyone else. Be the last outcast standing. @@ -73,11 +73,12 @@ Eliminate every other faction. Example Bool 2 Slider Example Number - Example Multplier + Example Multiplier Example Enum Example Button Outcast Killer Wins! Cerulean Rose Gold + Rainbow diff --git a/MiraAPI/Colors/ColorSpaces/CmykColor.cs b/MiraAPI/Colors/ColorSpaces/CmykColor.cs new file mode 100644 index 0000000..11ec563 --- /dev/null +++ b/MiraAPI/Colors/ColorSpaces/CmykColor.cs @@ -0,0 +1,107 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using MiraAPI.Utilities; +using UnityEngine; + +namespace MiraAPI.Colors.ColorSpaces; + +/// +/// A struct that denotes the CMYK (Cyan, Magenta, Yellow, Key) color space. +/// +/// The cyan component. +/// The magenta component. +/// The yellow component. +/// The key component. +/// The alpha component. +[Serializable] +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Unity parity.")] +public record struct CmykColor(float c, float m, float y, float k, float a = 1f) +{ + /// + /// Implicitly converts a to a Unity . + /// + /// The color being converted. + public static implicit operator Color(CmykColor c) + { + return ToColor(c); + } + + /// + /// Implicitly converts a Unity to a . + /// + /// The color being converted. + public static implicit operator CmykColor(Color c) + { + return FromColor(c); + } + + /// + /// Converts a to a standard Unity RGB . + /// + /// The CMYK color to convert. + /// A Unity representing the same visual color. + public static Color ToColor(CmykColor cmyk) + { + var key = 1f - cmyk.k; + + var r = (1f - cmyk.c) * key; + var g = (1f - cmyk.m) * key; + var b = (1f - cmyk.y) * key; + + return new Color(MathUtilities.Clamp01(r), MathUtilities.Clamp01(g), MathUtilities.Clamp01(b), cmyk.a); + } + + /// + /// Converts a standard Unity RGB to a . + /// + /// The Unity Color to convert. + /// A representing the same visual color. + public static CmykColor FromColor(Color color) + { + var r = color.r; + var g = color.g; + var b = color.b; + + var k = 1f - MathF.Max(r, MathF.Max(g, b)); + + if (k >= 1f) + return new CmykColor(0f, 0f, 0f, 1f, color.a); + + var invK = 1f / (1f - k); + + var c = (1f - r - k) * invK; + var m = (1f - g - k) * invK; + var y = (1f - b - k) * invK; + + return new CmykColor(MathUtilities.Clamp01(c), MathUtilities.Clamp01(m), MathUtilities.Clamp01(y), MathUtilities.Clamp01(k), color.a); + } + + /// + /// Linearly interpolates between two values. + /// + /// The starting color. + /// The ending color. + /// The interpolation value between the two colors, clamped to a 0 to 1 range. + /// The interpolated . + public static CmykColor Lerp(CmykColor a, CmykColor b, float t) + { + return LerpUnclamped(a, b, MathUtilities.Clamp01(t)); + } + + /// + /// Linearly interpolates between two values without clamping the interpolant. + /// + /// The starting color. + /// The ending color. + /// The interpolation value between the two colors. + /// The interpolated . + public static CmykColor LerpUnclamped(CmykColor a, CmykColor b, float t) + { + return new CmykColor( + MathUtilities.LerpUnclamped(a.c, b.c, t), + MathUtilities.LerpUnclamped(a.m, b.m, t), + MathUtilities.LerpUnclamped(a.y, b.y, t), + MathUtilities.LerpUnclamped(a.k, b.k, t), + MathUtilities.LerpUnclamped(a.a, b.a, t)); + } +} diff --git a/MiraAPI/Colors/ColorSpaces/HsbColor.cs b/MiraAPI/Colors/ColorSpaces/HsbColor.cs new file mode 100644 index 0000000..b566333 --- /dev/null +++ b/MiraAPI/Colors/ColorSpaces/HsbColor.cs @@ -0,0 +1,136 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using MiraAPI.Utilities; +using UnityEngine; + +namespace MiraAPI.Colors.ColorSpaces; + +/// +/// A struct that denotes the HSB (Hue, Saturation, Brightness) color space. +/// +/// The hue component. +/// The saturation component. +/// The brightness component. +/// The alpha component. +[Serializable] +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Unity parity.")] +public record struct HsbColor(float h, float s, float b, float a = 1f) +{ + /// + /// Implicitly converts an to a Unity . + /// + /// The color being converted. + public static implicit operator Color(HsbColor c) + { + return ToColor(c); + } + + /// + /// Implicitly converts a Unity to an . + /// + /// The color being converted. + public static implicit operator HsbColor(Color c) + { + return FromColor(c); + } + + /// + /// Converts an to a standard Unity RGB . + /// + /// The HSB color to convert. + /// A Unity representing the same visual color. + public static Color ToColor(HsbColor hsb) + { + if (hsb.s <= float.Epsilon) + return new Color(MathUtilities.Clamp01(hsb.b), MathUtilities.Clamp01(hsb.b), MathUtilities.Clamp01(hsb.b), hsb.a); + + var c = hsb.b * hsb.s; + var hPrime = hsb.h * 6f; + var x = c * (1f - MathF.Abs((hPrime % 2f) - 1f)); + var m = hsb.b - c; + + var (r, g, b) = hPrime switch + { + < 1f => (c, x, 0f), + < 2f => (x, c, 0f), + < 3f => (0f, c, x), + < 4f => (0f, x, c), + < 5f => (x, 0f, c), + <= 6f => (c, 0f, x), + _ => (0f, 0f, 0f), + }; + + return new Color( + MathUtilities.Clamp01(r + m), + MathUtilities.Clamp01(g + m), + MathUtilities.Clamp01(b + m), + hsb.a); + } + + /// + /// Converts a standard Unity RGB to an . + /// + /// The Unity Color to convert. + /// An representing the same visual color. + public static HsbColor FromColor(Color color) + { + var max = MathF.Max(color.r, MathF.Max(color.g, color.b)); + var min = MathF.Min(color.r, MathF.Min(color.g, color.b)); + var delta = max - min; + + var h = 0f; + + if (delta > 0f) + { + if (max == color.r) + h = (color.g - color.b) / delta % 6f; + else if (max == color.g) + h = (color.b - color.r) / delta + 2f; + else + h = (color.r - color.g) / delta + 4f; + + h /= 6f; + + if (h < 0f) + h += 1f; + } + + var s = max == 0f ? 0f : delta / max; + var b = max; + + return new HsbColor(h, s, b, color.a); + } + + /// + /// Linearly interpolates between two values, using the shortest path for the hue. + /// + /// The starting color. + /// The ending color. + /// The interpolation value between the two colors, clamped to a 0 to 1 range. + /// The interpolated . + public static HsbColor Lerp(HsbColor a, HsbColor b, float t) + { + return LerpUnclamped(a, b, MathUtilities.Clamp01(t)); + } + + /// + /// Linearly interpolates between two values without clamping the interpolant. + /// + /// The starting color. + /// The ending color. + /// The interpolation value between the two colors. + /// The interpolated . + public static HsbColor LerpUnclamped(HsbColor a, HsbColor b, float t) + { + var hueDifference = MathUtilities.Repeat(b.h - a.h, 1f); + + if (hueDifference > 0.5f) + hueDifference -= 1f; + + return new HsbColor( + MathUtilities.Repeat(a.h + (hueDifference * t), 1f), + MathUtilities.LerpUnclamped(a.s, b.s, t), + MathUtilities.LerpUnclamped(a.b, b.b, t), + MathUtilities.LerpUnclamped(a.a, b.a, t)); + } +} diff --git a/MiraAPI/Colors/ColorSpaces/HslColor.cs b/MiraAPI/Colors/ColorSpaces/HslColor.cs new file mode 100644 index 0000000..466f159 --- /dev/null +++ b/MiraAPI/Colors/ColorSpaces/HslColor.cs @@ -0,0 +1,132 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using MiraAPI.Utilities; +using UnityEngine; + +namespace MiraAPI.Colors.ColorSpaces; + +/// +/// A struct that denotes the HSL (Hue, Saturation, Lightness) color space. +/// +/// The hue component. +/// The saturation component. +/// The lightness component. +/// The alpha component. +[Serializable] +[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Unity parity.")] +public record struct HslColor(float h, float s, float l, float a = 1f) +{ + /// + /// Implicitly converts an to a Unity . + /// + /// The color being converted. + public static implicit operator Color(HslColor c) + { + return ToColor(c); + } + + /// + /// Implicitly converts a Unity to an . + /// + /// The color being converted. + public static implicit operator HslColor(Color c) + { + return FromColor(c); + } + + /// + /// Converts an to a standard Unity RGB . + /// + /// The HSL color to convert. + /// A Unity representing the same visual color. + public static Color ToColor(HslColor hsl) + { + if (hsl.s <= float.Epsilon) + return new Color(MathUtilities.Clamp01(hsl.l), MathUtilities.Clamp01(hsl.l), MathUtilities.Clamp01(hsl.l), hsl.a); + + var c = (1f - MathF.Abs(2f * hsl.l - 1f)) * hsl.s; + var hPrime = hsl.h * 6f; + var x = c * (1f - MathF.Abs((hPrime % 2f) - 1f)); + var m = hsl.l - c / 2f; + + var (r, g, b) = hPrime switch + { + < 1f => (c, x, 0f), + < 2f => (x, c, 0f), + < 3f => (0f, c, x), + < 4f => (0f, x, c), + < 5f => (x, 0f, c), + <= 6f => (c, 0f, x), + _ => (0f, 0f, 0f), + }; + + return new Color(MathUtilities.Clamp01(r + m), MathUtilities.Clamp01(g + m), MathUtilities.Clamp01(b + m), hsl.a); + } + + /// + /// Converts a standard Unity RGB to an . + /// + /// The Unity Color to convert. + /// An representing the same visual color. + public static HslColor FromColor(Color color) + { + var max = MathF.Max(color.r, MathF.Max(color.g, color.b)); + var min = MathF.Min(color.r, MathF.Min(color.g, color.b)); + var l = (max + min) / 2f; + var delta = max - min; + + var s = delta == 0f ? 0f : delta / (1f - MathF.Abs((2f * l) - 1f)); + + var h = 0f; + + if (delta > 0f) + { + if (max == color.r) + h = (color.g - color.b) / delta % 6f; + else if (max == color.g) + h = (color.b - color.r) / delta + 2f; + else + h = (color.r - color.g) / delta + 4f; + + h /= 6f; + + if (h < 0f) + h += 1f; + } + + return new HslColor(h, s, l, color.a); + } + + /// + /// Linearly interpolates between two values, using the shortest path for the hue. + /// + /// The starting color. + /// The ending color. + /// The interpolation value between the two colors, clamped to a 0 to 1 range. + /// The interpolated . + public static HslColor Lerp(HslColor a, HslColor b, float t) + { + return LerpUnclamped(a, b, MathUtilities.Clamp01(t)); + } + + /// + /// Linearly interpolates between two values without clamping the interpolant. + /// + /// The starting color. + /// The ending color. + /// The interpolation value between the two colors. + /// The interpolated . + public static HslColor LerpUnclamped(HslColor a, HslColor b, float t) + { + var hueDifference = MathUtilities.Repeat(b.h - a.h, 1f); + + if (hueDifference > 0.5f) + hueDifference -= 1f; + + return new HslColor( + MathUtilities.Repeat(a.h + (hueDifference * t), 1f), + MathUtilities.LerpUnclamped(a.s, b.s, t), + MathUtilities.LerpUnclamped(a.l, b.l, t), + MathUtilities.LerpUnclamped(a.a, b.a, t)); + } +} diff --git a/MiraAPI/Colors/CustomColor.cs b/MiraAPI/Colors/CustomColor.cs index a778e31..db91356 100644 --- a/MiraAPI/Colors/CustomColor.cs +++ b/MiraAPI/Colors/CustomColor.cs @@ -1,4 +1,5 @@ -using MiraAPI.Translation; +using System.Diagnostics.CodeAnalysis; +using MiraAPI.Translation; using MiraAPI.Utilities; using UnityEngine; @@ -10,17 +11,37 @@ namespace MiraAPI.Colors; /// The name of the option. /// The main color. /// The shadow color. -public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColor) +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal code.")] +[SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1513:Closing brace should be followed by blank line", Justification = "Looks weird.")] +public class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColor) { /// /// Gets or sets the main color. /// - public Color32 MainColor { get; set; } = mainColor; + public Color32 MainColor + { + get; + set + { + field = value; + InternalMainColor = value; + } + } + = mainColor; /// /// Gets or sets the shadow color. /// - public Color32 ShadowColor { get; set; } = shadowColor; + public Color32 ShadowColor + { + get; + set + { + field = value; + InternalShadowColor = value; + } + } + = shadowColor; /// /// Gets or sets the name of the color. @@ -32,6 +53,9 @@ public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 sha /// public CustomColorBrightness ColorBrightness { get; set; } = CustomColorBrightness.Darker; + internal Color InternalMainColor = mainColor; + internal Color InternalShadowColor = shadowColor; + /// public CustomColor(StringNames name, Color32 mainColor) : this(name, mainColor, mainColor.GetShadowColor(60)) @@ -56,6 +80,11 @@ public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 sha /// public enum CustomColorBrightness { + /// + /// An invalid color. + /// + Invalid = -1, + /// /// A lighter color. /// diff --git a/MiraAPI/Colors/PaletteManager.cs b/MiraAPI/Colors/PaletteManager.cs index fe7e725..3b9cd4a 100644 --- a/MiraAPI/Colors/PaletteManager.cs +++ b/MiraAPI/Colors/PaletteManager.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using HarmonyLib; +using UnityEngine; namespace MiraAPI.Colors; @@ -15,9 +17,36 @@ public static class PaletteManager public static CustomColor[] RegisteredColors => [.. CustomColors]; internal static readonly List CustomColors = []; + internal static readonly HashSet VariableColorNames = []; + + private static readonly CustomColorBrightness[] ColorBrightnessMap = + [ + CustomColorBrightness.Darker, // Red + CustomColorBrightness.Darker, // Blue + CustomColorBrightness.Darker, // Green + CustomColorBrightness.Lighter, // Pink + CustomColorBrightness.Lighter, // Orange + CustomColorBrightness.Lighter, // Yellow + CustomColorBrightness.Darker, // Black + CustomColorBrightness.Lighter, // White + CustomColorBrightness.Darker, // Purple + CustomColorBrightness.Darker, // Brown + CustomColorBrightness.Lighter, // Cyan + CustomColorBrightness.Lighter, // Lime + CustomColorBrightness.Darker, // Maroon + CustomColorBrightness.Lighter, // Rose + CustomColorBrightness.Lighter, // Banana + CustomColorBrightness.Darker, // Gray + CustomColorBrightness.Darker, // Tan + CustomColorBrightness.Lighter, // Coral + ]; + + private static int originalLength; internal static void RegisterAllColors() { + originalLength = Palette.PlayerColors.Length; + var colors = CustomColors.Select(x => x.MainColor).ToArray(); var shadowColors = CustomColors.Select(x => x.ShadowColor).ToArray(); var stringNames = CustomColors.Select(x => x.Name).ToArray(); @@ -29,4 +58,90 @@ public static class PaletteManager Palette.TextColors = Palette.TextColors.ToArray().AddRangeToArray(colors); Palette.TextOutlineColors = Palette.TextOutlineColors.ToArray().AddRangeToArray(shadowColors); } + + /// + /// Checks to see if the provided color ID belongs to a variable color. + /// + /// The id of the color. + /// true if the color is a variable color; false otherwise. + public static bool IsVariable(int colorId) + { + if ((uint)colorId >= (uint)Palette.ColorNames.Length) + return false; + + return VariableColorNames.Contains(Palette.ColorNames[colorId]); + } + + /// + /// Checks to see if the provided color ID belongs to a custom color. + /// + /// The ID of the color. + /// true if the color is a custom color; false otherwise. + public static bool IsCustomColor(int colorId) + { + return (uint)(colorId - originalLength) < (uint)CustomColors.Count; + } + + /// + /// Gets the main color of the provided color ID. + /// + /// The id of the color. + /// The main color associated with the provided ID. + public static Color GetMainColor(int colorId) + { + if ((uint)colorId >= (uint)Palette.ColorNames.Length) + return Color.white; + + return !TryGetCustomColor(colorId, out var custom) + ? Palette.PlayerColors[colorId] + : (custom is VariableColor variable + ? variable.EvaluateMainColor(Time.time) + : custom.InternalMainColor); + } + + /// + /// Gets the shadow color of the provided color ID. + /// + /// The ID of the color. + /// The shadow color associated with the provided ID. + public static Color GetShadowColor(int colorId) + { + if ((uint)colorId >= (uint)Palette.ColorNames.Length) + return Color.white; + + return !TryGetCustomColor(colorId, out var custom) + ? Palette.ShadowColors[colorId] + : (custom is VariableColor variable + ? variable.EvaluateShadowColor(Time.time) + : custom.InternalShadowColor); + } + + /// + /// Gets the brightness type of the color associated with the provided ID. + /// + /// The ID of the color. + /// The brightness type of the provided ID. + public static CustomColorBrightness GetBrightness(int colorId) + { + if ((uint)colorId >= (uint)Palette.ColorNames.Length) + return CustomColorBrightness.Invalid; + + return TryGetCustomColor(colorId, out var custom) + ? custom.ColorBrightness + : ColorBrightnessMap[colorId]; + } + + internal static bool TryGetCustomColor(int colorId, [NotNullWhen(true)] out CustomColor? custom) + { + var index = colorId - originalLength; + + if ((uint)index >= (uint)CustomColors.Count) + { + custom = null; + return false; + } + + custom = CustomColors[index]; + return true; + } } diff --git a/MiraAPI/Colors/VariableColor.cs b/MiraAPI/Colors/VariableColor.cs new file mode 100644 index 0000000..bfb9717 --- /dev/null +++ b/MiraAPI/Colors/VariableColor.cs @@ -0,0 +1,72 @@ +using System; +using MiraAPI.LocalSettings; +using MiraAPI.Translation; +using MiraAPI.Utilities; +using UnityEngine; + +namespace MiraAPI.Colors; + +/// +/// Represents a dynamic custom color that changes over time. +/// +/// The name of the color. +/// The function to evaluate the dynamic main color. +/// The function to evaluate the dynamic shadow color. +/// The static main color used when dynamic colors are disabled. +/// The static shadow color used when dynamic colors are disabled. +/// The default time that is used to automatically fill in the static colors when the latter is not provided. +public sealed class VariableColor(StringNames name, Func mainEvaluator, Func? shadowEvaluator, Color32? mainColor = null, Color32? shadowColor = null, float defaultTime = 0f) + : CustomColor(name, mainColor ?? mainEvaluator(defaultTime), shadowColor ?? shadowEvaluator?.Invoke(defaultTime) ?? (mainColor ?? mainEvaluator(defaultTime)).GetShadowColor(60)) +{ + /// + /// Gets or sets a delegate that takes in a time value and returns the dynamic main color. + /// + public Func MainEvaluator { get; set; } = mainEvaluator; + + /// + /// Gets or sets a delegate that takes in a time value and returns the dynamic shadow color. + /// + public Func ShadowEvaluator { get; set; } = shadowEvaluator ?? (t => mainEvaluator(t).GetShadowColor(0.24f)); + + /// + public VariableColor(StringNames name, Func mainEvaluator, Color32? mainColor = null, float defaultTime = 0f) + : this(name, mainEvaluator, null, mainColor, null, defaultTime) + { + } + + /// + public VariableColor(string name, Func mainEvaluator, Color32? mainColor = null, float defaultTime = 0f) + : this(name, mainEvaluator, null, mainColor, null, defaultTime) + { + } + + /// + public VariableColor(string name, Func mainEvaluator, Func? shadowEvaluator, Color32? mainColor = null, Color32? shadowColor = null, float defaultTime = 0f) + : this(MiraLocaleManager.GetOrCreateLocaleString(name), mainEvaluator, shadowEvaluator, mainColor, shadowColor, defaultTime) + { + } + + /// + /// Evaluates the dynamic main color based on the provided time value. + /// + /// The time to evaluate the color against. + /// The dynamically evaluated color, or the static if static variable colors are enabled in the user's settings. + public Color EvaluateMainColor(float time) + { + return LocalSettingsTabSingleton.Instance.EnableVariableColors.Value + ? MainEvaluator(time) + : InternalMainColor; + } + + /// + /// Evaluates the dynamic shadow color based on the provided time value. + /// + /// The time to evaluate the color against. + /// The dynamically evaluated color, or the static if static variable colors are enabled in the user's settings. + public Color EvaluateShadowColor(float time) + { + return LocalSettingsTabSingleton.Instance.EnableVariableColors.Value + ? ShadowEvaluator(time) + : InternalShadowColor; + } +} diff --git a/MiraAPI/Colors/VariableColorBehaviour.cs b/MiraAPI/Colors/VariableColorBehaviour.cs new file mode 100644 index 0000000..e0d4cea --- /dev/null +++ b/MiraAPI/Colors/VariableColorBehaviour.cs @@ -0,0 +1,102 @@ +using System.Diagnostics.CodeAnalysis; +using Il2CppInterop.Runtime.Attributes; +using MiraAPI.Colors; +using MiraAPI.Utilities; +using Reactor.Utilities.Attributes; +using UnityEngine; + +namespace MiraAPI; + +/// +/// The base class for variable color behaviour on s. +/// +[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity methods.")] +[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity fields.")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")] +public abstract class BaseVariableColorBehaviour : MonoBehaviour +{ + /// + /// The to apply. + /// + protected VariableColor? color; + + /// + /// Sets the current variable color. + /// + /// The new . + [HideFromIl2Cpp] + public void SetColor(VariableColor? newColor) + { + color = newColor; + } +} + +/// +/// A mono script that adjusts the material colors of a component that it's attached to based on the provided variable color. +/// +[RegisterInIl2Cpp] +[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity methods.")] +public sealed class VariableColorBehaviour : BaseVariableColorBehaviour +{ + private Renderer? rend; + + private static readonly Color VisorColor = Palette.VisorColor; + + /// + /// Sets the new target. + /// + /// The new that the script must target. + public void SetRend(Renderer? newRend) + { + rend = newRend; + } + + private void Awake() + { + rend = GetComponent(); + } + + private void Update() + { + if (color == null || rend == null) + return; + + var time = Time.time; + var material = rend.material; + + material.SetColor(ShaderID.VisorColor, VisorColor); + + material.SetColor(ShaderID.BodyColor, color.EvaluateMainColor(time)); + material.SetColor(ShaderID.BackColor, color.EvaluateShadowColor(time)); + } +} + +/// +/// A mono script that adjusts the graphic colors of a component that it's attached to based on the provided variable color. +/// +[RegisterInIl2Cpp] +[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity methods.")] +public sealed class BasicVariableColorBehaviour : BaseVariableColorBehaviour +{ + private SpriteRenderer? rend; + + /// + /// Sets the new target. + /// + /// The new that the script must target. + public void SetRend(SpriteRenderer? newRend) + { + rend = newRend; + } + + private void Awake() + { + rend = GetComponent(); + } + + private void Update() + { + if (color != null && rend != null) + rend.color = color.EvaluateMainColor(Time.time); + } +} diff --git a/MiraAPI/GameModes/HideAndSeekMode.cs b/MiraAPI/GameModes/HideAndSeekMode.cs index 316c3cf..ec99ce9 100644 --- a/MiraAPI/GameModes/HideAndSeekMode.cs +++ b/MiraAPI/GameModes/HideAndSeekMode.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using AmongUs.Data; @@ -462,7 +461,6 @@ public class HideAndSeekMode : AbstractGameMode } /// - [SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "Warning cascades into forcing the entire tree to be ternary operators.")] public override PlayerBodyTypes GetBodyType(PlayerControl player) { if (player == null || player.Data == null || player.Data.Role == null) diff --git a/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs b/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs index 646de56..40cf0f8 100644 --- a/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs +++ b/MiraAPI/GameOptions/OptionTypes/ModdedPlayerOption.cs @@ -43,7 +43,7 @@ public class ModdedPlayerOption : ModdedOption data.Title = StringName; data.Type = global::OptionTypes.Player; - Values = new(); + Values = []; AllowNone = allowNone; Value = DefaultValue; } diff --git a/MiraAPI/Hud/CustomPaginableMenu.cs b/MiraAPI/Hud/CustomPaginableMenu.cs index 27f7819..14f573b 100644 --- a/MiraAPI/Hud/CustomPaginableMenu.cs +++ b/MiraAPI/Hud/CustomPaginableMenu.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using MiraAPI.Patches.Stubs; using MiraAPI.Translation; +using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Utilities; using Reactor.Utilities.Extensions; @@ -100,7 +101,6 @@ public abstract class CustomPaginableMenu : CustomPhoneMenu GetFilteredEntries() { var query = NormalizeForSearch(searchText); @@ -248,8 +248,7 @@ public abstract class CustomPaginableMenu : CustomPhoneMenu() - ?? searchTextbox.gameObject.AddComponent(); + var searchFocusButton = searchTextbox.gameObject.GetOrAddComponent(); if (wikiClickSound != null) { searchFocusButton.ClickSound = wikiClickSound; diff --git a/MiraAPI/MiraApiPlugin.cs b/MiraAPI/MiraApiPlugin.cs index 4da8968..a07c36d 100644 --- a/MiraAPI/MiraApiPlugin.cs +++ b/MiraAPI/MiraApiPlugin.cs @@ -64,11 +64,9 @@ public partial class MiraApiPlugin : BasePlugin, IMiraPlugin private static MiraPluginManager? PluginManager { get; set; } internal Harmony Harmony { get; } = new(Id); - public static ConfigEntry WikiSortingGroup { get; private set; } - - public static ConfigEntry WikiSortingMethod { get; private set; } - - public static ConfigEntry WikiSortingFilter { get; private set; } + internal static ConfigEntry WikiSortingGroup { get; private set; } + internal static ConfigEntry WikiSortingMethod { get; private set; } + internal static ConfigEntry WikiSortingFilter { get; private set; } /// public override void Load() diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs index 1560955..bb6ed51 100644 --- a/MiraAPI/MiraApiSettings.cs +++ b/MiraAPI/MiraApiSettings.cs @@ -233,4 +233,10 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) /// [LocalToggleSetting] public ConfigEntry EnableSabotageBlares { get; private set; } = config.Bind("MiraApi.Accessibility", "MiraApi.EnableSabotageBlare", true); + + /// + /// Gets whether variable colors will remain static or not. + /// + [LocalToggleSetting] + public ConfigEntry EnableVariableColors { get; private set; } = config.Bind("MiraApi.Accessibility", "MiraApi.EnableVariableColors", true); } diff --git a/MiraAPI/Patches/.editorconfig b/MiraAPI/Patches/.editorconfig index 2622fc7..3b3da0b 100644 --- a/MiraAPI/Patches/.editorconfig +++ b/MiraAPI/Patches/.editorconfig @@ -12,5 +12,5 @@ dotnet_diagnostic.SA1611.severity = none # SA1615: Element return value should be documented. dotnet_diagnostic.SA1615.severity = none -# S125: Sections of code should not be commented out. -dotnet_diagnostic.S125.severity = none \ No newline at end of file +# SA1602: Enumeration items should be documented. +dotnet_diagnostic.SA1602.severity = none diff --git a/MiraAPI/Patches/Colors/VariableColorPatches.cs b/MiraAPI/Patches/Colors/VariableColorPatches.cs new file mode 100644 index 0000000..98bed64 --- /dev/null +++ b/MiraAPI/Patches/Colors/VariableColorPatches.cs @@ -0,0 +1,110 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using AmongUs.Data; +using HarmonyLib; +using MiraAPI.Colors; +using MiraAPI.Utilities; +using Reactor.Utilities.Extensions; +using UnityEngine; + +namespace MiraAPI.Patches.Colors; + +[HarmonyPatch(typeof(PlayerMaterial))] +internal static class SetPlayerMaterialPatch +{ + [HarmonyPatch(nameof(PlayerMaterial.SetColors), typeof(int), typeof(Renderer))] + public static bool Prefix(int colorId, Renderer rend) + { + rend.gameObject.GetOrAddComponent().SetColor( + PaletteManager.TryGetCustomColor(colorId, out var color) && color is VariableColor variable + ? variable + : null); + return !PaletteManager.IsVariable(colorId); + } + + [HarmonyPatch(nameof(PlayerMaterial.SetColors), typeof(Color), typeof(Renderer))] + public static void Prefix(Renderer rend) + { + rend.gameObject.GetOrAddComponent().SetColor(null); + } +} + +[HarmonyPatch(typeof(PlayerTab), nameof(PlayerTab.Update))] +internal static class PlayerTabPatch +{ + public static void Postfix(PlayerTab __instance) + { + for (var i = 0; i < __instance.ColorChips.Count; i++) + { + __instance.ColorChips[i].Inner.SpriteColor = PaletteManager.GetMainColor(i); + } + } +} + +[HarmonyPatch(typeof(ChatNotification), nameof(ChatNotification.Update))] +internal static class ChatNotifRainbowPatch +{ + private static readonly Regex RichTags = new(@"<[^>]*>", default, Regex.InfiniteMatchTimeout); + + public static void Postfix(ChatNotification __instance) + { + if (!__instance.gameObject.active) + return; + + var colorId = __instance.player.cosmetics.ColorId; + + if (!PaletteManager.IsVariable(colorId)) + return; + + var color = PaletteManager.GetMainColor(colorId); + var str = color.ToHtmlStringRGBA(); + var nameText = __instance.playerNameText; + nameText.text = "" + RichTags.Replace(nameText.text, string.Empty); + nameText.outlineColor = color.GetShadowColor(0.5f); + } +} + +[HarmonyPatch(typeof(HostInfoPanel), nameof(HostInfoPanel.Update))] +internal static class RainbowLobbyInfoPanePatch +{ + [SuppressMessage("Style", "IDE0045:Convert to conditional expression", Justification = "Operator becomes too large.")] + public static void Postfix(HostInfoPanel __instance) + { + if (!__instance.gameObject.activeInHierarchy) + return; + + var colorId = __instance.player.cosmetics.ColorId; + + if (!PaletteManager.IsVariable(colorId)) + return; + + var host = GameData.Instance.GetHost(); + + if (!host) + return; + + var text = PaletteManager.GetMainColor(colorId).ToHtmlStringRGBA(); + var label = __instance.hostLabel; + label.text = TranslationController.Instance.GetString(StringNames.HostNounLabel); + + if (__instance.ShouldBoldenHostLabel(DataManager.Settings.Language.CurrentLanguage)) + { + label.text = "" + label.text + ""; + } + + if (AmongUsClient.Instance.AmHost) + { + __instance.playerName.text = (string.IsNullOrEmpty(host.PlayerName) + ? "..." + : $"{host.PlayerName}") + + " " + + TranslationController.Instance.GetString(StringNames.HostYouLabel); + } + else + { + __instance.playerName.text = + (string.IsNullOrEmpty(host.PlayerName) ? "..." : $"{host.PlayerName}") + + " (" + __instance.player.ColorBlindName + ")"; + } + } +} diff --git a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs index e65f4c3..7e2b312 100644 --- a/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs +++ b/MiraAPI/Patches/Options/GameOptionsMenuPatch.cs @@ -25,9 +25,9 @@ namespace MiraAPI.Patches.Options; [HarmonyPatch(typeof(GameOptionsMenu))] internal static class GameOptionsMenuPatch { - private static readonly List VanillaHeaders = new(); - private static readonly List VanillaOptions = new(); - private static readonly List MainOptions = new(); + private static readonly List VanillaHeaders = []; + private static readonly List VanillaOptions = []; + private static readonly List MainOptions = []; private static readonly Dictionary> GameModeGroups = []; private static readonly Dictionary> GameModeOptions = []; private static readonly Dictionary> GameModeHeaders = []; diff --git a/MiraAPI/Patches/Options/SettingPatches.cs b/MiraAPI/Patches/Options/SettingPatches.cs index 8fd6581..59b956c 100644 --- a/MiraAPI/Patches/Options/SettingPatches.cs +++ b/MiraAPI/Patches/Options/SettingPatches.cs @@ -1,5 +1,4 @@ -using System.Diagnostics.CodeAnalysis; -using System.Globalization; +using System.Globalization; using System.Linq; using HarmonyLib; using MiraAPI.GameOptions; @@ -24,7 +23,6 @@ public static class SettingPatches /// to skip original method. [HarmonyPrefix] [HarmonyPatch(typeof(FloatGameSetting), nameof(FloatGameSetting.GetValueString))] - [SuppressMessage("Style", "IDE0045:Convert to conditional expression", Justification = "Warning cascades into forcing the entire tree to be ternary operators.")] public static bool ValueStringPatch( FloatGameSetting __instance, ref string __result, diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 92080e8..508b3f7 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -33,20 +33,13 @@ public static class RoleGuidePatches private static readonly List RoleEntries = []; private static readonly List ModifierEntries = []; - public static GameObject SearchComponentContainer; - public static PassiveButton SearchIconButton; + private static GameObject searchComponentContainer; private static SpriteRenderer _searchIconIdle; private static SpriteRenderer _searchIconHover; - public static TextMeshPro OrderSortingTmp; - public static PassiveButton SearchSortingOrderButton; private static SpriteRenderer _searchSortingOrderIdle; private static SpriteRenderer _searchSortingOrderHover; - public static TextMeshPro GroupSortingTmp; - public static PassiveButton SearchSortingGroupButton; private static SpriteRenderer _searchSortingGroupIdle; private static SpriteRenderer _searchSortingGroupHover; - public static TextMeshPro FilterSortingTmp; - public static PassiveButton SearchSortingFilterButton; private static SpriteRenderer _searchSortingFilterIdle; private static SpriteRenderer _searchSortingFilterHover; private static Scroller _rolesScroller; @@ -153,10 +146,6 @@ public static class RoleGuidePatches _advancedInfoTabScroller = null!; _currentAdvancedTabObject = null!; _loadingSpinner = null; - SearchIconButton = null!; - SearchSortingOrderButton = null!; - SearchSortingGroupButton = null!; - SearchSortingFilterButton = null!; _searchIconIdle = null!; _searchIconHover = null!; _searchSortingOrderIdle = null!; @@ -260,6 +249,7 @@ public static class RoleGuidePatches _titleText.transform.GetComponent().Destroy(); } } + if (!_titleIcon) { var titleIconObj = new GameObject("TitleIcon"); @@ -369,18 +359,18 @@ public static class RoleGuidePatches _guide = instance; _loading = false; - SearchComponentContainer = new GameObject(); - SearchComponentContainer.transform.SetParent(instance.MatchInfoParent.transform); - SearchComponentContainer.transform.localPosition = Vector3.zero; - instance.MatchInfoParent.transform.GetChild(1).transform.SetParent(SearchComponentContainer.transform); + searchComponentContainer = new GameObject(); + searchComponentContainer.transform.SetParent(instance.MatchInfoParent.transform); + searchComponentContainer.transform.localPosition = Vector3.zero; + instance.MatchInfoParent.transform.GetChild(1).transform.SetParent(searchComponentContainer.transform); var sortingOrderButton = Object.Instantiate( HudManager.Instance.SettingsButton, - SearchComponentContainer.transform); + searchComponentContainer.transform); sortingOrderButton.gameObject.SetActive(true); - SearchSortingOrderButton = sortingOrderButton.GetComponent(); + var searchSortingOrderButton = sortingOrderButton.GetComponent(); SetupSearchButton( - SearchSortingOrderButton, + searchSortingOrderButton, "SearchSortingOrderButton", SwitchMethod, GetSortMethodSprite(_sortMethod, false), @@ -391,11 +381,11 @@ public static class RoleGuidePatches var sortingGroupButton = Object.Instantiate( sortingOrderButton, - SearchComponentContainer.transform); + searchComponentContainer.transform); sortingGroupButton.gameObject.SetActive(true); - SearchSortingGroupButton = sortingGroupButton.GetComponent(); + var searchSortingGroupButton = sortingGroupButton.GetComponent(); SetupSearchButton( - SearchSortingGroupButton, + searchSortingGroupButton, "SearchSortingGroupButton", SwitchGrouping, GetSortGroupSprite(_sortGrouping, false), @@ -406,11 +396,11 @@ public static class RoleGuidePatches var sortingFilterButton = Object.Instantiate( sortingOrderButton, - SearchComponentContainer.transform); + searchComponentContainer.transform); sortingFilterButton.gameObject.SetActive(true); - SearchSortingFilterButton = sortingFilterButton.GetComponent(); + var searchSortingFilterButton = sortingFilterButton.GetComponent(); SetupSearchButton( - SearchSortingFilterButton, + searchSortingFilterButton, "SearchSortingFilterButton", SwitchFilter, GetSortFilterSprite(_sortFilter, false), @@ -463,9 +453,8 @@ public static class RoleGuidePatches sortingOrderButton, sortingOrderButton.transform.parent); searchIconButton.gameObject.SetActive(true); - SearchIconButton = searchIconButton.GetComponent(); SetupSearchButton( - SearchIconButton, + searchIconButton.GetComponent(), "SearchIconButton", ClearSearchText, MiraAssets.SearchIconIdleSprite, @@ -476,7 +465,7 @@ public static class RoleGuidePatches var searchBox = Object.Instantiate( instance.TabButtons[2].gameObject, - SearchComponentContainer.transform); + searchComponentContainer.transform); var searchButton = searchBox.GetComponent(); var tmpText = searchButton.transform.GetChild(0).GetComponent(); tmpText.GetComponent().Destroy(); @@ -621,26 +610,26 @@ public static class RoleGuidePatches modifiersButton.OnClick.AddListener((Action)(() => OpenModifiersTab())); instance.TabButtons.Add(modifiersButton); - OrderSortingTmp = Object.Instantiate( + var orderSortingTmp = Object.Instantiate( _titleText, sortingOrderButton.transform); - OrderSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.OrderLabel"); - OrderSortingTmp.fontSize = OrderSortingTmp.fontSizeMax = 3f; - OrderSortingTmp.alignment = TextAlignmentOptions.Center; - OrderSortingTmp.horizontalAlignment = HorizontalAlignmentOptions.Center; - OrderSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0); + orderSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.OrderLabel"); + orderSortingTmp.fontSize = orderSortingTmp.fontSizeMax = 3f; + orderSortingTmp.alignment = TextAlignmentOptions.Center; + orderSortingTmp.horizontalAlignment = HorizontalAlignmentOptions.Center; + orderSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0); - GroupSortingTmp = Object.Instantiate( - OrderSortingTmp, + var groupSortingTmp = Object.Instantiate( + orderSortingTmp, sortingGroupButton.transform); - OrderSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.GroupingLabel"); - GroupSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0); + orderSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.GroupingLabel"); + groupSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0); - FilterSortingTmp = Object.Instantiate( - OrderSortingTmp, + var filterSortingTmp = Object.Instantiate( + orderSortingTmp, sortingFilterButton.transform); - FilterSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.FilterLabel"); - FilterSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0); + filterSortingTmp.text = MiraLocaleManager.Get("MiraApi.Wiki.FilterLabel"); + filterSortingTmp.transform.localPosition = new Vector3(-0.65f, 0.4f, 0); playerButton.transform.localPosition = new Vector3(-3.6f, 0.656f, -0.2f); settingButton.transform.localPosition = new Vector3(-3.6f, 0.056f, -0.2f); @@ -731,11 +720,17 @@ public static class RoleGuidePatches } } - public static void OpenModifiersTab() => OpenTab(ModifiersTabIndex); + public static void OpenModifiersTab() + { + OpenTab(ModifiersTabIndex); + } - public static void OpenRolesTab() => OpenTab(RolesTabIndex); + public static void OpenRolesTab() + { + OpenTab(RolesTabIndex); + } - public sealed class DetailedPanel(string title, string category, string modId, RoleBehaviour? role, GameModifier? modifier) + private sealed class DetailedPanel(string title, string category, string modId, RoleBehaviour? role, GameModifier? modifier) { public MatchInfoRolePanel? Panel { get; set; } public string Title { get; } = title; @@ -803,14 +798,7 @@ public static class RoleGuidePatches private static IEnumerator CoPopulate(float delay) { - if (delay > 0f) - { - yield return new WaitForSeconds(delay); - } - else - { - yield return null; - } + yield return delay > 0f ? new WaitForSeconds(delay) : null; if (!_guide) { @@ -1048,14 +1036,8 @@ public static class RoleGuidePatches public static void SwitchFilter() { var stepUp = (SortingFilter)((int)_sortFilter + 1); - if (Enum.IsDefined(stepUp)) - { - _sortFilter = stepUp; - } - else - { - _sortFilter = SortingFilter.EnabledOnly; - } + + _sortFilter = Enum.IsDefined(stepUp) ? stepUp : SortingFilter.EnabledOnly; MiraApiPlugin.WikiSortingFilter.Value = _sortFilter; @@ -1094,14 +1076,8 @@ public static class RoleGuidePatches public static void SwitchMethod() { var stepUp = (SortingMethod)((int)_sortMethod + 1); - if (Enum.IsDefined(stepUp)) - { - _sortMethod = stepUp; - } - else - { - _sortMethod = SortingMethod.Alphabetical; - } + + _sortMethod = Enum.IsDefined(stepUp) ? stepUp : SortingMethod.Alphabetical; MiraApiPlugin.WikiSortingMethod.Value = _sortMethod; @@ -1288,7 +1264,7 @@ public static class RoleGuidePatches } _advancedInfoTabScroller?.gameObject.SetActive(false); - SearchComponentContainer?.SetActive(true); + searchComponentContainer?.SetActive(true); UpdateSpinner(); } @@ -1341,9 +1317,9 @@ public static class RoleGuidePatches _currentAdvancedTabObject.Destroy(); } - if (SearchComponentContainer) + if (searchComponentContainer) { - SearchComponentContainer.SetActive(false); + searchComponentContainer.SetActive(false); } if (_advancedInfoTabScroller == null) @@ -1543,20 +1519,20 @@ public static class RoleGuidePatches } } -public enum SortingGroups +internal enum SortingGroups { Ungrouped, Faction, AmountChance, } -public enum SortingMethod +internal enum SortingMethod { Alphabetical, AlphabeticalDescending, } -public enum SortingFilter +internal enum SortingFilter { EnabledOnly, DisabledOnly, diff --git a/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs b/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs index f047fd3..b856d22 100644 --- a/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs +++ b/MiraAPI/Patches/Stubs/RoleBehaviourStubs.cs @@ -39,10 +39,10 @@ public static class RoleBehaviourStubs } /// - /// Stub method for . + /// Stub method for . /// /// The object. - /// The to append the task hint to. + /// The to append the task hint to. [HarmonyReversePatch] [HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.AppendTaskHint), typeof(Il2CppSystem.Text.StringBuilder))] [MethodImpl(MethodImplOptions.NoInlining)] diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index a3b5158..b46d155 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -404,7 +404,7 @@ public sealed class MiraPluginManager foreach (var property in type.GetProperties()) { - if (property.PropertyType != typeof(CustomColor)) + if (!property.PropertyType.IsAssignableTo(typeof(CustomColor))) { continue; } @@ -416,6 +416,9 @@ public sealed class MiraPluginManager } PaletteManager.CustomColors.Add(color); + + if (color is VariableColor) + PaletteManager.VariableColorNames.Add(color.Name); } foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(CustomColor)))) diff --git a/MiraAPI/Presets/SavePresetPopup.cs b/MiraAPI/Presets/SavePresetPopup.cs index b9932ff..9405e83 100644 --- a/MiraAPI/Presets/SavePresetPopup.cs +++ b/MiraAPI/Presets/SavePresetPopup.cs @@ -16,6 +16,7 @@ namespace MiraAPI.Presets; [RegisterInIl2Cpp] [SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity convention.")] +[SuppressMessage("Design", "CA1050:Declare types in namespaces", Justification = "Unity script.")] #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member (Justification: Unity fields; ignore.) public class SavePresetPopup(nint cppPtr) : Minigame(cppPtr) { diff --git a/MiraAPI/Resources/Locale/en_US.xml b/MiraAPI/Resources/Locale/en_US.xml index ba60484..4ee0714 100644 --- a/MiraAPI/Resources/Locale/en_US.xml +++ b/MiraAPI/Resources/Locale/en_US.xml @@ -12,6 +12,7 @@ Display Wiki on Bottom Row Enable Sabotage Flashes Enable Sabotage Blare + Enable Variable Color Changes RETURN TO ROLE SETTINGS Custom Category 1 Custom Category 2 diff --git a/MiraAPI/Roles/ICustomRole.cs b/MiraAPI/Roles/ICustomRole.cs index 4d5e247..88b1f85 100644 --- a/MiraAPI/Roles/ICustomRole.cs +++ b/MiraAPI/Roles/ICustomRole.cs @@ -345,7 +345,14 @@ public interface ICustomRole : IOptionable /// Gets the list of abilities or other similar information to display below the text of a role's wiki page. /// [HideFromIl2Cpp] - public List WikiAbilities => []; + List WikiAbilities => []; } +/// +/// A data holder of an ability's wiki entry. +/// +/// The name of the ability. +/// The type of the ability. +/// The description of the ability. +/// The icon sprite of the ability. public record struct AdvancedWikiAbilityDescription(string Name, string AbilityType, string Description, LoadableAsset Icon); diff --git a/MiraAPI/Translation/MiraLocaleManager.cs b/MiraAPI/Translation/MiraLocaleManager.cs index d26f422..0bce8ca 100644 --- a/MiraAPI/Translation/MiraLocaleManager.cs +++ b/MiraAPI/Translation/MiraLocaleManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -162,7 +161,6 @@ public static class MiraLocaleManager /// The string id to find. /// Fallback string to use if no translation is found. /// A based on the key provided. - [SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "Warning cascades into forcing the entire tree to be ternary operators.")] public static string Get(MiraLanguage language, string key, string fallback = "") { if (Locale.TryGetValue(language, out var translations) && diff --git a/MiraAPI/Utilities/ControllableComparer.cs b/MiraAPI/Utilities/ControllableComparer.cs index 49c4b0e..5e7ac70 100644 --- a/MiraAPI/Utilities/ControllableComparer.cs +++ b/MiraAPI/Utilities/ControllableComparer.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; namespace MiraAPI.Utilities; @@ -9,7 +8,6 @@ internal sealed class ControllableComparer(T[] forcedToBottom, T[] forcedToTo where T : IComparable { /// - [SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "Warning cascades into forcing the entire tree to be ternary operators.")] public int Compare(T? x, T? y) { if ((forcedToBottom.Contains(x) && forcedToBottom.Contains(y)) || (forcedToTop.Contains(x) && forcedToTop.Contains(y))) diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index a39a6c6..21112f6 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -567,7 +567,28 @@ public static class Extensions /// The darkened . public static Color32 GetShadowColor(this Color32 color, byte darknessAmount) { - return new Color32((byte)Mathf.Clamp(color.r - darknessAmount, 0, 255), (byte)Mathf.Clamp(color.g - darknessAmount, 0, 255), (byte)Mathf.Clamp(color.b - darknessAmount, 0, 255), byte.MaxValue); + return + new Color32( + (byte)Mathf.Clamp(color.r - darknessAmount, 0, 255), + (byte)Mathf.Clamp(color.g - darknessAmount, 0, 255), + (byte)Mathf.Clamp(color.b - darknessAmount, 0, 255), + color.a); + } + + /// + /// Gets a darkened version of a . + /// + /// The original . + /// A darkness amount between 0 and 1. + /// The darkened . + public static Color GetShadowColor(this Color color, float darknessAmount) + { + return + new Color( + MathUtilities.Clamp01(color.r - darknessAmount), + MathUtilities.Clamp01(color.g - darknessAmount), + MathUtilities.Clamp01(color.b - darknessAmount), + color.a); } /// @@ -772,4 +793,17 @@ public static class Extensions userData.keyboardMaps[0].actionElementMaps.Add(map); return action; } + + /// + /// Gets a component that is attached to the provided , attaching and returning the component if it wasn't found. + /// + /// The component type. + /// The to get the component from. + /// The component. + public static T GetOrAddComponent(this GameObject go) + where T : Component + { + var comp = go.GetComponent(); + return comp ? comp : go.AddComponent(); + } } diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 773a4ae..413fb59 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -32,6 +32,11 @@ public static class Helpers { private static readonly Regex TrailingZeroRegex = new(@"\.0+(?!\d)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100)); + /// + /// Gets all of the options stored within the provided type. + /// + /// The type to get the options of. + /// A readonly list of options from the provided type. public static ReadOnlyCollection? GetModdedOptionsForType(Type classType) { var group = ModdedOptionsManager.Groups.FirstOrDefault(x => x.OptionableType == classType); @@ -39,6 +44,11 @@ public static class Helpers return group?.Children; } + /// + /// Gets the option text of the provided type. + /// + /// The type to get the text for. + /// The options text of the provided type. public static string GetOptionsText(Type classType) { var group = ModdedOptionsManager.Groups.FirstOrDefault(x => x.OptionableType == classType); @@ -836,7 +846,7 @@ public static class Helpers obj.transform.SetParent(parent.Inner.transform); obj.transform.localPosition = new Vector3(0f, 0.5f, 0f); var scrollHeight = variableTextSizing - ? (desc.textBounds.size.y - 2.5f) + maxTextSize + 0.475f + ? desc.textBounds.size.y - 2.5f + maxTextSize + 0.475f : desc.textBounds.size.y - 2.5f + Mathf.Ceil(num / 2f) * 1.45f; parent.SetYBoundsMax(Mathf.Clamp(scrollHeight, 0f, 999f)); return obj; @@ -857,6 +867,7 @@ public static class Helpers panel.roleDescription.fontSizeMin = 1.5f; panel.roleDescription.ForceMeshUpdate(); } + panel.roleIcon.sprite = ability.Icon.LoadAsset(); panel.roleIcon.SetSizeLimit(0.13f); diff --git a/MiraAPI/Utilities/MathUtilities.cs b/MiraAPI/Utilities/MathUtilities.cs new file mode 100644 index 0000000..a505a36 --- /dev/null +++ b/MiraAPI/Utilities/MathUtilities.cs @@ -0,0 +1,82 @@ +using System; + +namespace MiraAPI.Utilities; + +/// +/// Provides mathematical utility methods. +/// +public static class MathUtilities +{ + /// + /// Linearly interpolates between two values, clamping the interpolant between 0 and 1. + /// + /// The starting value. + /// The ending value. + /// The interpolation value, automatically clamped to a 0 to 1 range. + /// The interpolated float. + public static float Lerp(float a, float b, float t) + { + return a + ((b - a) * Clamp01(t)); + } + + /// + /// Linearly interpolates between two values without clamping the interpolant. + /// + /// The starting value. + /// The ending value. + /// The interpolation value. + /// The interpolated float. + public static float LerpUnclamped(float a, float b, float t) + { + return a + ((b - a) * t); + } + + /// + /// Clamps a floating-point value strictly between 0 and 1. + /// + /// The floating-point value to clamp. + /// The clamped value. + public static float Clamp01(float value) + { + return Math.Clamp(value, 0f, 1f); + } + + /// + /// Ping-pongs a value back and forth between a minimum and maximum value. + /// + /// The input time or interpolant. + /// The minimum boundary of the ping-pong wave. + /// The maximum boundary of the ping-pong wave. + /// The multiplier for the speed of the ping-pong effect. + /// A value oscillating continuously between and . + public static float PingPong(float t, float min, float max, float speed = 1f) + { + return min + PingPong(t, max - min, speed); + } + + /// + /// Ping-pongs a value back and forth between 0 and a specified length. + /// + /// The input time or interpolant. + /// The maximum value to bounce against, starting from 0. + /// The multiplier for the speed of the ping-pong effect. + /// A value oscillating continuously between 0 and . + public static float PingPong(float t, float length, float speed = 1f) + { + var dx = speed * t; + var floor = (int)MathF.Floor(dx); + var phase = floor % 2; // 0 = forward, 1 = backward + return Clamp01(((dx - floor) * ((2 * phase) - 1)) + 1 - phase) * length; + } + + /// + /// Loops a value strictly between 0 and a maximum length, behaving like a standard float modulo. + /// + /// The input value to loop. + /// The maximum length of the repeating sequence. + /// The wrapped value smoothly looping back to 0 upon reaching . + public static float Repeat(float t, float length) + { + return Math.Clamp(t - (MathF.Floor(t / length) * length), 0f, length); + } +} diff --git a/README.md b/README.md index 84823f4..59dfa6e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ -[![](https://dcbadge.limes.pink/api/server/AEfHJGwggC)](https://discord.gg/AEfHJGwggC) - [English] | [简体中文](Translated/README_zh.md) +[![Starlight badge](https://img.shields.io/badge/Download%20On-Starlight-d3b65e?style=for-the-badge)](https://starlight.allofus.dev/mods/mira.api) +[![](https://dcbadge.limes.pink/api/server/AEfHJGwggC)](https://discord.gg/AEfHJGwggC) + > This mod is not affiliated with Among Us or Innersloth LLC, and the content contained therein is not endorsed or otherwise sponsored by Innersloth LLC. Portions of the materials contained herein are property of Innersloth LLC. © Innersloth LLC. # Mira API