Combo highlight config, tooltips, nameplates, hotbars fixes
- Combo highlight: configurable color, glow, line style (solid/dashed/dotted), thickness - Tooltips: font selection, scaling slider, improved wrap/cramping handling - Nameplates: custom quest icons with config, position smoothing fix for jitter - Hotbars: hide keybinds on empty slots, combo highlight within icon bounds - HudHelper: restore default nameplates on plugin disable Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -224,6 +224,26 @@ namespace HSUI.Interface.Nameplates
|
||||
NameplateAnchor? barAnchor = GetBarAnchor(data);
|
||||
drawActions.AddRange(GetMainLabelDrawActions(data, barAnchor));
|
||||
|
||||
// Quest/state icon (e.g. ! ? above NPCs) - drawn when we have icon config and game provided icon ID
|
||||
if (_config is NameplateWithNPCBarConfig npcConfig &&
|
||||
npcConfig.IconConfig.Enabled &&
|
||||
data.NamePlateIconId > 0)
|
||||
{
|
||||
float alpha = _config.RangeConfig.AlphaForDistance(data.Distance, 1f);
|
||||
Vector2 anchorPos = barAnchor?.Position ?? (_config.Position + data.ScreenPosition);
|
||||
Vector2 anchorSize = barAnchor?.Size ?? Vector2.Zero;
|
||||
var pos = Utils.GetAnchoredPosition(anchorPos, -anchorSize, npcConfig.IconConfig.FrameAnchor);
|
||||
Vector2 iconPos = Utils.GetAnchoredPosition(pos + npcConfig.IconConfig.Position, npcConfig.IconConfig.Size, npcConfig.IconConfig.Anchor);
|
||||
|
||||
drawActions.Add((npcConfig.IconConfig.StrataLevel, () =>
|
||||
{
|
||||
DrawHelper.DrawInWindow(_config.ID + "_npcIcon", iconPos, npcConfig.IconConfig.Size, false, (drawList) =>
|
||||
{
|
||||
DrawHelper.DrawIcon((uint)data.NamePlateIconId, iconPos, npcConfig.IconConfig.Size, false, alpha, drawList);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
return drawActions;
|
||||
}
|
||||
|
||||
|
||||
@@ -413,6 +413,16 @@ namespace HSUI.Interface.GeneralElements
|
||||
[NestedConfig("Health Bar", 40)]
|
||||
public NameplateBarConfig BarConfig = null!;
|
||||
|
||||
/// <summary>Quest/state icon (e.g. ! ? above NPCs). Use Position and Size to resize and reposition around the nameplate.</summary>
|
||||
[NestedConfig("Icon (Quest/State)", 42)]
|
||||
public NameplateIconConfig IconConfig = new NameplateIconConfig(
|
||||
new Vector2(0, -28),
|
||||
new Vector2(32, 32),
|
||||
DrawAnchor.Bottom,
|
||||
DrawAnchor.Top
|
||||
)
|
||||
{ Strata = StrataLevel.LOWEST };
|
||||
|
||||
public NameplateBarConfig GetBarConfig() => BarConfig;
|
||||
|
||||
public NameplateWithNPCBarConfig(
|
||||
|
||||
@@ -75,10 +75,14 @@ namespace HSUI.Interface.Nameplates
|
||||
public IReadOnlyCollection<NameplateData> Data => _data.AsReadOnly();
|
||||
|
||||
private NameplatesCache _cache = new NameplatesCache(50);
|
||||
private Dictionary<uint, Vector2> _smoothedPositions = new(50);
|
||||
private const float PositionSmoothFactor = 0.3f; // Lerp factor: lower = smoother, higher = more responsive
|
||||
private const float PlayerPositionSmoothFactor = 0.15f; // Stronger smoothing for player (camera-follow causes more jitter)
|
||||
|
||||
private void ClientStateOnTerritoryChangedEvent(ushort territoryId)
|
||||
{
|
||||
_cache.Clear();
|
||||
_smoothedPositions.Clear();
|
||||
}
|
||||
|
||||
public unsafe void Update()
|
||||
@@ -107,6 +111,7 @@ namespace HSUI.Interface.Nameplates
|
||||
|
||||
_data = new List<NameplateData>();
|
||||
int activeCount = ui3DModule->NamePlateObjectInfoCount;
|
||||
var nextSmoothed = new Dictionary<uint, Vector2>(Math.Min(activeCount + 4, 54));
|
||||
|
||||
for (int i = 0; i < activeCount; i++)
|
||||
{
|
||||
@@ -127,18 +132,48 @@ namespace HSUI.Interface.Nameplates
|
||||
foundTarget = true;
|
||||
}
|
||||
|
||||
// ui nameplate
|
||||
// ui nameplate (may be stale when addon is hidden)
|
||||
NamePlateObject nameplateObject = addon->NamePlateObjectArray[objectInfo->NamePlateIndex];
|
||||
|
||||
// position
|
||||
Vector2 screenPos = new Vector2(
|
||||
nameplateObject.RootComponentNode->AtkResNode.X + nameplateObject.RootComponentNode->AtkResNode.Width / 2f,
|
||||
nameplateObject.RootComponentNode->AtkResNode.Y + nameplateObject.RootComponentNode->AtkResNode.Height
|
||||
);
|
||||
screenPos = ClampScreenPosition(screenPos);
|
||||
|
||||
Vector3 worldPos = new Vector3(obj->Position.X, obj->Position.Y + obj->Height * 2.2f, obj->Position.Z);
|
||||
|
||||
// Screen position: use addon when available (game's logic, stable). WorldToScreen when addon hidden/stale.
|
||||
var hudConfig = ConfigurationManager.Instance?.GetConfigObject<HUDOptionsConfig>();
|
||||
bool hidingAddon = _config.Enabled && (hudConfig?.HideDefaultHudWhenReplaced ?? true);
|
||||
bool isPlayer = Plugin.ObjectTable.LocalPlayer != null && new IntPtr(obj) == Plugin.ObjectTable.LocalPlayer.Address;
|
||||
|
||||
Vector2 screenPos;
|
||||
bool addonNodeValid = nameplateObject.RootComponentNode != null;
|
||||
// Use addon position when visible, or when hiding (game may still update nodes). Fall back to WorldToScreen if stale.
|
||||
bool useAddonPos = addonNodeValid && (addon->IsVisible || hidingAddon);
|
||||
if (useAddonPos)
|
||||
{
|
||||
float nx = nameplateObject.RootComponentNode->AtkResNode.X;
|
||||
float ny = nameplateObject.RootComponentNode->AtkResNode.Y;
|
||||
float nw = nameplateObject.RootComponentNode->AtkResNode.Width;
|
||||
float nh = nameplateObject.RootComponentNode->AtkResNode.Height;
|
||||
screenPos = new Vector2(nx + nw / 2f, ny + nh);
|
||||
// Sanity: when addon hidden, if pos looks stale (off-screen), fall back to WorldToScreen
|
||||
if (hidingAddon && (screenPos.X < -500 || screenPos.X > 3000 || screenPos.Y < -500 || screenPos.Y > 3000))
|
||||
{
|
||||
Plugin.GameGui.WorldToScreen(worldPos, out screenPos);
|
||||
useAddonPos = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Plugin.GameGui.WorldToScreen(worldPos, out screenPos);
|
||||
}
|
||||
screenPos = ClampScreenPosition(screenPos);
|
||||
// Temporal smoothing for WorldToScreen-sourced positions (addon pos is usually stable)
|
||||
uint objId = obj->GetGameObjectId().ObjectId;
|
||||
if (!useAddonPos && _smoothedPositions.TryGetValue(objId, out Vector2 prev))
|
||||
{
|
||||
float factor = isPlayer ? PlayerPositionSmoothFactor : PositionSmoothFactor;
|
||||
screenPos = Vector2.Lerp(prev, screenPos, factor);
|
||||
}
|
||||
nextSmoothed[objId] = screenPos;
|
||||
|
||||
// distance
|
||||
float distance = Vector3.Distance(camera.Object.Position, worldPos);
|
||||
|
||||
@@ -158,12 +193,18 @@ namespace HSUI.Interface.Nameplates
|
||||
isTitlePrefix = customTitleData.IsPrefix;
|
||||
}
|
||||
|
||||
// state icon
|
||||
int iconId = 0;
|
||||
AtkUldAsset* textureInfo = nameplateObject.NameIcon->PartsList->Parts[nameplateObject.NameIcon->PartId].UldAsset;
|
||||
if (textureInfo != null && textureInfo->AtkTexture.Resource != null)
|
||||
// Quest/state icon: use GameObject.NamePlateIconId (game logic, works when addon hidden).
|
||||
// Fallback to addon's NameIcon texture if GameObject has none (addon must be visible).
|
||||
int iconId = (int)obj->NamePlateIconId;
|
||||
if (iconId == 0)
|
||||
{
|
||||
iconId = (int)textureInfo->AtkTexture.Resource->IconId;
|
||||
try
|
||||
{
|
||||
AtkUldAsset* textureInfo = nameplateObject.NameIcon->PartsList->Parts[nameplateObject.NameIcon->PartId].UldAsset;
|
||||
if (textureInfo != null && textureInfo->AtkTexture.Resource != null)
|
||||
iconId = (int)textureInfo->AtkTexture.Resource->IconId;
|
||||
}
|
||||
catch { /* addon node may be null/stale when hidden */ }
|
||||
}
|
||||
|
||||
// order
|
||||
@@ -206,6 +247,7 @@ namespace HSUI.Interface.Nameplates
|
||||
catch { }
|
||||
}
|
||||
|
||||
_smoothedPositions = nextSmoothed;
|
||||
_data.Reverse();
|
||||
|
||||
// add target nameplate last
|
||||
@@ -253,22 +295,14 @@ namespace HSUI.Interface.Nameplates
|
||||
float margin = 20;
|
||||
|
||||
if (pos.X + nameplateSize.X > screenSize.X)
|
||||
{
|
||||
pos.X = screenSize.X - nameplateSize.X - margin;
|
||||
}
|
||||
else if (pos.X - nameplateSize.X < 0)
|
||||
{
|
||||
pos.X = nameplateSize.X + margin;
|
||||
}
|
||||
|
||||
if (pos.Y + nameplateSize.Y > screenSize.Y)
|
||||
{
|
||||
pos.Y = screenSize.Y - nameplateSize.Y - margin;
|
||||
}
|
||||
else if (pos.Y - nameplateSize.Y < 0)
|
||||
{
|
||||
pos.Y = nameplateSize.Y + margin;
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user