fix(ui): complete retail indicator detail panels
Port the authored effect row template, remaining-time and selection details, synchronize the full gmPanelUI child geometry, and route the burden indicator to Character Information panel 3. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
a96767ba6d
commit
d1d603105f
24 changed files with 3696 additions and 147 deletions
|
|
@ -16,16 +16,24 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
public const uint NegativeRootId = 0x10000121u;
|
||||
public const uint CloseId = 0x100000FCu;
|
||||
public const uint ListId = 0x10000123u;
|
||||
public const uint ScrollbarId = 0x10000124u;
|
||||
public const uint InfoTextId = 0x10000126u;
|
||||
public const uint RowTemplateId = 0x10000128u;
|
||||
public const uint RowIconId = 0x10000129u;
|
||||
public const uint RowLabelId = 0x1000012Au;
|
||||
public const uint RowDurationId = 0x1000012Bu;
|
||||
|
||||
private readonly Spellbook _spellbook;
|
||||
private readonly bool _positive;
|
||||
private readonly Func<double> _serverTime;
|
||||
private readonly Func<uint, uint> _resolveSpellIcon;
|
||||
private readonly EffectRowTemplateFactory _templates;
|
||||
private readonly string _selectPrompt;
|
||||
private readonly UiItemList _list;
|
||||
private readonly UiText? _info;
|
||||
private readonly UiButton? _close;
|
||||
private readonly Dictionary<uint, UiCatalogSlot> _rows = new();
|
||||
private readonly UiScrollbar? _scrollbar;
|
||||
private readonly Dictionary<uint, EffectRowTemplateFactory.EffectRow> _rows = new();
|
||||
private uint? _selectedSpellId;
|
||||
private double _lastDurationUpdate = double.NaN;
|
||||
private bool _disposed;
|
||||
|
|
@ -38,6 +46,8 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
bool positive,
|
||||
Func<double> serverTime,
|
||||
Func<uint, uint> resolveSpellIcon,
|
||||
EffectRowTemplateFactory templates,
|
||||
string selectPrompt,
|
||||
UiItemList list,
|
||||
Action? close)
|
||||
{
|
||||
|
|
@ -45,13 +55,18 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
_positive = positive;
|
||||
_serverTime = serverTime;
|
||||
_resolveSpellIcon = resolveSpellIcon;
|
||||
_templates = templates;
|
||||
_selectPrompt = selectPrompt;
|
||||
_list = list;
|
||||
_info = layout.FindElement(InfoTextId) as UiText;
|
||||
_close = layout.FindElement(CloseId) as UiButton;
|
||||
_scrollbar = layout.FindElement(ScrollbarId) as UiScrollbar;
|
||||
if (_close is not null) _close.OnClick = close;
|
||||
_list.Columns = 1;
|
||||
_list.CellWidth = Math.Max(1f, _list.Width);
|
||||
_list.CellHeight = 32f;
|
||||
_list.CellWidth = templates.Width;
|
||||
_list.CellHeight = templates.Height;
|
||||
if (_scrollbar is not null)
|
||||
_scrollbar.Model = _list.Scroll;
|
||||
ConfigureInfo();
|
||||
_spellbook.EnchantmentsChanged += Rebuild;
|
||||
Rebuild();
|
||||
|
|
@ -64,6 +79,8 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
Func<double> serverTime,
|
||||
Func<uint, (uint Texture, int Width, int Height)> spriteResolve,
|
||||
Func<uint, uint> resolveSpellIcon,
|
||||
EffectRowTemplateFactory templates,
|
||||
string selectPrompt,
|
||||
Action? close = null)
|
||||
{
|
||||
UiElement? host = layout.FindElement(ListId);
|
||||
|
|
@ -82,7 +99,15 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
host.AddChild(list);
|
||||
}
|
||||
return new EffectsUiController(
|
||||
layout, spellbook, positive, serverTime, resolveSpellIcon, list, close);
|
||||
layout,
|
||||
spellbook,
|
||||
positive,
|
||||
serverTime,
|
||||
resolveSpellIcon,
|
||||
templates,
|
||||
selectPrompt,
|
||||
list,
|
||||
close);
|
||||
}
|
||||
|
||||
public void Tick()
|
||||
|
|
@ -95,8 +120,10 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
return;
|
||||
_lastDurationUpdate = now;
|
||||
foreach (ActiveEnchantmentRecord enchantment in VisibleEnchantments())
|
||||
if (_rows.TryGetValue(enchantment.Identity, out UiCatalogSlot? row))
|
||||
row.Detail = FormatRemaining(enchantment, now);
|
||||
if (_rows.TryGetValue(
|
||||
enchantment.Identity,
|
||||
out EffectRowTemplateFactory.EffectRow? row))
|
||||
row.Remaining = FormatRemaining(enchantment, now);
|
||||
}
|
||||
|
||||
private void Rebuild()
|
||||
|
|
@ -111,22 +138,18 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
{
|
||||
_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);
|
||||
EffectRowTemplateFactory.EffectRow row = _templates.Create(
|
||||
enchantment.SpellId,
|
||||
metadata is null ? 0u : _resolveSpellIcon(enchantment.SpellId),
|
||||
metadata?.Name ?? $"Spell {enchantment.SpellId}",
|
||||
FormatRemaining(enchantment, _serverTime()));
|
||||
row.Slot.Clicked = () => Select(enchantment.SpellId);
|
||||
_rows[identity] = row;
|
||||
_list.AddItem(row);
|
||||
_list.AddItem(row.Slot);
|
||||
}
|
||||
}
|
||||
if (_selectedSpellId is uint selected
|
||||
&& !_rows.Values.Any(row => row.EntryId == selected))
|
||||
&& !_rows.Values.Any(row => row.Slot.EntryId == selected))
|
||||
_selectedSpellId = null;
|
||||
SyncSelection();
|
||||
}
|
||||
|
|
@ -145,7 +168,9 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
|
||||
private static string FormatRemaining(ActiveEnchantmentRecord enchantment, double now)
|
||||
{
|
||||
if (enchantment.Duration < 0) return "Permanent";
|
||||
// EffectInfoRegion::Update @ 0x004F1C00 starts from retail's empty
|
||||
// PString and only formats it when the remaining duration is >= 0.
|
||||
if (enchantment.Duration < 0) return string.Empty;
|
||||
double remaining = Math.Max(0, enchantment.StartTime + enchantment.Duration - now);
|
||||
if (!double.IsFinite(remaining)) return "--:--";
|
||||
remaining = Math.Min(remaining, TimeSpan.MaxValue.TotalSeconds);
|
||||
|
|
@ -165,25 +190,27 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
|
||||
private void SyncSelection()
|
||||
{
|
||||
foreach (UiCatalogSlot row in _rows.Values)
|
||||
row.Selected = row.EntryId == _selectedSpellId;
|
||||
foreach (EffectRowTemplateFactory.EffectRow row in _rows.Values)
|
||||
row.Slot.SetSelected(row.Slot.EntryId == _selectedSpellId);
|
||||
}
|
||||
|
||||
private void ConfigureInfo()
|
||||
{
|
||||
if (_info is null) return;
|
||||
_info.PreserveEndOnLayout = false;
|
||||
_info.LinesProvider = () =>
|
||||
{
|
||||
if (_selectedSpellId is not uint spellId)
|
||||
return Array.Empty<UiText.Line>();
|
||||
return [new UiText.Line(_selectPrompt, _info.DefaultColor)];
|
||||
ActiveEnchantmentRecord? selected = _spellbook.ActiveEnchantmentSnapshot
|
||||
.Cast<ActiveEnchantmentRecord?>()
|
||||
.FirstOrDefault(record => record?.SpellId == spellId);
|
||||
if (selected is null || !_spellbook.TryGetMetadata(selected.Value.SpellId, out SpellMetadata metadata))
|
||||
return Array.Empty<UiText.Line>();
|
||||
return [new UiText.Line(_selectPrompt, _info.DefaultColor)];
|
||||
return
|
||||
[
|
||||
new UiText.Line(metadata.Name, _info.DefaultColor),
|
||||
new UiText.Line(string.Empty, _info.DefaultColor),
|
||||
new UiText.Line(metadata.Description, _info.DefaultColor),
|
||||
];
|
||||
};
|
||||
|
|
@ -197,5 +224,6 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
_disposed = true;
|
||||
_spellbook.EnchantmentsChanged -= Rebuild;
|
||||
if (_close is not null) _close.OnClick = null;
|
||||
if (_scrollbar is not null) _scrollbar.Model = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue