using AcDream.Core.Items;
using AcDream.Core.Spells;
namespace AcDream.App.UI.Layout;
///
/// Shared retained-UI owner for retail item cooldowns. It performs the retail
/// heartbeat before drawing and gives every current and future
/// the same display projection.
///
public sealed class ItemCooldownUiController
{
private readonly Spellbook _spellbook;
private readonly ClientObjectTable _objects;
private readonly Func _currentTime;
private readonly List _lists = new();
private readonly Dictionary _stepByItemId = new();
private ItemCooldownUiController(
Spellbook spellbook,
ClientObjectTable objects,
Func currentTime)
{
_spellbook = spellbook;
_objects = objects;
_currentTime = currentTime;
}
public static ItemCooldownUiController Bind(
UiElement root,
Spellbook spellbook,
ClientObjectTable objects,
Func currentTime,
ItemCooldownAssets assets)
{
ArgumentNullException.ThrowIfNull(root);
ArgumentNullException.ThrowIfNull(spellbook);
ArgumentNullException.ThrowIfNull(objects);
ArgumentNullException.ThrowIfNull(currentTime);
var controller = new ItemCooldownUiController(
spellbook,
objects,
currentTime);
controller.Configure(root, assets.Sprites);
controller.Tick();
return controller;
}
public void Tick()
{
_stepByItemId.Clear();
if (!_spellbook.HasCooldownEnchantments)
return;
double now = _currentTime();
// Retail updates cooldown visibility from UIElement_UIItem::DoHeartbeat,
// not while drawing. Query each represented item once so an expired
// registry node can be removed safely before the render walk. A hidden
// retained subtree receives no heartbeat in retail, so only lists whose
// complete ancestry is visible participate.
foreach (UiItemList list in _lists)
{
if (!IsEffectivelyVisible(list))
continue;
for (int index = 0; index < list.GetNumUIItems(); index++)
{
UiItemSlot? cell = list.GetItem(index);
if (cell is null)
continue;
uint itemId = cell.ItemId;
if (itemId == 0u || _stepByItemId.ContainsKey(itemId))
continue;
int step = 0;
ClientObject? item = _objects.Get(itemId);
if (item?.CooldownId is { } cooldownId
&& item.CooldownDuration is { } duration
&& _spellbook.OnCooldown(
cooldownId,
now,
out double remaining))
step = ItemCooldownDisplay.GetOverlayStep(
duration,
remaining);
_stepByItemId.Add(itemId, step);
}
}
}
internal static bool IsEffectivelyVisible(UiElement element)
{
ArgumentNullException.ThrowIfNull(element);
for (UiElement? current = element; current is not null; current = current.Parent)
{
if (!current.Visible)
return false;
}
return true;
}
internal int GetOverlayStep(uint itemId)
=> _stepByItemId.TryGetValue(itemId, out int step) ? step : 0;
private void Configure(UiElement element, IReadOnlyList sprites)
{
if (element is UiItemList list)
{
_lists.Add(list);
list.CooldownSprites = sprites;
list.CooldownStepProvider = GetOverlayStep;
}
foreach (UiElement child in element.Children)
Configure(child, sprites);
}
}