diff --git a/MosswartMassacre/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj index a94f31d..614e483 100644 --- a/MosswartMassacre/MosswartMassacre.csproj +++ b/MosswartMassacre/MosswartMassacre.csproj @@ -367,7 +367,6 @@ - Shared\Spells\Spells.csv diff --git a/MosswartMassacre/ViewXML/vitalSharingOverlay.xml b/MosswartMassacre/ViewXML/vitalSharingOverlay.xml deleted file mode 100644 index 7913790..0000000 --- a/MosswartMassacre/ViewXML/vitalSharingOverlay.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/MosswartMassacre/Views/VitalSharingOverlayView.cs b/MosswartMassacre/Views/VitalSharingOverlayView.cs index b8a88e9..b90407e 100644 --- a/MosswartMassacre/Views/VitalSharingOverlayView.cs +++ b/MosswartMassacre/Views/VitalSharingOverlayView.cs @@ -1,53 +1,57 @@ using System; +using System.Collections.Generic; using System.Drawing; +using Decal.Adapter; using VirindiViewService; -using VirindiViewService.Controls; namespace MosswartMassacre.Views { /// - /// In-game overlay window showing live vitals for every character that has - /// opted-in to vital sharing via MosswartOverlord. Modeled after UB's - /// NetworkUI — one row per peer with HP/Stamina/Mana percentages coloured - /// to match the web sidebar palette. + /// In-game vital sharing overlay — a DxHud drawn directly onto the game + /// surface so the visual matches UtilityBelt's NetworkUI (DrawCharNameBackground). + /// One row per peer (including the local character), showing name, HP bar + /// across the full row, then Stamina (left half) and Mana (right half) at + /// the bottom half of the row. Colours match the MosswartOverlord player + /// sidebar palette (red / orange / blue). + /// + /// Toggle with /mm vitalsharing overlay, via the Settings tab button, or + /// VitalSharingOverlayView.ToggleWindow(). /// - internal class VitalSharingOverlayView : VVSBaseView + internal static class VitalSharingOverlayView { - private static VitalSharingOverlayView _instance; + // Layout constants — identical to UB NetworkUI so rows line up the same. + private const int HUD_X_OFFSET = 50; + private const int HUD_Y_OFFSET = 200; + private const int ROW_SIZE = 20; + private const int HUD_WIDTH = 260; + private const int CHAR_NAME_WIDTH = 260; + private const int HEALTH_BAR_HEIGHT = 11; - private HudList _peerList; - private System.Timers.Timer _refreshTimer; + // Colors matched to MosswartOverlord player sidebar (#ff4444 / #ffaa00 / #4488ff) + private const int HUD_OPACITY = 200; + private static readonly Color COLOR_HP_FILL = Color.FromArgb(HUD_OPACITY, 255, 68, 68); + private static readonly Color COLOR_HP_BG = Color.FromArgb(HUD_OPACITY, 60, 0, 0); + private static readonly Color COLOR_STA_FILL = Color.FromArgb(HUD_OPACITY, 255, 170, 0); + private static readonly Color COLOR_STA_BG = Color.FromArgb(HUD_OPACITY, 70, 30, 0); + private static readonly Color COLOR_MANA_FILL = Color.FromArgb(HUD_OPACITY, 68, 136, 255); + private static readonly Color COLOR_MANA_BG = Color.FromArgb(HUD_OPACITY, 0, 20, 55); + private static readonly Color COLOR_BORDER = Color.Black; + private static readonly Color COLOR_TEXT = Color.White; + private static readonly Color COLOR_TEXT_DARK = Color.FromArgb(255, 0, 0, 0); - // Colors matched to MosswartOverlord player sidebar - // health: #ff4444, stamina: #ffaa00, mana: #4488ff - private static readonly Color COLOR_HP = Color.FromArgb(255, 68, 68); - private static readonly Color COLOR_STA = Color.FromArgb(255, 170, 0); - private static readonly Color COLOR_MANA = Color.FromArgb(68, 136, 255); - private static readonly Color COLOR_NAME = Color.White; + private static DxHud _hud; + private static System.Windows.Forms.Timer _redrawTimer; + private static bool _visible; + private static int _lastRowCount; - private VitalSharingOverlayView() : base(null) - { - CreateFromXMLResource("MosswartMassacre.ViewXML.vitalSharingOverlay.xml"); - Init(); - } + public static bool IsVisible => _visible; public static void ToggleWindow() { try { - if (_instance == null) - { - _instance = new VitalSharingOverlayView(); - _instance.Show(); - } - else if (_instance.IsVisible) - { - _instance.Hide(); - } - else - { - _instance.Show(); - } + if (_visible) Hide(); + else Show(); } catch (Exception ex) { @@ -55,117 +59,226 @@ namespace MosswartMassacre.Views } } - public static void ForceClose() + public static void Show() { try { - _instance?.Dispose(); - _instance = null; - } - catch { } - } - - private void Init() - { - try - { - _peerList = GetControl("lstPeers"); - if (_peerList != null) + if (_visible && _hud != null) { - // columns: name, hp%, sta%, mana% - _peerList.AddColumn(typeof(HudStaticText), 160, null); - _peerList.AddColumn(typeof(HudStaticText), 55, null); - _peerList.AddColumn(typeof(HudStaticText), 55, null); - _peerList.AddColumn(typeof(HudStaticText), 55, null); + _hud.Enabled = true; + return; } - _refreshTimer = new System.Timers.Timer(500); - _refreshTimer.AutoReset = true; - _refreshTimer.Elapsed += (s, e) => + var rows = Math.Max(1, CountRows()); + var size = new Size(HUD_WIDTH, rows * ROW_SIZE + 5); + _hud = new DxHud(new Point(HUD_X_OFFSET, HUD_Y_OFFSET), size, 0) { - try { RefreshPeers(); } - catch (Exception ex) { PluginCore.WriteToChat($"[VitalShare] Overlay refresh error: {ex.Message}"); } + Enabled = true, + Alpha = 255, }; - _refreshTimer.Start(); + _visible = true; + _lastRowCount = rows; - RefreshPeers(); + if (_redrawTimer == null) + { + _redrawTimer = new System.Windows.Forms.Timer(); + _redrawTimer.Interval = 250; // 4 fps is plenty for vital bars + _redrawTimer.Tick += (s, e) => Render(); + } + _redrawTimer.Start(); + + Render(); } catch (Exception ex) { - PluginCore.WriteToChat($"[VitalShare] Overlay init error: {ex.Message}"); + PluginCore.WriteToChat($"[VitalShare] Overlay show error: {ex.Message}"); } } - private void RefreshPeers() + public static void Hide() { - if (_peerList == null) return; - - var snaps = VitalSharingTracker.GetPeerSnapshots(); - - _peerList.ClearRows(); - - if (snaps == null || snaps.Length == 0) + try { - var row = _peerList.AddRow(); - ((HudStaticText)row[0]).Text = "(no peers)"; - ((HudStaticText)row[0]).TextColor = Color.Gray; - ((HudStaticText)row[1]).Text = ""; - ((HudStaticText)row[2]).Text = ""; - ((HudStaticText)row[3]).Text = ""; - return; + _redrawTimer?.Stop(); + if (_hud != null) + { + _hud.Enabled = false; + _hud.Dispose(); + _hud = null; + } + _visible = false; } - - Array.Sort(snaps, (a, b) => - string.Compare(a.CharacterName, b.CharacterName, StringComparison.OrdinalIgnoreCase)); - - foreach (var p in snaps) + catch (Exception ex) { - var row = _peerList.AddRow(); - - var name = (HudStaticText)row[0]; - name.Text = p.CharacterName; - name.TextColor = COLOR_NAME; - - var hp = (HudStaticText)row[1]; - hp.Text = FormatPct(p.CurrentHealth, p.MaxHealth); - hp.TextColor = COLOR_HP; - - var sta = (HudStaticText)row[2]; - sta.Text = FormatPct(p.CurrentStamina, p.MaxStamina); - sta.TextColor = COLOR_STA; - - var mana = (HudStaticText)row[3]; - mana.Text = FormatPct(p.CurrentMana, p.MaxMana); - mana.TextColor = COLOR_MANA; + PluginCore.WriteToChat($"[VitalShare] Overlay hide error: {ex.Message}"); } } - private static string FormatPct(int cur, int max) + private static int CountRows() { - if (max <= 0) return "--"; - int pct = (int)Math.Round(100.0 * cur / max); - if (pct < 0) pct = 0; - if (pct > 100) pct = 100; - return pct + "%"; + int n = VitalSharingTracker.GetPeerSnapshots()?.Length ?? 0; + // self is always drawn as the first row + return n + 1; } - protected override void Dispose(bool disposing) + private static List BuildRows() { - if (disposing) + var rows = new List(); + var self = VitalSharingTracker.GetSelfSnapshot(); + if (self != null) rows.Add(self); + + var peers = VitalSharingTracker.GetPeerSnapshots(); + if (peers != null) { + Array.Sort(peers, (a, b) => + string.Compare(a.CharacterName, b.CharacterName, StringComparison.OrdinalIgnoreCase)); + foreach (var p in peers) + { + if (self != null && string.Equals(p.CharacterName, self.CharacterName, StringComparison.OrdinalIgnoreCase)) + continue; + rows.Add(p); + } + } + return rows; + } + + private static void Render() + { + try + { + if (_hud == null || !_visible) return; + + var rows = BuildRows(); + if (rows.Count == 0) return; + + // Resize the hud if the row count changed + if (rows.Count != _lastRowCount) + { + _lastRowCount = rows.Count; + // DxHud has no resize; re-create if size changed + var loc = _hud.Location; + _hud.Dispose(); + _hud = new DxHud(loc, new Size(HUD_WIDTH, rows.Count * ROW_SIZE + 5), 0) + { + Enabled = true, + Alpha = 255, + }; + } + + var tex = _hud.Texture; + tex.BeginRender(); try { - if (_refreshTimer != null) + // Clear background + tex.Fill(new Rectangle(0, 0, HUD_WIDTH, rows.Count * ROW_SIZE + 5), Color.Transparent); + + int offset = 0; + foreach (var p in rows) { - _refreshTimer.Stop(); - _refreshTimer.Dispose(); - _refreshTimer = null; + DrawRowBackground(tex, offset, p); + offset += ROW_SIZE; } + + // Text passes + tex.BeginText("Arial", 7, 100, false, 1, 255); + try + { + offset = 0; + foreach (var p in rows) + { + DrawRowText(tex, offset, p); + offset += ROW_SIZE; + } + } + finally { tex.EndText(); } + + tex.BeginText("Arial", 5, 100, false); + try + { + offset = 0; + foreach (var p in rows) + { + DrawRowTextSmall(tex, offset, p); + offset += ROW_SIZE; + } + } + finally { tex.EndText(); } + } + finally + { + tex.EndRender(); } - catch { } } - base.Dispose(disposing); - if (_instance == this) _instance = null; + catch (Exception ex) + { + PluginCore.WriteToChat($"[VitalShare] Overlay render error: {ex.Message}"); + } + } + + private static double Ratio(int cur, int max) + { + if (max <= 0) return 0; + var r = (double)cur / max; + if (r < 0) return 0; + if (r > 1) return 1; + return r; + } + + private static void DrawRowBackground(DxTexture tex, int offset, VitalSharingTracker.PeerSnapshot p) + { + var area = new Rectangle(1, offset + 1, HUD_WIDTH - 2, ROW_SIZE - 2); + double hp = Ratio(p.CurrentHealth, p.MaxHealth); + double sta = Ratio(p.CurrentStamina, p.MaxStamina); + double mana = Ratio(p.CurrentMana, p.MaxMana); + + // Health bar (full row width, top half) + tex.Fill(new Rectangle(area.Left, area.Top, CHAR_NAME_WIDTH, HEALTH_BAR_HEIGHT), COLOR_HP_BG); + tex.Fill(new Rectangle(area.Left, area.Top, (int)(CHAR_NAME_WIDTH * hp), HEALTH_BAR_HEIGHT), COLOR_HP_FILL); + + // Stamina bar (left half, bottom) + int halfW = CHAR_NAME_WIDTH / 2; + int bottomH = area.Height - HEALTH_BAR_HEIGHT; + tex.Fill(new Rectangle(area.Left, area.Top + HEALTH_BAR_HEIGHT, halfW, bottomH), COLOR_STA_BG); + tex.Fill(new Rectangle(area.Left, area.Top + HEALTH_BAR_HEIGHT, (int)(halfW * sta), bottomH), COLOR_STA_FILL); + + // Mana bar (right half, bottom) + tex.Fill(new Rectangle(area.Left + halfW, area.Top + HEALTH_BAR_HEIGHT, halfW, bottomH), COLOR_MANA_BG); + tex.Fill(new Rectangle(area.Left + halfW, area.Top + HEALTH_BAR_HEIGHT, (int)(halfW * mana), bottomH), COLOR_MANA_FILL); + + // Outer border + var tl = new PointF(area.Left, area.Top); + var tr = new PointF(area.Left + CHAR_NAME_WIDTH, area.Top); + var br = new PointF(area.Left + CHAR_NAME_WIDTH, area.Top + area.Height); + var bl = new PointF(area.Left, area.Top + area.Height); + tex.DrawLine(tl, tr, COLOR_BORDER, 1); + tex.DrawLine(tr, br, COLOR_BORDER, 1); + tex.DrawLine(br, bl, COLOR_BORDER, 1); + tex.DrawLine(bl, tl, COLOR_BORDER, 1); + } + + private static void DrawRowText(DxTexture tex, int offset, VitalSharingTracker.PeerSnapshot p) + { + var area = new Rectangle(0, offset, HUD_WIDTH, ROW_SIZE); + // name (top-left) + tex.WriteText(p.CharacterName, COLOR_TEXT, WriteTextFormats.SingleLine, + new Rectangle(area.X + 4, area.Y + 1, CHAR_NAME_WIDTH, ROW_SIZE)); + // hp % (top-right) + int hpPct = p.MaxHealth > 0 ? (int)Math.Round(100.0 * p.CurrentHealth / p.MaxHealth) : 0; + tex.WriteText($"{hpPct}%", COLOR_TEXT, WriteTextFormats.Right, + new Rectangle(area.X + 4, area.Y + 1, CHAR_NAME_WIDTH - 8, ROW_SIZE)); + } + + private static void DrawRowTextSmall(DxTexture tex, int offset, VitalSharingTracker.PeerSnapshot p) + { + var area = new Rectangle(0, offset, HUD_WIDTH, ROW_SIZE); + int staPct = p.MaxStamina > 0 ? (int)Math.Round(100.0 * p.CurrentStamina / p.MaxStamina) : 0; + int manaPct = p.MaxMana > 0 ? (int)Math.Round(100.0 * p.CurrentMana / p.MaxMana) : 0; + // stamina % bottom-left + tex.WriteText($"{staPct}%", COLOR_TEXT_DARK, WriteTextFormats.SingleLine, + new Rectangle(area.X + 4, area.Y + HEALTH_BAR_HEIGHT, CHAR_NAME_WIDTH, ROW_SIZE)); + // mana % bottom-right-half + tex.WriteText($"{manaPct}%", COLOR_TEXT_DARK, WriteTextFormats.SingleLine, + new Rectangle(area.X + 4 + (CHAR_NAME_WIDTH / 2), area.Y + HEALTH_BAR_HEIGHT, CHAR_NAME_WIDTH, ROW_SIZE)); } } } diff --git a/MosswartMassacre/VitalSharingTracker.cs b/MosswartMassacre/VitalSharingTracker.cs index 2dd0ed0..2a8a3fc 100644 --- a/MosswartMassacre/VitalSharingTracker.cs +++ b/MosswartMassacre/VitalSharingTracker.cs @@ -85,6 +85,30 @@ namespace MosswartMassacre } } + /// + /// Snapshot for the local character (first row in the overlay so you + /// always see yourself even before any peer traffic arrives). + /// + public static PeerSnapshot GetSelfSnapshot() + { + try + { + var actions = CoreManager.Current.Actions; + return new PeerSnapshot + { + CharacterName = CoreManager.Current.CharacterFilter?.Name ?? "", + CurrentHealth = actions.Vital[VitalType.CurrentHealth], + MaxHealth = actions.Vital[VitalType.MaximumHealth], + CurrentStamina = actions.Vital[VitalType.CurrentStamina], + MaxStamina = actions.Vital[VitalType.MaximumStamina], + CurrentMana = actions.Vital[VitalType.CurrentMana], + MaxMana = actions.Vital[VitalType.MaximumMana], + LastUpdate = DateTime.UtcNow, + }; + } + catch { return null; } + } + public VitalSharingTracker(IPluginLogger logger) { _logger = logger; diff --git a/MosswartMassacre/bin/Release/MosswartMassacre.dll b/MosswartMassacre/bin/Release/MosswartMassacre.dll index fe32eff..5e1fb33 100644 Binary files a/MosswartMassacre/bin/Release/MosswartMassacre.dll and b/MosswartMassacre/bin/Release/MosswartMassacre.dll differ