using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using AcDream.Core.Spells;
namespace AcDream.App.UI.Layout;
/// Retail gmEffectsUI positive/negative instance binding.
public sealed class EffectsUiController : IRetainedPanelController
{
public const uint LayoutId = 0x2100001Bu;
// gmPanelUI's 0x10000184/185 are panel-slot IDs, not roots in this LayoutDesc.
// The authored positive/negative gmEffectsUI instances inherit template 0x10000122.
public const uint PositiveRootId = 0x1000011Fu;
public const uint NegativeRootId = 0x10000121u;
public const uint CloseId = 0x100000FCu;
public const uint ListId = 0x10000123u;
public const uint InfoTextId = 0x10000126u;
private readonly Spellbook _spellbook;
private readonly bool _positive;
private readonly Func _serverTime;
private readonly Func _resolveSpellIcon;
private readonly UiItemList _list;
private readonly UiText? _info;
private readonly UiButton? _close;
private readonly Dictionary _rows = new();
private uint? _selectedSpellId;
private double _lastDurationUpdate = double.NaN;
private bool _disposed;
internal uint? SelectedSpellId => _selectedSpellId;
private EffectsUiController(
ImportedLayout layout,
Spellbook spellbook,
bool positive,
Func serverTime,
Func resolveSpellIcon,
UiItemList list,
Action? close)
{
_spellbook = spellbook;
_positive = positive;
_serverTime = serverTime;
_resolveSpellIcon = resolveSpellIcon;
_list = list;
_info = layout.FindElement(InfoTextId) as UiText;
_close = layout.FindElement(CloseId) as UiButton;
if (_close is not null) _close.OnClick = close;
_list.Columns = 1;
_list.CellWidth = Math.Max(1f, _list.Width);
_list.CellHeight = 32f;
ConfigureInfo();
_spellbook.EnchantmentsChanged += Rebuild;
Rebuild();
}
public static EffectsUiController? Bind(
ImportedLayout layout,
Spellbook spellbook,
bool positive,
Func serverTime,
Func spriteResolve,
Func resolveSpellIcon,
Action? close = null)
{
UiElement? host = layout.FindElement(ListId);
if (host is null) return null;
UiItemList list;
if (host is UiItemList itemList)
list = itemList;
else
{
list = new UiItemList(spriteResolve)
{
Width = host.Width,
Height = host.Height,
Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right | AnchorEdges.Bottom,
};
host.AddChild(list);
}
return new EffectsUiController(
layout, spellbook, positive, serverTime, resolveSpellIcon, list, close);
}
public void Tick()
{
double now = _serverTime();
if (!double.IsFinite(now)) return;
if (double.IsFinite(_lastDurationUpdate)
&& now >= _lastDurationUpdate
&& now - _lastDurationUpdate < 1.0)
return;
_lastDurationUpdate = now;
foreach (ActiveEnchantmentRecord enchantment in VisibleEnchantments())
if (_rows.TryGetValue(enchantment.Identity, out UiCatalogSlot? row))
row.Detail = FormatRemaining(enchantment, now);
}
private void Rebuild()
{
_lastDurationUpdate = double.NaN;
_rows.Clear();
ActiveEnchantmentRecord[] enchantments = VisibleEnchantments().ToArray();
using (_list.DeferLayout())
{
_list.Flush();
foreach (ActiveEnchantmentRecord enchantment in enchantments)
{
_spellbook.TryGetMetadata(enchantment.SpellId, out SpellMetadata? metadata);
uint identity = enchantment.Identity;
var row = new UiCatalogSlot
{
EntryId = enchantment.SpellId,
CatalogIconTexture = metadata is null ? 0u : _resolveSpellIcon(enchantment.SpellId),
Label = metadata?.Name ?? $"Spell {enchantment.SpellId}",
Detail = FormatRemaining(enchantment, _serverTime()),
ShowLabel = true,
SpriteResolve = _list.SpriteResolve,
};
row.Clicked = () => Select(enchantment.SpellId);
_rows[identity] = row;
_list.AddItem(row);
}
}
if (_selectedSpellId is uint selected
&& !_rows.Values.Any(row => row.EntryId == selected))
_selectedSpellId = null;
SyncSelection();
}
private IEnumerable VisibleEnchantments()
=> _spellbook.EnchantmentsInEffectSnapshot
.Where(record =>
{
if (!_spellbook.TryGetMetadata(record.SpellId, out SpellMetadata metadata))
return false;
return metadata.IsBeneficial == _positive;
})
.OrderBy(record => _spellbook.TryGetMetadata(record.SpellId, out SpellMetadata metadata)
? metadata.Name : record.SpellId.ToString(CultureInfo.InvariantCulture),
StringComparer.OrdinalIgnoreCase);
private static string FormatRemaining(ActiveEnchantmentRecord enchantment, double now)
{
if (enchantment.Duration < 0) return "Permanent";
double remaining = Math.Max(0, enchantment.StartTime + enchantment.Duration - now);
if (!double.IsFinite(remaining)) return "--:--";
remaining = Math.Min(remaining, TimeSpan.MaxValue.TotalSeconds);
TimeSpan time = TimeSpan.FromSeconds(remaining);
return time.TotalHours >= 1
? $"{(int)time.TotalHours}:{time.Minutes:00}:{time.Seconds:00}"
: $"{time.Minutes}:{time.Seconds:00}";
}
private void Select(uint spellId)
{
// Retail gmEffectsUI::SetSelectedSpell @ 0x004B8290 stores the
// token's spell stat, not its enchantment layer identity.
_selectedSpellId = _selectedSpellId == spellId ? null : spellId;
SyncSelection();
}
private void SyncSelection()
{
foreach (UiCatalogSlot row in _rows.Values)
row.Selected = row.EntryId == _selectedSpellId;
}
private void ConfigureInfo()
{
if (_info is null) return;
_info.LinesProvider = () =>
{
if (_selectedSpellId is not uint spellId)
return Array.Empty();
ActiveEnchantmentRecord? selected = _spellbook.ActiveEnchantmentSnapshot
.Cast()
.FirstOrDefault(record => record?.SpellId == spellId);
if (selected is null || !_spellbook.TryGetMetadata(selected.Value.SpellId, out SpellMetadata metadata))
return Array.Empty();
return
[
new UiText.Line(metadata.Name, _info.DefaultColor),
new UiText.Line(metadata.Description, _info.DefaultColor),
];
};
}
public void OnShown() => Rebuild();
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_spellbook.EnchantmentsChanged -= Rebuild;
if (_close is not null) _close.OnClick = null;
}
}