diff --git a/MinecraftClient/Commands/EffectsCommand.cs b/MinecraftClient/Commands/EffectsCommand.cs new file mode 100644 index 00000000..700764aa --- /dev/null +++ b/MinecraftClient/Commands/EffectsCommand.cs @@ -0,0 +1,67 @@ +ο»Ώusing System.Linq; +using System.Text; +using Brigadier.NET; +using Brigadier.NET.Builder; +using MinecraftClient.CommandHandler; + +namespace MinecraftClient.Commands +{ + public class EffectsCommand : Command + { + public override string CmdName { get { return "effects"; } } + public override string CmdUsage { get { return "effects"; } } + public override string CmdDesc { get { return Translations.cmd_effects_desc; } } + + public override void RegisterCommand(CommandDispatcher dispatcher) + { + dispatcher.Register(l => l.Literal("help") + .Then(l => l.Literal(CmdName) + .Executes(r => GetUsage(r.Source, string.Empty)) + ) + ); + + dispatcher.Register(l => l.Literal(CmdName) + .Executes(r => ShowEffects(r.Source)) + .Then(l => l.Literal("_help") + .Executes(r => GetUsage(r.Source, string.Empty)) + .Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName))) + ); + } + + private int GetUsage(CmdResult r, string? cmd) + { + return r.SetAndReturn(cmd switch + { +#pragma warning disable format // @formatter:off + _ => GetCmdDescTranslated(), +#pragma warning restore format // @formatter:on + }); + } + + private int ShowEffects(CmdResult r) + { + McClient handler = CmdResult.currentHandler!; + if (!handler.GetEntityHandlingEnabled()) + return r.SetAndReturn(CmdResult.Status.FailNeedEntity); + + var effects = handler.GetPlayerEffects() + .Values + .Where(effectData => !effectData.IsExpired) + .OrderBy(effectData => effectData.Effect) + .ToArray(); + + if (effects.Length == 0) + return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_effects_none); + + StringBuilder response = new(); + response.AppendLine(Translations.cmd_effects_header); + foreach (var effectData in effects) + { + response.AppendLine(string.Format(Translations.cmd_effects_entry, + effectData.GetDisplayName(), effectData.GetRemainingDurationText())); + } + + return r.SetAndReturn(CmdResult.Status.Done, response.ToString().TrimEnd()); + } + } +} diff --git a/MinecraftClient/Inventory/EffectData.cs b/MinecraftClient/Inventory/EffectData.cs new file mode 100644 index 00000000..c13e4312 --- /dev/null +++ b/MinecraftClient/Inventory/EffectData.cs @@ -0,0 +1,194 @@ +namespace MinecraftClient.Inventory; + +using System; +using System.Collections.Generic; +using System.Linq; +using MinecraftClient.Protocol; +using MinecraftClient.Protocol.Message; + +/// +/// Represents an active status effect on an entity +/// +public class EffectData +{ + /// + /// The type of effect + /// + public Effects Effect { get; set; } + + /// + /// Effect amplifier (level - 1, e.g., 0 = level I, 1 = level II) + /// + public int Amplifier { get; set; } + + /// + /// Duration in ticks (20 ticks = 1 second). -1 for infinite. + /// + public int Duration { get; set; } + + /// + /// Effect flags (ambient, show particles, show icon) + /// + public byte Flags { get; set; } + + /// + /// Time when the effect was applied + /// + public DateTime StartTime { get; set; } + + public EffectData(Effects effect, int amplifier, int duration, byte flags) + { + Effect = effect; + Amplifier = amplifier; + Duration = duration; + Flags = flags; + StartTime = DateTime.UtcNow; + } + + /// + /// Check if this is an infinite duration effect + /// + public bool IsInfinite => Duration == -1 || Duration == int.MaxValue; + + /// + /// Check if the effect has expired + /// + public bool IsExpired + { + get + { + if (IsInfinite) return false; + return GetElapsedTicks() >= Duration; + } + } + + /// + /// Get remaining duration in ticks + /// + public int RemainingTicks + { + get + { + if (IsInfinite) return -1; + return Math.Max(0, Duration - GetElapsedTicks()); + } + } + + /// + /// Get remaining duration in seconds + /// + public int RemainingSeconds + { + get + { + if (IsInfinite) return -1; + return (RemainingTicks + 19) / 20; + } + } + + /// + /// Get the translated effect name from Minecraft translations + /// + public string GetTranslatedName() + { + var key = $"effect.minecraft.{Effect.ToString().ToUnderscoreCase()}"; + var translated = ChatParser.TranslateString(key); + return string.IsNullOrEmpty(translated) ? Effect.ToString() : translated; + } + + /// + /// Get the translated effect name with level when applicable + /// + public string GetDisplayName() + { + string translatedName = GetTranslatedName(); + if (Amplifier <= 0) + return translatedName; + + return string.Format(Translations.effect_name_with_amplifier, translatedName, + EnchantmentMapping.ConvertLevelToRomanNumbers(Amplifier + 1)); + } + + /// + /// Get the translated effect name prefixed with the best-fit indefinite article + /// + public string GetDisplayNameWithArticle() + { + string displayName = GetDisplayName(); + char? firstLetter = displayName + .TrimStart() + .FirstOrDefault(char.IsLetter); + + if (firstLetter is null) + return displayName; + + string article = "AEIOUaeiou".Contains(firstLetter.Value) + ? Translations.effect_article_an + : Translations.effect_article_a; + return $"{article} {displayName}"; + } + + /// + /// Get the configured short duration label for the remaining time + /// + public string GetRemainingDurationText() + { + return FormatShortDuration(RemainingSeconds); + } + + /// + /// Get the configured short duration label for the initial effect duration + /// + public string GetInitialDurationText() + { + if (IsInfinite) + return Translations.effect_duration_unlimited; + + int durationSeconds = (Duration + 19) / 20; + return FormatShortDuration(durationSeconds); + } + + /// + /// Format a duration for compact UI output + /// + /// Duration in seconds, -1 for unlimited + public static string FormatShortDuration(int seconds) + { + if (seconds < 0) + return Translations.effect_duration_short_unlimited; + + if (seconds < 60) + return string.Format(Translations.effect_duration_short_seconds, seconds); + + int minutes = seconds / 60; + int remainingSeconds = seconds % 60; + if (seconds < 3600) + { + return remainingSeconds == 0 + ? string.Format(Translations.effect_duration_short_minutes, minutes) + : string.Format(Translations.effect_duration_short_minutes_seconds, minutes, remainingSeconds); + } + + int hours = seconds / 3600; + int remainingMinutes = (seconds % 3600) / 60; + return remainingMinutes == 0 + ? string.Format(Translations.effect_duration_short_hours, hours) + : string.Format(Translations.effect_duration_short_hours_minutes, hours, remainingMinutes); + } + + private int GetElapsedTicks() + { + return (int)((DateTime.UtcNow - StartTime).TotalMilliseconds / 50); + } +} + +/// +/// Extension method for converting PascalCase to snake_case +/// +public static class StringExtensions +{ + public static string ToUnderscoreCase(this string str) + { + return string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x.ToString() : x.ToString())).ToLower(); + } +} diff --git a/MinecraftClient/Mapping/Entity.cs b/MinecraftClient/Mapping/Entity.cs index 3d1dd67e..33b250f0 100644 --- a/MinecraftClient/Mapping/Entity.cs +++ b/MinecraftClient/Mapping/Entity.cs @@ -99,6 +99,11 @@ namespace MinecraftClient.Mapping /// public Dictionary Equipment; + /// + /// Active status effects on this entity + /// + public Dictionary ActiveEffects { get; private set; } + /// /// Create a new entity based on Entity ID, Entity Type and location /// @@ -112,6 +117,7 @@ namespace MinecraftClient.Mapping Location = location; Health = 1.0f; Equipment = new Dictionary(); + ActiveEffects = new Dictionary(); Item = new Item(ItemType.Air, 0, null); } @@ -128,6 +134,7 @@ namespace MinecraftClient.Mapping Location = location; Health = 1.0f; Equipment = new Dictionary(); + ActiveEffects = new Dictionary(); Item = new Item(ItemType.Air, 0, null); Yaw = yaw * (1F / 256) * 360; // to angle in 360 degree Pitch = pitch * (1F / 256) * 360; @@ -151,6 +158,7 @@ namespace MinecraftClient.Mapping Name = name; Health = 1.0f; Equipment = new Dictionary(); + ActiveEffects = new Dictionary(); Item = new Item(ItemType.Air, 0, null); Yaw = yaw * (1F / 256) * 360; // to angle in 360 degree Pitch = pitch * (1F / 256) * 360; diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 4d82a1d4..e76b9b80 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -102,6 +102,9 @@ namespace MinecraftClient private int playerLevel; private int playerTotalExperience; private byte CurrentSlot = 0; + + // player effects + private readonly Dictionary playerEffects = new(); // Sneaking public bool IsSneaking { get; set; } = false; @@ -141,6 +144,16 @@ namespace MinecraftClient public bool GetIsSupportPreviewsChat() { return isSupportPreviewsChat; } public float GetHealth() { return playerHealth; } public int GetSaturation() { return playerFoodSaturation; } + + /// + /// Get the player's active effects + /// + /// Dictionary of active effects + public Dictionary GetPlayerEffects() + { + return new Dictionary(playerEffects); + } + public int GetLevel() { return playerLevel; } public int GetTotalExperience() { return playerTotalExperience; } public byte GetCurrentSlot() { return CurrentSlot; } @@ -616,6 +629,26 @@ namespace MinecraftClient SendRespawnPacket(); } + // Check for expired effects + if (playerEffects.Count > 0) + { + var expiredEffects = playerEffects + .Where(e => e.Value.IsExpired) + .Select(e => e.Key) + .ToList(); + + foreach (var effect in expiredEffects) + { + if (!playerEffects.Remove(effect, out var effectData)) + continue; + + ConsoleIO.WriteLine(string.Format(Translations.bot_effect_expired, effectData.GetDisplayName())); + + if (entities.TryGetValue(playerEntityID, out var playerEntity)) + playerEntity.ActiveEffects.Remove(effect); + } + } + lock (threadTasksLock) { while (threadTasks.Count > 0) @@ -3394,8 +3427,61 @@ namespace MinecraftClient /// public void OnEntityEffect(int entityid, Effects effect, int amplifier, int duration, byte flags, bool hasFactorData, Dictionary? factorCodec) { - if (entities.ContainsKey(entityid)) - DispatchBotEvent(bot => bot.OnEntityEffect(entities[entityid], effect, amplifier, duration, flags)); + Entity? entity = null; + if (entities.TryGetValue(entityid, out var trackedEntity)) + { + entity = trackedEntity; + } + + var effectData = new EffectData(effect, amplifier, duration, flags); + entity?.ActiveEffects[effect] = effectData; + + if (entityid == playerEntityID) + { + playerEffects.TryGetValue(effect, out var previousPlayerEffect); + playerEffects[effect] = effectData; + + bool shouldAnnounceEffectGain = previousPlayerEffect is null + || previousPlayerEffect.Amplifier != amplifier + || (effectData.IsInfinite && !previousPlayerEffect.IsInfinite) + || (!effectData.IsInfinite && duration > previousPlayerEffect.RemainingTicks + 20); + + if (shouldAnnounceEffectGain) + { + ConsoleIO.WriteLine(string.Format(Translations.bot_effect_gained, + effectData.GetDisplayNameWithArticle(), effectData.GetInitialDurationText())); + } + } + + if (entity is not null) + DispatchBotEvent(bot => bot.OnEntityEffect(entity, effect, amplifier, duration, flags)); + } + + /// + /// Called when an entity has an effect removed + /// + /// Entity ID + /// Effect that was removed + public void OnRemoveEntityEffect(int entityid, Effects effect) + { + Entity? entity = null; + EffectData? removedEffectData = null; + + if (entities.TryGetValue(entityid, out var trackedEntity)) + { + entity = trackedEntity; + if (entity.ActiveEffects.Remove(effect, out var entityEffectData)) + removedEffectData = entityEffectData; + } + + if (entityid == playerEntityID && playerEffects.Remove(effect, out var playerEffectData)) + removedEffectData ??= playerEffectData; + + if (entityid == playerEntityID && removedEffectData is not null) + ConsoleIO.WriteLine(string.Format(Translations.bot_effect_expired, removedEffectData.GetDisplayName())); + + if (entity is not null) + DispatchBotEvent(bot => bot.OnRemoveEntityEffect(entity, effect)); } /// diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index b9128587..be0914bc 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -2513,11 +2513,12 @@ namespace MinecraftClient.Protocol.Handlers { var entityId = dataTypes.ReadNextVarInt(packetData); var effectId = protocolVersion >= MC_1_18_2_Version - ? dataTypes.ReadNextVarInt(packetData) + ? dataTypes.ReadNextVarInt(packetData) + 1 : dataTypes.ReadNextByte(packetData); - if (Enum.TryParse(effectId.ToString(), out Effects effect)) + if (Enum.IsDefined(typeof(Effects), effectId)) { + var effect = (Effects)effectId; var amplifier = dataTypes.ReadNextByte(packetData); var duration = dataTypes.ReadNextVarInt(packetData); var flags = dataTypes.ReadNextByte(packetData); @@ -2536,6 +2537,22 @@ namespace MinecraftClient.Protocol.Handlers } } + break; + case PacketTypesIn.RemoveEntityEffect: + if (handler.GetEntityHandlingEnabled()) + { + var entityId = dataTypes.ReadNextVarInt(packetData); + var effectId = protocolVersion >= MC_1_18_2_Version + ? dataTypes.ReadNextVarInt(packetData) + 1 + : dataTypes.ReadNextByte(packetData); + + if (Enum.IsDefined(typeof(Effects), effectId)) + { + var effect = (Effects)effectId; + handler.OnRemoveEntityEffect(entityId, effect); + } + } + break; case PacketTypesIn.DestroyEntities: if (handler.GetEntityHandlingEnabled()) diff --git a/MinecraftClient/Protocol/IMinecraftComHandler.cs b/MinecraftClient/Protocol/IMinecraftComHandler.cs index 7edb83cd..94fe0590 100644 --- a/MinecraftClient/Protocol/IMinecraftComHandler.cs +++ b/MinecraftClient/Protocol/IMinecraftComHandler.cs @@ -434,6 +434,19 @@ namespace MinecraftClient.Protocol /// factorCodec void OnEntityEffect(int entityid, Effects effect, int amplifier, int duration, byte flags, bool hasFactorData, Dictionary? factorCodec); + /// + /// Called when an entity has an effect removed + /// + /// Entity ID + /// Effect that was removed + void OnRemoveEntityEffect(int entityid, Effects effect); + + /// + /// Get the player's active effects + /// + /// Dictionary of active effects + Dictionary GetPlayerEffects(); + /// /// Called when Soreboard Objective /// diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs index 143d1b26..66213cc9 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs @@ -1843,16 +1843,25 @@ namespace MinecraftClient { /// /// Looks up a localized string similar to Show inventory layout as ASCII art in inventory command.. /// - internal static string Main_Advanced_show_inventory_layout { - get { - return ResourceManager.GetString("Main.Advanced.show_inventory_layout", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to System messages for server ops.. - /// - internal static string Main_Advanced_show_system_messages { + internal static string Main_Advanced_show_inventory_layout { + get { + return ResourceManager.GetString("Main.Advanced.show_inventory_layout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show full effect names and levels in the TUI status bar instead of compact effect icons only.. + /// + internal static string Main_Advanced_show_effect_names_in_tui { + get { + return ResourceManager.GetString("Main.Advanced.show_effect_names_in_tui", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to System messages for server ops.. + /// + internal static string Main_Advanced_show_system_messages { get { return ResourceManager.GetString("Main.Advanced.show_system_messages", resourceCulture); } diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx index 06421374..6d91740b 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx @@ -705,6 +705,9 @@ Usage examples: "/tell <mybot> connect Server1", "/connect Server2" Show inventory layout as ASCII art in inventory command. + + Show full effect names and levels in the TUI status bar instead of compact effect icons only. + System messages for server ops. diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index d20d36af..d76a3bed 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -3522,6 +3522,42 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to list your currently active effects.. + /// + internal static string cmd_effects_desc { + get { + return ResourceManager.GetString("cmd.effects.desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to - {0} ({1}). + /// + internal static string cmd_effects_entry { + get { + return ResourceManager.GetString("cmd.effects.entry", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Active effects:. + /// + internal static string cmd_effects_header { + get { + return ResourceManager.GetString("cmd.effects.header", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No active effects.. + /// + internal static string cmd_effects_none { + get { + return ResourceManager.GetString("cmd.effects.none", resourceCulture); + } + } + /// /// Looks up a localized string similar to Display Health and Food saturation.. /// @@ -6520,5 +6556,113 @@ namespace MinecraftClient { return ResourceManager.GetString("tui.inventory.item_count", resourceCulture); } } + + /// + /// Looks up a localized string similar to You're now under {0} effect (Duration: {1}).. + /// + internal static string bot_effect_gained { + get { + return ResourceManager.GetString("bot.effect.gained", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Effect {0} has expired. + /// + internal static string bot_effect_expired { + get { + return ResourceManager.GetString("bot.effect.expired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unlimited. + /// + internal static string effect_duration_unlimited { + get { + return ResourceManager.GetString("effect.duration.unlimited", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to a. + /// + internal static string effect_article_a { + get { + return ResourceManager.GetString("effect.article.a", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to an. + /// + internal static string effect_article_an { + get { + return ResourceManager.GetString("effect.article.an", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}h. + /// + internal static string effect_duration_short_hours { + get { + return ResourceManager.GetString("effect.duration.short.hours", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}h {1}m. + /// + internal static string effect_duration_short_hours_minutes { + get { + return ResourceManager.GetString("effect.duration.short.hours_minutes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}m. + /// + internal static string effect_duration_short_minutes { + get { + return ResourceManager.GetString("effect.duration.short.minutes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}m {1}s. + /// + internal static string effect_duration_short_minutes_seconds { + get { + return ResourceManager.GetString("effect.duration.short.minutes_seconds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}s. + /// + internal static string effect_duration_short_seconds { + get { + return ResourceManager.GetString("effect.duration.short.seconds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ∞. + /// + internal static string effect_duration_short_unlimited { + get { + return ResourceManager.GetString("effect.duration.short.unlimited", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} {1}. + /// + internal static string effect_name_with_amplifier { + get { + return ResourceManager.GetString("effect.name.with_amplifier", resourceCulture); + } + } } } diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index 5eb2fa76..10b2ef3a 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -1237,6 +1237,18 @@ Change EnableEmoji=false in the settings if the display is confusing. follow <player name|stop> [-f] (Use -f to enable un-safe walking) + + list your currently active effects. + + + - {0} ({1}) + + + Active effects: + + + No active effects. + Display Health and Food saturation. @@ -2299,4 +2311,40 @@ see item details. {0} items - \ No newline at end of file + + You're now under {0} effect (Duration: {1}). + + + Effect {0} has expired + + + Unlimited + + + a + + + an + + + {0}h + + + {0}h {1}m + + + {0}m + + + {0}m {1}s + + + {0}s + + + ∞ + + + {0} {1} + + diff --git a/MinecraftClient/Scripting/ChatBot.cs b/MinecraftClient/Scripting/ChatBot.cs index 586a6fa8..f62e1377 100644 --- a/MinecraftClient/Scripting/ChatBot.cs +++ b/MinecraftClient/Scripting/ChatBot.cs @@ -333,6 +333,13 @@ namespace MinecraftClient.Scripting /// effect flags public virtual void OnEntityEffect(Entity entity, Effects effect, int amplifier, int duration, byte flags) { } + /// + /// Called when an entity has an effect removed (expired or cleared) + /// + /// Entity + /// Effect that was removed + public virtual void OnRemoveEntityEffect(Entity entity, Effects effect) { } + /// /// Called when a scoreboard objective updated /// diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index a3368c8e..e77514da 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -817,6 +817,9 @@ namespace MinecraftClient [TomlInlineComment("$Main.Advanced.show_inventory_layout$")] public bool ShowInventoryLayout = true; + [TomlInlineComment("$Main.Advanced.show_effect_names_in_tui$")] + public bool ShowEffectNamesInTUI = false; + [TomlInlineComment("$Main.Advanced.terrain_and_movements$")] public bool TerrainAndMovements = false; diff --git a/MinecraftClient/Tui/MainTuiView.cs b/MinecraftClient/Tui/MainTuiView.cs index 397f1475..6a4079a9 100644 --- a/MinecraftClient/Tui/MainTuiView.cs +++ b/MinecraftClient/Tui/MainTuiView.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -9,6 +10,7 @@ using Avalonia.Input; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Threading; +using MinecraftClient.Inventory; namespace MinecraftClient.Tui { @@ -866,6 +868,45 @@ namespace MinecraftClient.Tui Foreground = new SolidColorBrush(Color.FromRgb(220, 190, 100)), }); + // Add effects display + var effects = client.GetPlayerEffects().Values + .Where(effectData => !effectData.IsExpired) + .OrderBy(effectData => effectData.Effect) + .ToArray(); + if (effects.Length > 0) + { + bool showEffectNamesInTui = Settings.Config.Main.Advanced.ShowEffectNamesInTUI; + + _statusBar.Inlines.Add(new Avalonia.Controls.Documents.Run(" | ") + { + Foreground = Brushes.Gray, + }); + + bool first = true; + foreach (var effectData in effects) + { + if (!first) + { + _statusBar.Inlines.Add(new Avalonia.Controls.Documents.Run(", ") + { + Foreground = Brushes.Gray, + }); + } + first = false; + + var color = GetEffectIconAndColor(effectData.Effect).Color; + var displayText = showEffectNamesInTui + ? effectData.GetDisplayName() + : GetCompactEffectLabel(effectData); + displayText = $"{displayText} ({effectData.GetRemainingDurationText()})"; + + _statusBar.Inlines.Add(new Avalonia.Controls.Documents.Run(displayText) + { + Foreground = color, + }); + } + } + _statusBar.IsVisible = true; } @@ -884,6 +925,54 @@ namespace MinecraftClient.Tui return sb.ToString(); } + private static string GetCompactEffectLabel(EffectData effectData) + { + var icon = GetEffectIconAndColor(effectData.Effect).Icon; + return effectData.Amplifier > 0 + ? $"{icon}{effectData.Amplifier + 1}" + : icon; + } + + private static (string Icon, IBrush Color) GetEffectIconAndColor(Effects effect) + { + return effect switch + { + Effects.Speed => ("⚑", new SolidColorBrush(Color.FromRgb(135, 206, 235))), + Effects.Slowness => ("🐒", new SolidColorBrush(Color.FromRgb(139, 139, 139))), + Effects.Haste => ("⛏", new SolidColorBrush(Color.FromRgb(255, 215, 0))), + Effects.MiningFatigue => ("πŸ”¨", new SolidColorBrush(Color.FromRgb(64, 64, 64))), + Effects.Strength => ("βš”", new SolidColorBrush(Color.FromRgb(255, 99, 71))), + Effects.InstantHealth => ("❀", new SolidColorBrush(Color.FromRgb(255, 182, 193))), + Effects.InstantDamage => ("πŸ’€", new SolidColorBrush(Color.FromRgb(139, 0, 0))), + Effects.JumpBoost => ("🦘", new SolidColorBrush(Color.FromRgb(50, 205, 50))), + Effects.Nausea => ("πŸ’«", new SolidColorBrush(Color.FromRgb(85, 107, 47))), + Effects.Regeneration => ("✨", new SolidColorBrush(Color.FromRgb(255, 105, 180))), + Effects.Resistance => ("πŸ›‘", new SolidColorBrush(Color.FromRgb(112, 128, 144))), + Effects.FireResistance => ("πŸ”₯", new SolidColorBrush(Color.FromRgb(255, 140, 0))), + Effects.WaterBreathing => ("🐟", new SolidColorBrush(Color.FromRgb(0, 191, 255))), + Effects.Invisibility => ("πŸ‘»", new SolidColorBrush(Color.FromRgb(200, 200, 200))), + Effects.Blindness => ("πŸ•Ά", new SolidColorBrush(Color.FromRgb(50, 50, 50))), + Effects.NightVision => ("πŸ‘", new SolidColorBrush(Color.FromRgb(0, 255, 127))), + Effects.Hunger => ("πŸ”", new SolidColorBrush(Color.FromRgb(139, 69, 19))), + Effects.Weakness => ("πŸ’ͺ", new SolidColorBrush(Color.FromRgb(128, 128, 128))), + Effects.Poison => ("☠", new SolidColorBrush(Color.FromRgb(75, 0, 130))), + Effects.Wither => ("πŸ₯€", new SolidColorBrush(Color.FromRgb(0, 0, 0))), + Effects.HealthBoost => ("πŸ’–", new SolidColorBrush(Color.FromRgb(255, 20, 147))), + Effects.Absorption => ("πŸ’›", new SolidColorBrush(Color.FromRgb(255, 215, 0))), + Effects.Saturation => ("πŸ–", new SolidColorBrush(Color.FromRgb(255, 165, 0))), + Effects.Glowing => ("πŸ’‘", new SolidColorBrush(Color.FromRgb(255, 255, 150))), + Effects.Levitation => ("🎈", new SolidColorBrush(Color.FromRgb(147, 112, 219))), + Effects.Luck => ("πŸ€", new SolidColorBrush(Color.FromRgb(50, 205, 50))), + Effects.BadLuck => ("πŸˆβ€β¬›", new SolidColorBrush(Color.FromRgb(128, 0, 0))), + Effects.SlowFalling => ("πŸͺΆ", new SolidColorBrush(Color.FromRgb(255, 182, 193))), + Effects.ConduitPower => ("🐑", new SolidColorBrush(Color.FromRgb(0, 255, 255))), + Effects.DolphinsGrace => ("🐬", new SolidColorBrush(Color.FromRgb(135, 206, 235))), + Effects.BadOmen => ("🏴", new SolidColorBrush(Color.FromRgb(0, 100, 0))), + Effects.HerooftheVillage => ("πŸŽ‰", new SolidColorBrush(Color.FromRgb(255, 215, 0))), + _ => ("✦", new SolidColorBrush(Color.FromRgb(200, 200, 200))), + }; + } + #endregion #region Overlay diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 527776a0..76336fba 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -507,6 +507,16 @@ Coordinate = { x = 145, y = 64, z = 2045 } - **Default:** `true` +#### `ShowEffectNamesInTUI` + +- **Description:** + + This setting lets you show full effect names and levels in the TUI status bar instead of the compact icon-only effect display. + +- **Type:** `boolean` + +- **Default:** `false` + #### `TerrainAndMovements` - **Description:** diff --git a/docs/guide/usage.md b/docs/guide/usage.md index e57a23e1..6c8e284c 100644 --- a/docs/guide/usage.md +++ b/docs/guide/usage.md @@ -453,6 +453,21 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q +
+effects + +- **Description:** + + Lists the status effects currently applied to your player. + +- **Usage:** + + ``` + /effects + ``` + +
+
entity