diff --git a/MosswartMassacre/PluginSettings.cs b/MosswartMassacre/PluginSettings.cs
index 90e14b1..ec5ac31 100644
--- a/MosswartMassacre/PluginSettings.cs
+++ b/MosswartMassacre/PluginSettings.cs
@@ -25,6 +25,8 @@ namespace MosswartMassacre
private bool _verboseLogging = false;
private bool _autoUpdateEnabled = true;
private bool _vitalSharingEnabled = false;
+ private int _vitalSharingOverlayX = 50;
+ private int _vitalSharingOverlayY = 200;
private ChestLooterSettings _chestLooterSettings = new ChestLooterSettings();
public static PluginSettings Instance => _instance
@@ -204,6 +206,18 @@ namespace MosswartMassacre
set { _vitalSharingEnabled = value; Save(); }
}
+ public int VitalSharingOverlayX
+ {
+ get => _vitalSharingOverlayX;
+ set { _vitalSharingOverlayX = value; Save(); }
+ }
+
+ public int VitalSharingOverlayY
+ {
+ get => _vitalSharingOverlayY;
+ set { _vitalSharingOverlayY = value; Save(); }
+ }
+
public ChestLooterSettings ChestLooterSettings
{
get
diff --git a/MosswartMassacre/Views/VitalSharingOverlayView.cs b/MosswartMassacre/Views/VitalSharingOverlayView.cs
index b90407e..7db3439 100644
--- a/MosswartMassacre/Views/VitalSharingOverlayView.cs
+++ b/MosswartMassacre/Views/VitalSharingOverlayView.cs
@@ -8,24 +8,25 @@ namespace MosswartMassacre.Views
{
///
/// 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).
+ /// surface so the visual matches UtilityBelt's NetworkUI
+ /// (DrawCharNameBackground / DrawRangeIndicator). One row per peer
+ /// (plus the local character), showing character name, direction arrow
+ /// pointing at the peer, distance in meters, HP bar across the full
+ /// name area, and Stamina + Mana bars side-by-side on the bottom half.
+ /// Colours match the MosswartOverlord player sidebar palette.
///
- /// Toggle with /mm vitalsharing overlay, via the Settings tab button, or
- /// VitalSharingOverlayView.ToggleWindow().
+ /// Hold Ctrl + left-drag to reposition the overlay (same behaviour as
+ /// UB's NetworkUI). The position is persisted to plugin settings.
///
internal static class VitalSharingOverlayView
{
// 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 CHAR_NAME_WIDTH = 180;
private const int HEALTH_BAR_HEIGHT = 11;
+ private const int RANGE_WIDTH = 34;
+ private const int PADDING = 4;
+ private const int HUD_WIDTH = CHAR_NAME_WIDTH + PADDING + ROW_SIZE + RANGE_WIDTH + PADDING;
// Colors matched to MosswartOverlord player sidebar (#ff4444 / #ffaa00 / #4488ff)
private const int HUD_OPACITY = 200;
@@ -38,12 +39,20 @@ namespace MosswartMassacre.Views
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);
+ private static readonly Color COLOR_DRAG_BORDER = Color.Yellow;
private static DxHud _hud;
private static System.Windows.Forms.Timer _redrawTimer;
private static bool _visible;
private static int _lastRowCount;
+ // Drag state
+ private static bool _isHoldingControl;
+ private static bool _isDragging;
+ private static Point _dragStart;
+ private static Point _dragBaseHudPos;
+ private static bool _windowMessageHooked;
+
public static bool IsVisible => _visible;
public static void ToggleWindow()
@@ -71,7 +80,10 @@ namespace MosswartMassacre.Views
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)
+ var loc = new Point(
+ PluginSettings.Instance.VitalSharingOverlayX,
+ PluginSettings.Instance.VitalSharingOverlayY);
+ _hud = new DxHud(loc, size, 0)
{
Enabled = true,
Alpha = 255,
@@ -87,6 +99,7 @@ namespace MosswartMassacre.Views
}
_redrawTimer.Start();
+ HookWindowMessages();
Render();
}
catch (Exception ex)
@@ -100,6 +113,7 @@ namespace MosswartMassacre.Views
try
{
_redrawTimer?.Stop();
+ UnhookWindowMessages();
if (_hud != null)
{
_hud.Enabled = false;
@@ -107,6 +121,7 @@ namespace MosswartMassacre.Views
_hud = null;
}
_visible = false;
+ _isDragging = false;
}
catch (Exception ex)
{
@@ -114,11 +129,116 @@ namespace MosswartMassacre.Views
}
}
+ // ---------- Window message / drag handling ----------
+
+ private static void HookWindowMessages()
+ {
+ if (_windowMessageHooked) return;
+ try
+ {
+ CoreManager.Current.WindowMessage += OnWindowMessage;
+ _windowMessageHooked = true;
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[VitalShare] Hook error: {ex.Message}");
+ }
+ }
+
+ private static void UnhookWindowMessages()
+ {
+ if (!_windowMessageHooked) return;
+ try
+ {
+ CoreManager.Current.WindowMessage -= OnWindowMessage;
+ }
+ catch { }
+ _windowMessageHooked = false;
+ _isHoldingControl = false;
+ _isDragging = false;
+ }
+
+ // Window message constants (from winuser.h)
+ private const int WM_KEYDOWN = 0x0100;
+ private const int WM_KEYUP = 0x0101;
+ private const int WM_MOUSEMOVE = 0x0200;
+ private const int WM_LBUTTONDOWN = 0x0201;
+ private const int WM_LBUTTONUP = 0x0202;
+ private const int VK_CONTROL = 0x11;
+
+ private static Rectangle HudRect()
+ {
+ if (_hud == null) return Rectangle.Empty;
+ return new Rectangle(_hud.Location.X, _hud.Location.Y, HUD_WIDTH, Math.Max(1, _lastRowCount) * ROW_SIZE + 5);
+ }
+
+ private static void OnWindowMessage(object sender, WindowMessageEventArgs e)
+ {
+ try
+ {
+ if (!_visible || _hud == null) return;
+
+ switch (e.Msg)
+ {
+ case WM_KEYDOWN:
+ if ((int)e.WParam == VK_CONTROL) _isHoldingControl = true;
+ return;
+ case WM_KEYUP:
+ if ((int)e.WParam == VK_CONTROL)
+ {
+ _isHoldingControl = false;
+ _isDragging = false;
+ }
+ return;
+ }
+
+ if (!_isHoldingControl) return;
+
+ var mouse = new Point(e.LParam);
+
+ switch (e.Msg)
+ {
+ case WM_LBUTTONDOWN:
+ if (HudRect().Contains(mouse))
+ {
+ _isDragging = true;
+ _dragStart = mouse;
+ _dragBaseHudPos = _hud.Location;
+ e.Eat = true;
+ }
+ break;
+ case WM_MOUSEMOVE:
+ if (_isDragging)
+ {
+ int nx = _dragBaseHudPos.X + (mouse.X - _dragStart.X);
+ int ny = _dragBaseHudPos.Y + (mouse.Y - _dragStart.Y);
+ _hud.Location = new Point(nx, ny);
+ e.Eat = true;
+ }
+ break;
+ case WM_LBUTTONUP:
+ if (_isDragging)
+ {
+ _isDragging = false;
+ PluginSettings.Instance.VitalSharingOverlayX = _hud.Location.X;
+ PluginSettings.Instance.VitalSharingOverlayY = _hud.Location.Y;
+ e.Eat = true;
+ }
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[VitalShare] WindowMessage error: {ex.Message}");
+ }
+ }
+
+ // ---------- Row enumeration ----------
+
private static int CountRows()
{
int n = VitalSharingTracker.GetPeerSnapshots()?.Length ?? 0;
- // self is always drawn as the first row
- return n + 1;
+ return n + 1; // self row
}
private static List BuildRows()
@@ -142,6 +262,8 @@ namespace MosswartMassacre.Views
return rows;
}
+ // ---------- Rendering ----------
+
private static void Render()
{
try
@@ -151,11 +273,9 @@ namespace MosswartMassacre.Views
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)
@@ -169,7 +289,6 @@ namespace MosswartMassacre.Views
tex.BeginRender();
try
{
- // Clear background
tex.Fill(new Rectangle(0, 0, HUD_WIDTH, rows.Count * ROW_SIZE + 5), Color.Transparent);
int offset = 0;
@@ -179,14 +298,33 @@ namespace MosswartMassacre.Views
offset += ROW_SIZE;
}
+ // Direction arrows (everyone except the self row)
+ offset = 0;
+ for (int i = 0; i < rows.Count; i++)
+ {
+ if (i > 0) DrawDirectionArrow(tex, offset, rows[i]);
+ offset += ROW_SIZE;
+ }
+
+ // Drag-highlight border when Ctrl is held
+ if (_isHoldingControl)
+ {
+ var w = HUD_WIDTH;
+ var h = rows.Count * ROW_SIZE + 5;
+ tex.DrawLine(new PointF(0, 0), new PointF(w - 1, 0), COLOR_DRAG_BORDER, 1);
+ tex.DrawLine(new PointF(w - 1, 0), new PointF(w - 1, h - 1), COLOR_DRAG_BORDER, 1);
+ tex.DrawLine(new PointF(w - 1, h - 1), new PointF(0, h - 1), COLOR_DRAG_BORDER, 1);
+ tex.DrawLine(new PointF(0, h - 1), new PointF(0, 0), COLOR_DRAG_BORDER, 1);
+ }
+
// Text passes
tex.BeginText("Arial", 7, 100, false, 1, 255);
try
{
offset = 0;
- foreach (var p in rows)
+ for (int i = 0; i < rows.Count; i++)
{
- DrawRowText(tex, offset, p);
+ DrawRowText(tex, offset, rows[i], isSelf: i == 0);
offset += ROW_SIZE;
}
}
@@ -226,7 +364,7 @@ namespace MosswartMassacre.Views
private static void DrawRowBackground(DxTexture tex, int offset, VitalSharingTracker.PeerSnapshot p)
{
- var area = new Rectangle(1, offset + 1, HUD_WIDTH - 2, ROW_SIZE - 2);
+ var area = new Rectangle(1, offset + 1, CHAR_NAME_WIDTH, ROW_SIZE - 2);
double hp = Ratio(p.CurrentHealth, p.MaxHealth);
double sta = Ratio(p.CurrentStamina, p.MaxStamina);
double mana = Ratio(p.CurrentMana, p.MaxMana);
@@ -245,7 +383,7 @@ namespace MosswartMassacre.Views
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
+ // Outer border around the vital area
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);
@@ -256,16 +394,101 @@ namespace MosswartMassacre.Views
tex.DrawLine(bl, tl, COLOR_BORDER, 1);
}
- private static void DrawRowText(DxTexture tex, int offset, VitalSharingTracker.PeerSnapshot p)
+ // Draw a small triangle pointing toward the peer's position relative
+ // to the local character, tinted red by distance.
+ private static void DrawDirectionArrow(DxTexture tex, int offset, VitalSharingTracker.PeerSnapshot p)
+ {
+ if (!p.HasPosition) return;
+
+ double selfEW, selfNS;
+ double myHeading;
+ try
+ {
+ var me = Coordinates.Me;
+ selfEW = me.EW;
+ selfNS = me.NS;
+ myHeading = CoreManager.Current.Actions.Heading;
+ }
+ catch { return; }
+
+ double dx = p.EW - selfEW;
+ double dy = p.NS - selfNS;
+ double distance = Math.Sqrt(dx * dx + dy * dy) * 240.0; // 1 coord = 240m
+ if (distance < 0.1) return;
+
+ // Angle from north (positive Y), clockwise. atan2(dx, dy) gives
+ // 0 when target is due north, rotating east positively.
+ double worldAngleDeg = Math.Atan2(dx, dy) * 180.0 / Math.PI;
+ double relative = worldAngleDeg - myHeading;
+ double rad = relative * Math.PI / 180.0;
+
+ // Arrow anchored in the small square just right of the vital area
+ float cx = CHAR_NAME_WIDTH + PADDING + ROW_SIZE / 2f;
+ float cy = offset + ROW_SIZE / 2f;
+ float size = ROW_SIZE * 0.35f;
+
+ // Tip in direction of `rad`, base corners 140° behind
+ double tipX = cx + Math.Sin(rad) * size;
+ double tipY = cy - Math.Cos(rad) * size;
+ double leftRad = rad + (140.0 * Math.PI / 180.0);
+ double rightRad = rad - (140.0 * Math.PI / 180.0);
+ double leftX = cx + Math.Sin(leftRad) * size * 0.75;
+ double leftY = cy - Math.Cos(leftRad) * size * 0.75;
+ double rightX = cx + Math.Sin(rightRad) * size * 0.75;
+ double rightY = cy - Math.Cos(rightRad) * size * 0.75;
+
+ int fade = (int)Math.Max(0, Math.Min(255, 255 - distance * 2));
+ var tint = Color.FromArgb(255, 255, fade, fade);
+
+ tex.DrawLine(new PointF((float)tipX, (float)tipY), new PointF((float)leftX, (float)leftY), tint, 2);
+ tex.DrawLine(new PointF((float)leftX, (float)leftY), new PointF((float)rightX, (float)rightY), tint, 2);
+ tex.DrawLine(new PointF((float)rightX, (float)rightY), new PointF((float)tipX, (float)tipY), tint, 2);
+ }
+
+ private static string FormatDistance(double meters)
+ {
+ if (meters < 1000) return meters.ToString("N0");
+ if (meters < 10000) return (meters / 1000).ToString("N1") + "k";
+ return (meters / 1000).ToString("N0") + "k";
+ }
+
+ private static void DrawRowText(DxTexture tex, int offset, VitalSharingTracker.PeerSnapshot p, bool isSelf)
{
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)
+ // hp % (top-right of name area)
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));
+ new Rectangle(area.X + 2, area.Y + 1, CHAR_NAME_WIDTH - 4, ROW_SIZE));
+
+ // range text to the right of the arrow square
+ if (!isSelf)
+ {
+ if (p.HasPosition)
+ {
+ double selfEW, selfNS;
+ try
+ {
+ var me = Coordinates.Me;
+ selfEW = me.EW; selfNS = me.NS;
+ }
+ catch { return; }
+ double dx = p.EW - selfEW;
+ double dy = p.NS - selfNS;
+ double distance = Math.Sqrt(dx * dx + dy * dy) * 240.0;
+ int fade = (int)Math.Max(0, Math.Min(255, 255 - distance * 2));
+ var tint = Color.FromArgb(255, 255, fade, fade);
+ tex.WriteText(FormatDistance(distance), tint, WriteTextFormats.SingleLine,
+ new Rectangle(CHAR_NAME_WIDTH + PADDING + ROW_SIZE, offset + 5, RANGE_WIDTH, 12));
+ }
+ else
+ {
+ tex.WriteText("??", Color.Gray, WriteTextFormats.SingleLine,
+ new Rectangle(CHAR_NAME_WIDTH + PADDING + ROW_SIZE, offset + 5, RANGE_WIDTH, 12));
+ }
+ }
}
private static void DrawRowTextSmall(DxTexture tex, int offset, VitalSharingTracker.PeerSnapshot p)
diff --git a/MosswartMassacre/VitalSharingTracker.cs b/MosswartMassacre/VitalSharingTracker.cs
index 2a8a3fc..6a93b6b 100644
--- a/MosswartMassacre/VitalSharingTracker.cs
+++ b/MosswartMassacre/VitalSharingTracker.cs
@@ -68,6 +68,10 @@ namespace MosswartMassacre
public int CurrentHealth, MaxHealth;
public int CurrentStamina, MaxStamina;
public int CurrentMana, MaxMana;
+ // Position of the peer in world coordinates (from share_position_update)
+ public double EW, NS, Z;
+ public double Heading;
+ public bool HasPosition;
public DateTime LastUpdate;
}
@@ -208,6 +212,8 @@ namespace MosswartMassacre
HandleShareCastSuccess(root);
break;
case "share_position_update":
+ HandleSharePositionUpdate(root);
+ break;
case "share_item_update":
// Dashboard informational only. No VTank feeding.
break;
@@ -273,6 +279,32 @@ namespace MosswartMassacre
});
}
+ private void HandleSharePositionUpdate(JObject root)
+ {
+ string fromChar = (string)root["character_name"] ?? "";
+ if (string.IsNullOrEmpty(fromChar)) return;
+ if (fromChar.Equals(SafeCharacterName(), StringComparison.OrdinalIgnoreCase))
+ return;
+
+ double ew = (double?)root["ew"] ?? 0;
+ double ns = (double?)root["ns"] ?? 0;
+ double z = (double?)root["z"] ?? 0;
+ double heading = (double?)root["heading"] ?? 0;
+
+ lock (_peersLock)
+ {
+ if (!_peers.TryGetValue(fromChar, out var snap))
+ {
+ snap = new PeerSnapshot { CharacterName = fromChar };
+ _peers[fromChar] = snap;
+ }
+ snap.EW = ew; snap.NS = ns; snap.Z = z;
+ snap.Heading = heading;
+ snap.HasPosition = true;
+ snap.LastUpdate = DateTime.UtcNow;
+ }
+ }
+
private void HandleShareCastAttempt(JObject root)
{
string fromChar = (string)root["character_name"] ?? "";
diff --git a/MosswartMassacre/bin/Release/MosswartMassacre.dll b/MosswartMassacre/bin/Release/MosswartMassacre.dll
index 5e1fb33..70d1e1b 100644
Binary files a/MosswartMassacre/bin/Release/MosswartMassacre.dll and b/MosswartMassacre/bin/Release/MosswartMassacre.dll differ