feat(items): port retail shared cooldown overlays
Preserve public shared-cooldown metadata, resolve the authoritative cooldown enchantment with retail expiry semantics, and project the exact ten DAT-authored radial steps through the shared retained item-slot architecture. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
19e8863f4e
commit
6b1ae4fb76
27 changed files with 875 additions and 20 deletions
|
|
@ -193,6 +193,17 @@ public sealed class ClientObject
|
|||
public ushort? AmmoType { get; set; }
|
||||
/// <summary>Retail <c>PublicWeenieDesc._spellID</c>; used by caster endowments.</summary>
|
||||
public uint? SpellId { get; set; }
|
||||
/// <summary>
|
||||
/// Retail <c>PublicWeenieDesc._cooldown_id</c>. Positive values name a
|
||||
/// shared item-cooldown group whose player enchantment id is
|
||||
/// <c>CooldownId + 0x8000</c>.
|
||||
/// </summary>
|
||||
public uint? CooldownId { get; set; }
|
||||
/// <summary>
|
||||
/// Retail <c>PublicWeenieDesc._cooldown_duration</c>, in seconds. This is
|
||||
/// the denominator used by <c>UIElement_UIItem::UpdateCooldownDisplay</c>.
|
||||
/// </summary>
|
||||
public double? CooldownDuration { get; set; }
|
||||
/// <summary>Client-side trade state used by ItemHolder legality gates.</summary>
|
||||
public int TradeState { get; set; }
|
||||
/// <summary>Resolved membership in SpellComponentTable (retail IsComponentPack).</summary>
|
||||
|
|
@ -261,7 +272,9 @@ public readonly record struct WeenieData(
|
|||
string? PluralName = null,
|
||||
uint? PetOwnerId = null,
|
||||
ushort? AmmoType = null,
|
||||
uint? SpellId = null);
|
||||
uint? SpellId = null,
|
||||
uint? CooldownId = null,
|
||||
double? CooldownDuration = null);
|
||||
|
||||
/// <summary>
|
||||
/// Retail ITEM_USEABLE helpers (acclient.h:6478, ItemUses::* at 0x004fccd0).
|
||||
|
|
|
|||
|
|
@ -194,6 +194,8 @@ public sealed class ClientObjectTable
|
|||
/// <see cref="ClientObject.Effects"/>.</summary>
|
||||
public const uint UiEffectsPropertyId = 18u;
|
||||
public const uint CurrentWieldedLocationPropertyId = 10u;
|
||||
public const uint SharedCooldownPropertyId = 280u;
|
||||
public const uint CooldownDurationPropertyId = 167u;
|
||||
|
||||
public int ObjectCount => _objects.Count;
|
||||
public int ContainerCount => _containers.Count;
|
||||
|
|
@ -613,6 +615,7 @@ public sealed class ClientObjectTable
|
|||
foreach (var kv in incoming.Strings) item.Properties.Strings[kv.Key] = kv.Value;
|
||||
foreach (var kv in incoming.DataIds) item.Properties.DataIds[kv.Key] = kv.Value;
|
||||
foreach (var kv in incoming.InstanceIds) item.Properties.InstanceIds[kv.Key] = kv.Value;
|
||||
ApplyCooldownProperties(item, incoming);
|
||||
ObjectUpdated?.Invoke(item);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -640,6 +643,7 @@ public sealed class ClientObjectTable
|
|||
foreach (var kv in incoming.Strings) item.Properties.Strings[kv.Key] = kv.Value;
|
||||
foreach (var kv in incoming.DataIds) item.Properties.DataIds[kv.Key] = kv.Value;
|
||||
foreach (var kv in incoming.InstanceIds) item.Properties.InstanceIds[kv.Key] = kv.Value;
|
||||
ApplyCooldownProperties(item, incoming);
|
||||
if (!existed) ObjectAdded?.Invoke(item); else ObjectUpdated?.Invoke(item);
|
||||
}
|
||||
|
||||
|
|
@ -656,6 +660,7 @@ public sealed class ClientObjectTable
|
|||
ClientObjectPlacement previous = ClientObjectPlacement.From(item);
|
||||
item.Properties.Ints[propertyId] = value;
|
||||
if (propertyId == UiEffectsPropertyId) item.Effects = (uint)value;
|
||||
if (propertyId == SharedCooldownPropertyId) item.CooldownId = (uint)value;
|
||||
if (propertyId == CurrentWieldedLocationPropertyId)
|
||||
item.CurrentlyEquippedLocation = (EquipMask)(uint)value;
|
||||
if (propertyId == CurrentWieldedLocationPropertyId)
|
||||
|
|
@ -664,6 +669,20 @@ public sealed class ClientObjectTable
|
|||
return true;
|
||||
}
|
||||
|
||||
private static void ApplyCooldownProperties(
|
||||
ClientObject item,
|
||||
PropertyBundle properties)
|
||||
{
|
||||
if (properties.Ints.TryGetValue(
|
||||
SharedCooldownPropertyId,
|
||||
out int cooldownId))
|
||||
item.CooldownId = (uint)cooldownId;
|
||||
if (properties.Floats.TryGetValue(
|
||||
CooldownDurationPropertyId,
|
||||
out double cooldownDuration))
|
||||
item.CooldownDuration = cooldownDuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a single PropertyInt64 update to an object's bundle and fire
|
||||
/// ObjectUpdated so bound widgets refresh. Int64 counterpart of
|
||||
|
|
@ -738,6 +757,9 @@ public sealed class ClientObjectTable
|
|||
if (d.CombatUse is { } combatUse) obj.CombatUse = combatUse;
|
||||
if (d.AmmoType is { } ammoType) obj.AmmoType = ammoType;
|
||||
if (d.SpellId is { } spellId) obj.SpellId = spellId;
|
||||
if (d.CooldownId is { } cooldownId) obj.CooldownId = cooldownId;
|
||||
if (d.CooldownDuration is { } cooldownDuration)
|
||||
obj.CooldownDuration = cooldownDuration;
|
||||
if (d.RadarBlipColor is { } radarBlipColor) obj.RadarBlipColor = radarBlipColor;
|
||||
if (d.RadarBehavior is { } radarBehavior) obj.RadarBehavior = radarBehavior;
|
||||
if (d.ItemsCapacity is { } ic) obj.ItemsCapacity = ic;
|
||||
|
|
|
|||
31
src/AcDream.Core/Items/ItemCooldownDisplay.cs
Normal file
31
src/AcDream.Core/Items/ItemCooldownDisplay.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
namespace AcDream.Core.Items;
|
||||
|
||||
/// <summary>
|
||||
/// Retail item-cooldown projection shared by inventory, equipment, and shortcut
|
||||
/// UIItems.
|
||||
///
|
||||
/// <para>
|
||||
/// Port of <c>UIElement_UIItem::UpdateCooldownDisplay @ 0x004E1E20</c>.
|
||||
/// The remaining fraction selects one of ten authored radial overlays.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class ItemCooldownDisplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Retail's exact ten-step display formula:
|
||||
/// <c>trunc((remaining / itemDuration) * 10 + 1)</c>. Results 1..9 select
|
||||
/// the 10%..90% art, while 10 or above selects the 100% art.
|
||||
/// </summary>
|
||||
public static int GetOverlayStep(double itemDuration, double remaining)
|
||||
{
|
||||
if (!(itemDuration > 0d) || !(remaining > 0d))
|
||||
return 0;
|
||||
|
||||
double step = Math.Truncate((remaining / itemDuration) * 10d + 1d);
|
||||
if (!(step >= 1d))
|
||||
return 0;
|
||||
if (step >= 10d)
|
||||
return 10;
|
||||
return (int)step;
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,9 @@ namespace AcDream.Core.Spells;
|
|||
/// </summary>
|
||||
public sealed class Spellbook
|
||||
{
|
||||
public const uint CooldownSpellOffset = 0x8000u;
|
||||
public const uint CooldownBucket = 8u;
|
||||
|
||||
private readonly Dictionary<uint, float> _learnedSpells = new();
|
||||
private readonly Dictionary<uint, ActiveEnchantmentRecord> _activeById = new();
|
||||
private readonly Dictionary<uint, List<uint>> _enchantmentOrderByBucket = new();
|
||||
|
|
@ -240,6 +243,46 @@ public sealed class Spellbook
|
|||
if (changed) NotifyEnchantmentsChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CEnchantmentRegistry::OnCooldown @ 0x005943C0</c>.
|
||||
/// Scans the cooldown list head-to-tail, stops at the first shared-group
|
||||
/// match, and removes that node when its monotonic lifetime has expired.
|
||||
/// </summary>
|
||||
public bool OnCooldown(
|
||||
uint cooldownId,
|
||||
double currentTime,
|
||||
out double remaining)
|
||||
{
|
||||
remaining = 0d;
|
||||
if (cooldownId == 0u
|
||||
|| !_enchantmentOrderByBucket.TryGetValue(
|
||||
CooldownBucket,
|
||||
out List<uint>? order))
|
||||
return false;
|
||||
|
||||
uint cooldownSpellId = unchecked(cooldownId + CooldownSpellOffset);
|
||||
for (int index = 0; index < order.Count; index++)
|
||||
{
|
||||
uint identity = order[index];
|
||||
if (!_activeById.TryGetValue(
|
||||
identity,
|
||||
out ActiveEnchantmentRecord record)
|
||||
|| (record.SpellId & 0xFFFFu) != cooldownSpellId)
|
||||
continue;
|
||||
|
||||
remaining = record.Duration + record.StartTime - currentTime;
|
||||
if (remaining > 0d)
|
||||
return true;
|
||||
|
||||
RemoveActiveEnchantment(identity, out _);
|
||||
EnchantmentRemoved?.Invoke(record);
|
||||
NotifyEnchantmentsChanged();
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail EnchantmentRegistry::PurgeEnchantments (0x00594DE0): purge only
|
||||
/// finite-duration multiplicative/additive enchantments. Vitae, cooldowns,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue