fix(ui): select and examine favorite spells like retail
This commit is contained in:
parent
043ab10b3c
commit
3e31b0ac70
23 changed files with 974 additions and 34 deletions
|
|
@ -405,6 +405,39 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
canSend: () => late.Session.IsInWorld);
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.MagicRuntimeCreated);
|
||||
|
||||
uint MagicSkillLevel(MagicSchool school)
|
||||
{
|
||||
static uint SkillId(MagicSchool value) => value switch
|
||||
{
|
||||
MagicSchool.CreatureEnchantment => 0x1Fu,
|
||||
MagicSchool.ItemEnchantment => 0x20u,
|
||||
MagicSchool.LifeMagic => 0x21u,
|
||||
MagicSchool.WarMagic => 0x22u,
|
||||
MagicSchool.VoidMagic => 0x2Bu,
|
||||
_ => 0u,
|
||||
};
|
||||
|
||||
uint skillId = SkillId(school);
|
||||
if (skillId != 0u)
|
||||
return d.LocalPlayer.GetSkill(skillId)?.CurrentLevel ?? 0u;
|
||||
|
||||
uint highest = 0u;
|
||||
foreach (MagicSchool candidate in new[]
|
||||
{
|
||||
MagicSchool.CreatureEnchantment,
|
||||
MagicSchool.ItemEnchantment,
|
||||
MagicSchool.LifeMagic,
|
||||
MagicSchool.WarMagic,
|
||||
MagicSchool.VoidMagic,
|
||||
})
|
||||
{
|
||||
highest = Math.Max(
|
||||
highest,
|
||||
d.LocalPlayer.GetSkill(SkillId(candidate))?.CurrentLevel ?? 0u);
|
||||
}
|
||||
return highest;
|
||||
}
|
||||
|
||||
foreach (IMouse mouse in d.Input.Mice)
|
||||
host.WireMouse(mouse);
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.MouseInputWired);
|
||||
|
|
@ -500,6 +533,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
iconComposer.GetSpellComponentIcon,
|
||||
d.Selection,
|
||||
d.MagicCatalog.GetSpellLevel,
|
||||
magic.GetExamineComponents,
|
||||
MagicSkillLevel,
|
||||
guid => d.Selection.Select(guid, SelectionChangeSource.Inventory),
|
||||
guid => late.Session.TryUseItem(guid, d.Log),
|
||||
(tab, position, spellId) =>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@ public sealed record SpellComponentDescriptor(
|
|||
uint Category,
|
||||
uint IconId);
|
||||
|
||||
/// <summary>One resolved formula cell in retail's spell examination subview.</summary>
|
||||
public readonly record struct SpellExamineComponent(
|
||||
uint SpellComponentId,
|
||||
SpellComponentDescriptor Descriptor,
|
||||
bool Owned);
|
||||
|
||||
/// <summary>
|
||||
/// App-layer projection of retail's spell, component, and school-focus DAT tables.
|
||||
/// Keeping this catalog outside <c>GameWindow</c> makes the SCID/WCID boundary and
|
||||
|
|
@ -52,6 +58,20 @@ public sealed class MagicCatalog
|
|||
public CoreSpellTable SpellTable { get; }
|
||||
public IReadOnlyDictionary<uint, SpellComponentDescriptor> Components { get; }
|
||||
|
||||
public bool TryGetComponentBySpellComponentId(
|
||||
uint spellComponentId,
|
||||
out SpellComponentDescriptor descriptor)
|
||||
{
|
||||
if (_wcidByScid.TryGetValue(spellComponentId, out uint weenieClassId)
|
||||
&& Components.TryGetValue(weenieClassId, out SpellComponentDescriptor? found))
|
||||
{
|
||||
descriptor = found;
|
||||
return true;
|
||||
}
|
||||
descriptor = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsComponentPack(uint weenieClassId)
|
||||
=> Components.ContainsKey(weenieClassId);
|
||||
|
||||
|
|
@ -143,15 +163,43 @@ public sealed class MagicCatalog
|
|||
/// </summary>
|
||||
public sealed class MagicRuntime
|
||||
{
|
||||
private MagicRuntime(MagicCatalog catalog, SpellCastingController casting)
|
||||
private readonly SpellComponentRequirementService _requirements;
|
||||
|
||||
private MagicRuntime(
|
||||
MagicCatalog catalog,
|
||||
SpellCastingController casting,
|
||||
SpellComponentRequirementService requirements)
|
||||
{
|
||||
Catalog = catalog;
|
||||
Casting = casting;
|
||||
_requirements = requirements;
|
||||
}
|
||||
|
||||
public MagicCatalog Catalog { get; }
|
||||
public SpellCastingController Casting { get; }
|
||||
|
||||
public IReadOnlyList<SpellExamineComponent> GetExamineComponents(uint spellId)
|
||||
{
|
||||
IReadOnlyList<uint> formula = _requirements.GetAppropriateFormula(spellId);
|
||||
if (formula.Count == 0)
|
||||
return [];
|
||||
|
||||
var result = new List<SpellExamineComponent>(formula.Count);
|
||||
foreach (uint spellComponentId in formula)
|
||||
{
|
||||
if (spellComponentId == 0u
|
||||
|| !Catalog.TryGetComponentBySpellComponentId(
|
||||
spellComponentId,
|
||||
out SpellComponentDescriptor descriptor))
|
||||
continue;
|
||||
result.Add(new SpellExamineComponent(
|
||||
spellComponentId,
|
||||
descriptor,
|
||||
_requirements.IsComponentOwned(spellComponentId)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static MagicRuntime Create(
|
||||
MagicCatalog catalog,
|
||||
Spellbook spellbook,
|
||||
|
|
@ -198,7 +246,7 @@ public sealed class MagicRuntime
|
|||
incrementBusy: incrementBusy,
|
||||
canSend: canSend,
|
||||
targetCompatibleSilent: (target, spell) => TargetCompatible(target, spell, false));
|
||||
return new MagicRuntime(catalog, casting);
|
||||
return new MagicRuntime(catalog, casting, requirements);
|
||||
}
|
||||
|
||||
public void Reset() => Casting.Reset();
|
||||
|
|
|
|||
|
|
@ -75,6 +75,31 @@ public sealed class SpellComponentRequirementService
|
|||
/// (0x00567D50): an infused school or a directly carried school focus uses
|
||||
/// the scarab-only formula; otherwise the account-customized formula wins.
|
||||
/// </summary>
|
||||
public IReadOnlyList<uint> GetAppropriateFormula(uint spellId)
|
||||
{
|
||||
if (!_formulas.TryGetValue(spellId, out SpellFormulaDefinition? formula))
|
||||
return [];
|
||||
uint playerGuid = _playerGuid();
|
||||
return GetAppropriateFormula(
|
||||
formula,
|
||||
_objects.Get(playerGuid),
|
||||
playerGuid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the component tracker would leave a formula icon unghosted.
|
||||
/// The input is a retail spell-component id (SCID), not a weenie class id.
|
||||
/// </summary>
|
||||
public bool IsComponentOwned(uint spellComponentId)
|
||||
{
|
||||
if (!_wcidByScid.TryGetValue(spellComponentId, out uint weenieClassId))
|
||||
return false;
|
||||
uint playerGuid = _playerGuid();
|
||||
return _objects.Objects.Any(item =>
|
||||
item.WeenieClassId == weenieClassId
|
||||
&& IsOwnedByPlayer(item, playerGuid));
|
||||
}
|
||||
|
||||
private IReadOnlyList<uint> GetAppropriateFormula(
|
||||
SpellFormulaDefinition formula,
|
||||
ClientObject? player,
|
||||
|
|
|
|||
|
|
@ -553,6 +553,31 @@ public sealed class ItemInteractionController : IDisposable
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases only the object-appraisal transaction before the local spell
|
||||
/// examination subview takes ownership of the shared floaty window.
|
||||
///
|
||||
/// Retail <c>gmExaminationUI::ExamineSpell @ 0x004B6900</c> decrements the
|
||||
/// appraisal busy reference when a response is pending, clears both pending
|
||||
/// and current object ids, and sends <c>CM_Item::Event_Appraise(0)</c>. It
|
||||
/// does not clear unrelated use/inventory busy references.
|
||||
/// </summary>
|
||||
public void CancelObjectAppraisalForSpell()
|
||||
{
|
||||
if (_awaitingAppraisalId == 0u && _currentAppraisalId == 0u)
|
||||
return;
|
||||
|
||||
if (_awaitingAppraisalId != 0u)
|
||||
{
|
||||
_awaitingAppraisalId = 0u;
|
||||
if (_busyCount > 0)
|
||||
_busyCount--;
|
||||
}
|
||||
_currentAppraisalId = 0u;
|
||||
_sendExamine?.Invoke(0u);
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Projects retail <c>gmToolbarUI::HandleSelectionChanged @ 0x004BF380</c>
|
||||
/// from the live selected object's public description. This is deliberately
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Globalization;
|
||||
using System.Text;
|
||||
using AcDream.App.Spells;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
|
@ -32,6 +34,13 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
public const uint CreatureDisplayNameId = 0x1000014Eu;
|
||||
public const uint CreatureLevelValueId = 0x1000014Cu;
|
||||
public const uint SpellPanelId = 0x10000153u;
|
||||
public const uint SpellSchoolTextId = 0x1000015Eu;
|
||||
public const uint SpellIconId = 0x1000015Fu;
|
||||
public const uint SpellManaTextId = 0x10000160u;
|
||||
public const uint SpellDurationTextId = 0x10000161u;
|
||||
public const uint SpellRangeTextId = 0x10000162u;
|
||||
public const uint SpellDisplayTextId = 0x10000163u;
|
||||
public const uint SpellFormulaListId = 0x1000032Du;
|
||||
|
||||
private const uint TemplateStringProperty = 5u;
|
||||
private const uint CharacterMarkerIntProperty = 0x105u;
|
||||
|
|
@ -51,7 +60,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
private readonly Action _closeWindow;
|
||||
private readonly UiElement _itemPanel;
|
||||
private readonly UiElement _creaturePanel;
|
||||
private readonly UiElement? _spellPanel;
|
||||
private readonly UiElement _spellPanel;
|
||||
private readonly UiText _title;
|
||||
private readonly UiText _itemText;
|
||||
private readonly UiText? _inscriptionText;
|
||||
|
|
@ -64,6 +73,22 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
private readonly CreatureAppraisalRowTemplateFactory? _creatureRowTemplates;
|
||||
private readonly CreatureDisplayNameResolver _creatureNames;
|
||||
private readonly RetailAppraisalNameResolver _itemNames;
|
||||
private readonly Func<uint, uint> _resolveSpellIcon;
|
||||
private readonly Func<uint, uint> _resolveComponentIcon;
|
||||
private readonly Func<uint, IReadOnlyList<SpellExamineComponent>> _spellComponents;
|
||||
private readonly Func<MagicSchool, uint> _magicSkill;
|
||||
private readonly SpellExamineComponentTemplateFactory? _spellComponentTemplates;
|
||||
private readonly UiText _spellSchool;
|
||||
private readonly UiText _spellMana;
|
||||
private readonly UiText _spellDuration;
|
||||
private readonly UiText _spellRange;
|
||||
private readonly UiText _spellDisplay;
|
||||
private readonly UiElement _spellIconHost;
|
||||
private readonly UiElement _spellFormulaHost;
|
||||
private readonly UiTextureElement _spellIcon;
|
||||
private readonly List<UiElement> _spellFormulaCells = [];
|
||||
private readonly float _spellFormulaOriginX;
|
||||
private readonly float _spellFormulaCellWidth;
|
||||
private AppraisalView _activeView;
|
||||
private uint _itemObjectId;
|
||||
private uint _creatureObjectId;
|
||||
|
|
@ -75,6 +100,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
private string _scribeName = string.Empty;
|
||||
private string _oldInscription = string.Empty;
|
||||
private bool _presentationInscribable;
|
||||
private uint _spellId;
|
||||
private bool _windowVisible;
|
||||
private double _refreshElapsed;
|
||||
private bool _disposed;
|
||||
|
|
@ -93,11 +119,17 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
Action close,
|
||||
UiElement itemPanel,
|
||||
UiElement creaturePanel,
|
||||
UiElement spellPanel,
|
||||
UiText title,
|
||||
UiText itemText,
|
||||
CreatureAppraisalRowTemplateFactory? creatureRowTemplates,
|
||||
CreatureDisplayNameResolver? creatureNames,
|
||||
RetailAppraisalNameResolver? itemNames)
|
||||
RetailAppraisalNameResolver? itemNames,
|
||||
Func<uint, uint>? resolveSpellIcon,
|
||||
Func<uint, uint>? resolveComponentIcon,
|
||||
Func<uint, IReadOnlyList<SpellExamineComponent>>? spellComponents,
|
||||
Func<MagicSchool, uint>? magicSkill,
|
||||
SpellExamineComponentTemplateFactory? spellComponentTemplates)
|
||||
{
|
||||
_layout = layout;
|
||||
_objects = objects;
|
||||
|
|
@ -112,7 +144,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
_closeWindow = close;
|
||||
_itemPanel = itemPanel;
|
||||
_creaturePanel = creaturePanel;
|
||||
_spellPanel = layout.FindElement(SpellPanelId);
|
||||
_spellPanel = spellPanel;
|
||||
_title = title;
|
||||
_itemText = itemText;
|
||||
_creatureRowTemplates = creatureRowTemplates;
|
||||
|
|
@ -120,6 +152,20 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
?? new CreatureDisplayNameResolver(
|
||||
new Dictionary<uint, string>());
|
||||
_itemNames = itemNames ?? RetailAppraisalNameResolver.Empty;
|
||||
_resolveSpellIcon = resolveSpellIcon ?? (_ => 0u);
|
||||
_resolveComponentIcon = resolveComponentIcon ?? (_ => 0u);
|
||||
_spellComponents = spellComponents ?? (_ => []);
|
||||
_magicSkill = magicSkill ?? (_ => 0u);
|
||||
_spellComponentTemplates = spellComponentTemplates;
|
||||
_spellSchool = (UiText)layout.FindElement(SpellSchoolTextId)!;
|
||||
_spellMana = (UiText)layout.FindElement(SpellManaTextId)!;
|
||||
_spellDuration = (UiText)layout.FindElement(SpellDurationTextId)!;
|
||||
_spellRange = (UiText)layout.FindElement(SpellRangeTextId)!;
|
||||
_spellDisplay = (UiText)layout.FindElement(SpellDisplayTextId)!;
|
||||
_spellIconHost = layout.FindElement(SpellIconId)!;
|
||||
_spellFormulaHost = layout.FindElement(SpellFormulaListId)!;
|
||||
_spellFormulaOriginX = _spellFormulaHost.Left;
|
||||
_spellFormulaCellWidth = _spellFormulaHost.Width;
|
||||
_inscriptionText = layout.FindElement(InscriptionTextId) as UiText;
|
||||
_inscriptionField = layout.FindElement(InscriptionTextId) as UiField;
|
||||
_signature = layout.FindElement(SignatureTextId) as UiText;
|
||||
|
|
@ -164,6 +210,22 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
_inscriptionBackground.ClickThrough = false;
|
||||
}
|
||||
|
||||
if (_spellIconHost is UiDatElement spellIconDat)
|
||||
spellIconDat.MediaVisible = false;
|
||||
_spellIcon = new UiTextureElement
|
||||
{
|
||||
Width = _spellIconHost.Width,
|
||||
Height = _spellIconHost.Height,
|
||||
Anchors = AnchorEdges.Left | AnchorEdges.Top
|
||||
| AnchorEdges.Right | AnchorEdges.Bottom,
|
||||
};
|
||||
_spellIconHost.AddChild(_spellIcon);
|
||||
ConfigureSpellText(_spellSchool);
|
||||
ConfigureSpellText(_spellMana);
|
||||
ConfigureSpellText(_spellDuration);
|
||||
ConfigureSpellText(_spellRange);
|
||||
ConfigureSpellText(_spellDisplay, wrap: true);
|
||||
|
||||
if (creatureRowTemplates is not null
|
||||
&& layout.FindElement(CreatureStatsListId) is { } statsHost
|
||||
&& layout.FindElement(CreatureExtraListId) is { } extraHost
|
||||
|
|
@ -184,6 +246,11 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
}
|
||||
|
||||
_selection.Changed += HandleSelectionChanged;
|
||||
_objects.ObjectAdded += HandleSpellComponentObjectChanged;
|
||||
_objects.ObjectMoved += HandleSpellComponentObjectMoved;
|
||||
_objects.ObjectRemoved += HandleSpellComponentObjectChanged;
|
||||
_objects.StackSizeUpdated += HandleSpellComponentObjectChanged;
|
||||
_objects.Cleared += HandleSpellComponentObjectsCleared;
|
||||
SetActiveView(AppraisalView.Item);
|
||||
}
|
||||
|
||||
|
|
@ -204,7 +271,12 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
Action close,
|
||||
CreatureAppraisalRowTemplateFactory? creatureRowTemplates = null,
|
||||
CreatureDisplayNameResolver? creatureNames = null,
|
||||
RetailAppraisalNameResolver? itemNames = null)
|
||||
RetailAppraisalNameResolver? itemNames = null,
|
||||
Func<uint, uint>? resolveSpellIcon = null,
|
||||
Func<uint, uint>? resolveComponentIcon = null,
|
||||
Func<uint, IReadOnlyList<SpellExamineComponent>>? spellComponents = null,
|
||||
Func<MagicSchool, uint>? magicSkill = null,
|
||||
SpellExamineComponentTemplateFactory? spellComponentTemplates = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(layout);
|
||||
ArgumentNullException.ThrowIfNull(objects);
|
||||
|
|
@ -220,8 +292,16 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
|
||||
if (layout.FindElement(ItemPanelId) is not { } itemPanel
|
||||
|| layout.FindElement(CreaturePanelId) is not { } creaturePanel
|
||||
|| layout.FindElement(SpellPanelId) is not { } spellPanel
|
||||
|| layout.FindElement(TitleId) is not UiText title
|
||||
|| layout.FindElement(ItemTextId) is not UiText itemText)
|
||||
|| layout.FindElement(ItemTextId) is not UiText itemText
|
||||
|| layout.FindElement(SpellSchoolTextId) is not UiText
|
||||
|| layout.FindElement(SpellManaTextId) is not UiText
|
||||
|| layout.FindElement(SpellDurationTextId) is not UiText
|
||||
|| layout.FindElement(SpellRangeTextId) is not UiText
|
||||
|| layout.FindElement(SpellDisplayTextId) is not UiText
|
||||
|| layout.FindElement(SpellIconId) is null
|
||||
|| layout.FindElement(SpellFormulaListId) is null)
|
||||
return null;
|
||||
|
||||
return new AppraisalUiController(
|
||||
|
|
@ -238,11 +318,79 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
close,
|
||||
itemPanel,
|
||||
creaturePanel,
|
||||
spellPanel,
|
||||
title,
|
||||
itemText,
|
||||
creatureRowTemplates,
|
||||
creatureNames,
|
||||
itemNames);
|
||||
itemNames,
|
||||
resolveSpellIcon,
|
||||
resolveComponentIcon,
|
||||
spellComponents,
|
||||
magicSkill,
|
||||
spellComponentTemplates);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens retail's local spell-examination subview. Unlike object appraisal,
|
||||
/// this performs no network request and never changes the globally selected
|
||||
/// object. Source: <c>gmExaminationUI::ExamineSpell @ 0x004B6900</c> and
|
||||
/// <c>SpellExamineUI::ExamineSpell @ 0x004B6210</c>.
|
||||
/// </summary>
|
||||
public bool ExamineSpell(uint spellId)
|
||||
{
|
||||
if (spellId == 0u
|
||||
|| !_spellbook.TryGetMetadata(spellId, out SpellMetadata metadata))
|
||||
return false;
|
||||
|
||||
_interaction.CancelObjectAppraisalForSpell();
|
||||
_spellId = spellId;
|
||||
_titleValue = metadata.Name;
|
||||
SetSpellText(_spellSchool, $"School: {metadata.School}");
|
||||
|
||||
string mana = metadata.ManaCost > 0
|
||||
? metadata.ManaCost.ToString(CultureInfo.InvariantCulture)
|
||||
: "???";
|
||||
if (metadata.ManaModifier > 0u)
|
||||
mana += $" + {metadata.ManaModifier.ToString(CultureInfo.InvariantCulture)} per target";
|
||||
SetSpellText(_spellMana, $"Mana: {mana}");
|
||||
|
||||
string duration = string.Empty;
|
||||
if (metadata.Duration > 0f && metadata.Duration != -1f)
|
||||
{
|
||||
uint displayed = metadata.Duration >= 60f
|
||||
? (uint)(metadata.Duration / 60f)
|
||||
: (uint)metadata.Duration;
|
||||
duration = metadata.Duration >= 60f
|
||||
? $"Duration: {displayed.ToString(CultureInfo.InvariantCulture)} min."
|
||||
: $"Duration: {displayed.ToString(CultureInfo.InvariantCulture)} sec.";
|
||||
}
|
||||
SetSpellText(_spellDuration, duration);
|
||||
|
||||
float range = MathF.Min(
|
||||
metadata.BaseRangeConstant
|
||||
+ metadata.BaseRangeModifier * _magicSkill(metadata.SchoolId),
|
||||
75f);
|
||||
SetSpellText(
|
||||
_spellRange,
|
||||
range > 0f
|
||||
? string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Range: {0:F1} yds.",
|
||||
range / 0.9144f)
|
||||
: string.Empty);
|
||||
|
||||
IReadOnlyList<SpellExamineComponent> components =
|
||||
_spellComponents(spellId);
|
||||
SetSpellText(_spellDisplay, BuildSpellDisplay(metadata, components));
|
||||
_spellDisplay.Scroll.SetScrollY(0);
|
||||
_spellIcon.Texture = _resolveSpellIcon(spellId);
|
||||
RebuildSpellFormula(components);
|
||||
|
||||
SetActiveView(AppraisalView.Spell);
|
||||
_refreshElapsed = 0;
|
||||
_show();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -335,7 +483,15 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
_itemObjectId = 0;
|
||||
_creatureObjectId = 0;
|
||||
_characterObjectId = 0;
|
||||
_spellId = 0u;
|
||||
_refreshElapsed = 0;
|
||||
_spellIcon.Texture = 0u;
|
||||
SetSpellText(_spellSchool, string.Empty);
|
||||
SetSpellText(_spellMana, string.Empty);
|
||||
SetSpellText(_spellDuration, string.Empty);
|
||||
SetSpellText(_spellRange, string.Empty);
|
||||
SetSpellText(_spellDisplay, string.Empty);
|
||||
ClearSpellFormula();
|
||||
SetActiveView(AppraisalView.Item);
|
||||
ClearCreatureText();
|
||||
}
|
||||
|
|
@ -579,6 +735,75 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
scrollbar.Model = text.Scroll;
|
||||
}
|
||||
|
||||
private static void ConfigureSpellText(UiText text, bool wrap = false)
|
||||
{
|
||||
text.PreserveEndOnLayout = false;
|
||||
text.ClickThrough = true;
|
||||
if (wrap)
|
||||
{
|
||||
text.OneLine = false;
|
||||
text.VerticalJustify = VJustify.Top;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetSpellText(UiText text, string value)
|
||||
=> text.LinesProvider = () => IndicatorDetailText.Shape(text, value);
|
||||
|
||||
private static string BuildSpellDisplay(
|
||||
SpellMetadata metadata,
|
||||
IReadOnlyList<SpellExamineComponent> components)
|
||||
{
|
||||
var display = new StringBuilder(metadata.Description);
|
||||
if (components.Count == 0)
|
||||
return display.ToString();
|
||||
|
||||
display.Append("\nCOMPONENTS:");
|
||||
foreach (SpellExamineComponent component in components)
|
||||
display.Append("\n ").Append(component.Descriptor.Name);
|
||||
return display.ToString();
|
||||
}
|
||||
|
||||
private void RebuildSpellFormula(
|
||||
IReadOnlyList<SpellExamineComponent> components)
|
||||
{
|
||||
ClearSpellFormula();
|
||||
if (_spellComponentTemplates is null || components.Count == 0)
|
||||
{
|
||||
_spellFormulaHost.Width = _spellFormulaCellWidth;
|
||||
_spellFormulaHost.Left = _spellFormulaOriginX;
|
||||
return;
|
||||
}
|
||||
|
||||
float cellWidth = _spellFormulaCellWidth;
|
||||
_spellFormulaHost.Left =
|
||||
_spellFormulaOriginX - cellWidth * 0.5f * (components.Count - 1);
|
||||
_spellFormulaHost.Width = cellWidth * components.Count;
|
||||
_spellFormulaHost.ResetAnchorCapture();
|
||||
for (int i = 0; i < components.Count; i++)
|
||||
{
|
||||
SpellExamineComponent component = components[i];
|
||||
UiElement cell = _spellComponentTemplates.Create(
|
||||
_resolveComponentIcon(component.Descriptor.WeenieClassId),
|
||||
component.Owned);
|
||||
cell.LayoutPolicy = null;
|
||||
cell.Anchors = AnchorEdges.Left | AnchorEdges.Top;
|
||||
cell.Left = i * cellWidth;
|
||||
cell.Top = 0f;
|
||||
_spellFormulaHost.AddChild(cell);
|
||||
_spellFormulaCells.Add(cell);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearSpellFormula()
|
||||
{
|
||||
foreach (UiElement cell in _spellFormulaCells)
|
||||
_spellFormulaHost.RemoveChild(cell);
|
||||
_spellFormulaCells.Clear();
|
||||
_spellFormulaHost.Left = _spellFormulaOriginX;
|
||||
_spellFormulaHost.Width = _spellFormulaCellWidth;
|
||||
_spellFormulaHost.ResetAnchorCapture();
|
||||
}
|
||||
|
||||
private void SetText(uint elementId, string value, bool scrollable = false)
|
||||
{
|
||||
if (_layout.FindElement(elementId) is not UiText text)
|
||||
|
|
@ -623,8 +848,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
_activeView = view;
|
||||
_itemPanel.Visible = view == AppraisalView.Item;
|
||||
_creaturePanel.Visible = view is AppraisalView.Creature or AppraisalView.Character;
|
||||
if (_spellPanel is not null)
|
||||
_spellPanel.Visible = false;
|
||||
_spellPanel.Visible = view == AppraisalView.Spell;
|
||||
|
||||
UiElement? creatureInfo = _layout.FindElement(0x1000014Du);
|
||||
UiElement? characterInfo = _layout.FindElement(0x1000014Fu);
|
||||
|
|
@ -704,12 +928,39 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
_closeWindow();
|
||||
}
|
||||
|
||||
private void HandleSpellComponentObjectChanged(ClientObject _)
|
||||
=> RefreshSpellComponents();
|
||||
|
||||
private void HandleSpellComponentObjectMoved(ClientObjectMove _)
|
||||
=> RefreshSpellComponents();
|
||||
|
||||
private void HandleSpellComponentObjectsCleared()
|
||||
=> RefreshSpellComponents();
|
||||
|
||||
private void RefreshSpellComponents()
|
||||
{
|
||||
if (_activeView != AppraisalView.Spell
|
||||
|| _spellId == 0u
|
||||
|| !_spellbook.TryGetMetadata(_spellId, out SpellMetadata metadata))
|
||||
return;
|
||||
|
||||
IReadOnlyList<SpellExamineComponent> components =
|
||||
_spellComponents(_spellId);
|
||||
SetSpellText(_spellDisplay, BuildSpellDisplay(metadata, components));
|
||||
RebuildSpellFormula(components);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
_selection.Changed -= HandleSelectionChanged;
|
||||
_objects.ObjectAdded -= HandleSpellComponentObjectChanged;
|
||||
_objects.ObjectMoved -= HandleSpellComponentObjectMoved;
|
||||
_objects.ObjectRemoved -= HandleSpellComponentObjectChanged;
|
||||
_objects.StackSizeUpdated -= HandleSpellComponentObjectChanged;
|
||||
_objects.Cleared -= HandleSpellComponentObjectsCleared;
|
||||
if (_close is not null)
|
||||
_close.OnClick = null;
|
||||
if (_inscriptionField is not null)
|
||||
|
|
@ -720,6 +971,8 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
}
|
||||
if (_inscriptionBackground is not null)
|
||||
_inscriptionBackground.OnClick = null;
|
||||
ClearSpellFormula();
|
||||
_spellIconHost.RemoveChild(_spellIcon);
|
||||
foreach (UiScrollbar scrollbar in Descendants(_layout.Root).OfType<UiScrollbar>())
|
||||
if (ReferenceEquals(scrollbar.Model, _itemText.Scroll)
|
||||
|| (_inscriptionText is not null
|
||||
|
|
@ -735,4 +988,5 @@ public enum AppraisalView
|
|||
Item,
|
||||
Creature,
|
||||
Character,
|
||||
Spell,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
using AcDream.Content;
|
||||
using DatReaderWriter;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates the authored formula-icon template used by retail
|
||||
/// <c>SpellExamineUI::ExamineSpell @ 0x004B6210</c>. The controller replaces
|
||||
/// the template root's placeholder image with the resolved component icon while
|
||||
/// retaining child <c>0x10000330</c> as the missing-component overlay.
|
||||
/// </summary>
|
||||
public sealed class SpellExamineComponentTemplateFactory
|
||||
{
|
||||
public const uint TemplateId = 0x1000032Eu;
|
||||
public const uint MissingOverlayId = 0x10000330u;
|
||||
|
||||
private readonly ElementInfo _template;
|
||||
private readonly Func<uint, (uint tex, int w, int h)> _resolveSprite;
|
||||
private readonly UiDatFont? _defaultFont;
|
||||
private readonly IReadOnlyDictionary<uint, UiDatFont?> _fonts;
|
||||
|
||||
public SpellExamineComponentTemplateFactory(
|
||||
ElementInfo template,
|
||||
Func<uint, (uint tex, int w, int h)> resolveSprite,
|
||||
UiDatFont? defaultFont,
|
||||
IReadOnlyDictionary<uint, UiDatFont?>? fonts = null)
|
||||
{
|
||||
_template = template ?? throw new ArgumentNullException(nameof(template));
|
||||
_resolveSprite = resolveSprite ?? throw new ArgumentNullException(nameof(resolveSprite));
|
||||
_defaultFont = defaultFont;
|
||||
_fonts = fonts ?? new Dictionary<uint, UiDatFont?>();
|
||||
}
|
||||
|
||||
public static SpellExamineComponentTemplateFactory? TryLoad(
|
||||
IDatReaderWriter dats,
|
||||
Func<uint, (uint tex, int w, int h)> resolveSprite,
|
||||
UiDatFont? defaultFont,
|
||||
Func<uint, UiDatFont?>? resolveFont)
|
||||
{
|
||||
ElementInfo? template = LayoutImporter.ImportInfos(
|
||||
dats,
|
||||
AppraisalUiController.LayoutId,
|
||||
TemplateId);
|
||||
if (template is null)
|
||||
return null;
|
||||
|
||||
var fonts = new Dictionary<uint, UiDatFont?>();
|
||||
CaptureFonts(template, resolveFont, fonts);
|
||||
return new SpellExamineComponentTemplateFactory(
|
||||
template,
|
||||
resolveSprite,
|
||||
defaultFont,
|
||||
fonts);
|
||||
}
|
||||
|
||||
public UiElement Create(uint iconTexture, bool owned)
|
||||
{
|
||||
ImportedLayout content = LayoutImporter.Build(
|
||||
_template,
|
||||
_resolveSprite,
|
||||
_defaultFont,
|
||||
did => _fonts.TryGetValue(did, out UiDatFont? font)
|
||||
? font
|
||||
: _defaultFont);
|
||||
UiElement root = content.Root;
|
||||
if (root is UiDatElement datRoot)
|
||||
datRoot.MediaVisible = false;
|
||||
|
||||
var icon = new UiTextureElement
|
||||
{
|
||||
Width = root.Width,
|
||||
Height = root.Height,
|
||||
Anchors = AnchorEdges.Left | AnchorEdges.Top
|
||||
| AnchorEdges.Right | AnchorEdges.Bottom,
|
||||
Texture = iconTexture,
|
||||
ZOrder = int.MinValue / 2,
|
||||
};
|
||||
root.AddChild(icon);
|
||||
|
||||
if (content.FindElement(MissingOverlayId) is { } missing)
|
||||
missing.Visible = !owned;
|
||||
return root;
|
||||
}
|
||||
|
||||
private static void CaptureFonts(
|
||||
ElementInfo info,
|
||||
Func<uint, UiDatFont?>? resolveFont,
|
||||
Dictionary<uint, UiDatFont?> fonts)
|
||||
{
|
||||
if (info.FontDid != 0u && !fonts.ContainsKey(info.FontDid))
|
||||
fonts[info.FontDid] = resolveFont?.Invoke(info.FontDid);
|
||||
foreach (ElementInfo child in info.Children)
|
||||
CaptureFonts(child, resolveFont, fonts);
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
private readonly Func<uint, uint> _resolveSpellIcon;
|
||||
private readonly Func<ClientObject, uint> _resolveItemDragIcon;
|
||||
private readonly Action<uint> _useItem;
|
||||
private readonly Action<uint>? _examineSpell;
|
||||
private readonly Action<int, int, uint>? _addFavorite;
|
||||
private readonly Action<int, uint>? _removeFavorite;
|
||||
private readonly UiShortcutDigitGraphics? _shortcutDigits;
|
||||
|
|
@ -74,6 +75,7 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
Func<uint, uint> resolveSpellIcon,
|
||||
Func<ClientObject, uint> resolveItemDragIcon,
|
||||
Action<uint> useItem,
|
||||
Action<uint>? examineSpell,
|
||||
SelectionState selection,
|
||||
Action<int, int, uint>? addFavorite,
|
||||
Action<int, uint>? removeFavorite,
|
||||
|
|
@ -94,6 +96,7 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
_resolveSpellIcon = resolveSpellIcon;
|
||||
_resolveItemDragIcon = resolveItemDragIcon;
|
||||
_useItem = useItem;
|
||||
_examineSpell = examineSpell;
|
||||
_addFavorite = addFavorite;
|
||||
_removeFavorite = removeFavorite;
|
||||
_shortcutDigits = shortcutDigits;
|
||||
|
|
@ -120,7 +123,11 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
|
||||
for (int i = 0; i < _lists.Length; i++)
|
||||
{
|
||||
if (_lists[i] is not { } list || _scrollbars[i] is not { } scrollbar)
|
||||
if (_lists[i] is not { } list)
|
||||
continue;
|
||||
list.PrimaryCatalogEntryPressed = SelectSpell;
|
||||
list.ExamineCatalogEntryRequested = _examineSpell;
|
||||
if (_scrollbars[i] is not { } scrollbar)
|
||||
continue;
|
||||
list.HorizontalScroll = true;
|
||||
scrollbar.Horizontal = true;
|
||||
|
|
@ -161,7 +168,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
Action<int, int, uint>? addFavorite,
|
||||
Action<int, uint>? removeFavorite,
|
||||
UiShortcutDigitGraphics? shortcutDigits = null,
|
||||
uint emptySlotSprite = 0u)
|
||||
uint emptySlotSprite = 0u,
|
||||
Action<uint>? examineSpell = null)
|
||||
{
|
||||
if (layout.FindElement(CastButtonId) is not UiButton cast
|
||||
|| layout.FindElement(EndowmentId) is not { } endowmentHost)
|
||||
|
|
@ -185,7 +193,7 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
|
||||
return new SpellcastingUiController(
|
||||
layout, spellbook, casting, objects, playerGuid, resolveSpellIcon,
|
||||
resolveItemDragIcon, useItem, selection,
|
||||
resolveItemDragIcon, useItem, examineSpell, selection,
|
||||
addFavorite, removeFavorite,
|
||||
tabs, groups, lists, scrollbars, cast, endowmentHost,
|
||||
shortcutDigits, emptySlotSprite);
|
||||
|
|
@ -357,8 +365,11 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
DropSpellbookShortcut(shortcut, targetTab, position);
|
||||
},
|
||||
};
|
||||
slot.Clicked = () => SelectSpell(id);
|
||||
slot.DoubleClicked = () => { SelectSpell(id); CastSelected(); };
|
||||
// gmSpellcastingUI::ListenToElementMessage @ 0x004C7AB0
|
||||
// selects on left press (message parameter 7). A completed
|
||||
// double click casts, but release is not required merely to
|
||||
// change the current spell.
|
||||
slot.DoubleClicked = CastSelected;
|
||||
list.AddItem(slot);
|
||||
}
|
||||
}
|
||||
|
|
@ -557,6 +568,12 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
_objects.ObjectRemoved -= OnObjectChanged;
|
||||
_objects.Cleared -= OnObjectsCleared;
|
||||
foreach (UiElement tab in _tabs) SetClick(tab, null);
|
||||
foreach (UiItemList? list in _lists)
|
||||
{
|
||||
if (list is null) continue;
|
||||
list.PrimaryCatalogEntryPressed = null;
|
||||
list.ExamineCatalogEntryRequested = null;
|
||||
}
|
||||
_cast.OnClick = null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ public sealed record MagicRuntimeBindings(
|
|||
Func<uint, uint> ResolveComponentIcon,
|
||||
SelectionState Selection,
|
||||
Func<uint, int> SpellLevel,
|
||||
Func<uint, IReadOnlyList<SpellExamineComponent>> SpellComponents,
|
||||
Func<MagicSchool, uint> MagicSkill,
|
||||
Action<uint> SelectObject,
|
||||
Action<uint> UseItem,
|
||||
Action<int, int, uint> AddFavorite,
|
||||
|
|
@ -831,7 +833,8 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
_bindings.Magic.AddFavorite,
|
||||
_bindings.Magic.RemoveFavorite,
|
||||
LoadShortcutDigitGraphics(),
|
||||
favoriteEmptySprite);
|
||||
favoriteEmptySprite,
|
||||
examineSpell: spellId => AppraisalController?.ExamineSpell(spellId));
|
||||
if (spellcasting is null)
|
||||
Console.WriteLine("[M3] spellcasting: required controls missing in LayoutDesc 0x21000073.");
|
||||
|
||||
|
|
@ -965,6 +968,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
{
|
||||
ImportedLayout? layout;
|
||||
CreatureAppraisalRowTemplateFactory? creatureRows;
|
||||
SpellExamineComponentTemplateFactory? spellComponentTemplates;
|
||||
CreatureDisplayNameResolver? creatureNames;
|
||||
RetailAppraisalNameResolver? itemNames;
|
||||
lock (_bindings.Assets.DatLock)
|
||||
|
|
@ -981,6 +985,12 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
_bindings.Assets.ResolveSprite,
|
||||
_bindings.Assets.DefaultFont,
|
||||
_bindings.Assets.ResolveFont);
|
||||
spellComponentTemplates =
|
||||
SpellExamineComponentTemplateFactory.TryLoad(
|
||||
_bindings.Assets.Dats,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
_bindings.Assets.DefaultFont,
|
||||
_bindings.Assets.ResolveFont);
|
||||
creatureNames = CreatureDisplayNameResolver.Load(
|
||||
_bindings.Assets.Dats);
|
||||
itemNames = RetailAppraisalNameResolver.Load(
|
||||
|
|
@ -1008,7 +1018,12 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
close: () => CloseWindow(WindowNames.Examination),
|
||||
creatureRowTemplates: creatureRows,
|
||||
creatureNames: creatureNames,
|
||||
itemNames: itemNames);
|
||||
itemNames: itemNames,
|
||||
resolveSpellIcon: _bindings.Magic.ResolveSpellIcon,
|
||||
resolveComponentIcon: _bindings.Magic.ResolveComponentIcon,
|
||||
spellComponents: _bindings.Magic.SpellComponents,
|
||||
magicSkill: _bindings.Magic.MagicSkill,
|
||||
spellComponentTemplates: spellComponentTemplates);
|
||||
if (controller is null)
|
||||
{
|
||||
Console.WriteLine(
|
||||
|
|
|
|||
|
|
@ -58,6 +58,9 @@ public sealed class UiCatalogSlot : UiItemSlot
|
|||
switch (e.Type)
|
||||
{
|
||||
case UiEventType.MouseDown:
|
||||
if (EntryId != 0u
|
||||
&& FindList() is { PrimaryCatalogEntryPressed: { } pressed })
|
||||
pressed(EntryId);
|
||||
return true;
|
||||
case UiEventType.Click:
|
||||
Clicked?.Invoke();
|
||||
|
|
@ -65,6 +68,11 @@ public sealed class UiCatalogSlot : UiItemSlot
|
|||
case UiEventType.DoubleClick:
|
||||
DoubleClicked?.Invoke();
|
||||
return true;
|
||||
case UiEventType.RightClick:
|
||||
if (EntryId != 0u
|
||||
&& FindList() is { ExamineCatalogEntryRequested: { } examine })
|
||||
examine(EntryId);
|
||||
return true;
|
||||
case UiEventType.DragBegin:
|
||||
if (e.Payload is not null) DragBegan?.Invoke(e.Payload);
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -96,6 +96,25 @@ public sealed class UiItemList : UiElement
|
|||
/// </summary>
|
||||
public Action<uint>? ExamineItemRequested { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Non-weenie catalog-entry left-button-down command supplied by the owning
|
||||
/// panel. Spell favorites use this distinct path so a spell id is never
|
||||
/// published as the globally selected object GUID.
|
||||
///
|
||||
/// Retail <c>gmSpellcastingUI::ListenToElementMessage @ 0x004C7AB0</c>
|
||||
/// handles UIItem message parameter 7 (left press) by calling
|
||||
/// <c>SpellCastSubMenu::SetSelected</c>.
|
||||
/// </summary>
|
||||
public Action<uint>? PrimaryCatalogEntryPressed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Local examination command for non-weenie catalog entries. Retail
|
||||
/// <c>UIElement_ItemList::ListenToElementMessage @ 0x004E4D50</c>
|
||||
/// routes right-clicked <c>spellID</c> values to
|
||||
/// <c>ClientUISystem::ExamineSpell</c> without changing the selected object.
|
||||
/// </summary>
|
||||
public Action<uint>? ExamineCatalogEntryRequested { get; set; }
|
||||
|
||||
private uint _cellEmptySprite;
|
||||
/// <summary>Empty-slot sprite for THIS list's cells, resolved from the dat cell template
|
||||
/// (retail attribute 0x1000000e -> catalog 0x21000037 prototype's ItemSlot_Empty; see
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ public class UiItemSlot : UiElement
|
|||
public override bool IsDragSource => ItemId != 0;
|
||||
|
||||
/// <summary>Walk up to the containing <see cref="UiItemList"/> (the drop handler owner).</summary>
|
||||
private UiItemList? FindList()
|
||||
protected UiItemList? FindList()
|
||||
{
|
||||
UiElement? e = Parent;
|
||||
while (e is not null) { if (e is UiItemList l) return l; e = e.Parent; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue