fix(movement): invalidate burden on enchantment changes
This commit is contained in:
parent
2b9dfec9d7
commit
d6e8b60303
13 changed files with 231 additions and 25 deletions
|
|
@ -98,6 +98,43 @@ Copy this block when adding a new issue:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## #272 — Strength enchantments do not invalidate burden
|
||||||
|
|
||||||
|
**Status:** DONE — 2026-07-31 (implementation, automated gates, and user live
|
||||||
|
buff/death gate)
|
||||||
|
**Severity:** HIGH (movement state and retained HUD disagree with retail)
|
||||||
|
**Filed:** 2026-07-31
|
||||||
|
**Component:** player qualities / enchantments / burden
|
||||||
|
|
||||||
|
**Symptom:** while overburdened, casting a Strength spell did not reduce the
|
||||||
|
burden state until the base Strength attribute changed. Dying purged the
|
||||||
|
Strength spell but did not restore the overburdened state.
|
||||||
|
|
||||||
|
**Root cause:** `LiveSessionEventRouter.RecomputeBurden` read raw
|
||||||
|
`AttributeValue.Current` and subscribed only to base Strength/object-table
|
||||||
|
changes. The indicator bar and inventory meter had the same raw-Strength
|
||||||
|
composition, and the inventory meter did not observe enchantment changes.
|
||||||
|
Retail `CACQualities::InqLoad @ 0x0058F130` calls
|
||||||
|
`CACQualities::InqAttribute @ 0x00591A00`, which applies
|
||||||
|
`CACQualities::EnchantAttribute @ 0x00594570`; every load query therefore uses
|
||||||
|
effective Strength.
|
||||||
|
|
||||||
|
**Fix:** all three consumers now read
|
||||||
|
`LocalPlayerState.GetEffectiveAttribute(Strength)`. The Runtime burden owner,
|
||||||
|
indicator bar, and inventory meter subscribe to the canonical
|
||||||
|
`Spellbook.EnchantmentsChanged` edge, covering add, remove, expiration,
|
||||||
|
dispel, and death purge through one path. Regression tests pin both
|
||||||
|
buff-to-unburdened and purge-to-overburdened transitions without any base
|
||||||
|
attribute update.
|
||||||
|
|
||||||
|
**Acceptance:** overload a character, cast a Strength spell, and observe the
|
||||||
|
burden icon/meter plus movement update immediately. Die while the spell is
|
||||||
|
active and observe the spell purge restore the overburdened icon/meter and
|
||||||
|
movement immediately. **Passed live 2026-07-31:** the user confirmed the
|
||||||
|
burden state now updates correctly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## #271 — Stair-side collision reverses uphill movement and rapidly slides the player down
|
## #271 — Stair-side collision reverses uphill movement and rapidly slides the player down
|
||||||
|
|
||||||
**Status:** DONE — 2026-07-31 (implementation + user live gate)
|
**Status:** DONE — 2026-07-31 (implementation + user live gate)
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,11 @@ TS-8 are retired by focused and end-to-end packet tests. #269's
|
||||||
capture-driven slope-slide residual is also closed and user-accepted:
|
capture-driven slope-slide residual is also closed and user-accepted:
|
||||||
`CTransition::validate_transition` now performs retail's non-OK-only
|
`CTransition::validate_transition` now performs retail's non-OK-only
|
||||||
remembered-plane restore with the preceding `OBJECTINFO::kill_velocity`.
|
remembered-plane restore with the preceding `OBJECTINFO::kill_velocity`.
|
||||||
Campaign P now continues with the unfinished live matrix rows.
|
The matrix then exposed #272: burden was invalidated by base Strength but not
|
||||||
|
by Strength enchantment add/purge. Runtime movement plus both retained burden
|
||||||
|
surfaces now use effective Strength and the canonical enchantment-change edge;
|
||||||
|
automated gates pass and the connected buff/death gate was user-accepted on
|
||||||
|
2026-07-31. Campaign P then continues with the unfinished live matrix rows.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -400,6 +400,14 @@ root-caused, retail-ported, and user-accepted in the same session:
|
||||||
and the complete Release suite passes 10,062 tests / 5 skips. The user
|
and the complete Release suite passes 10,062 tests / 5 skips. The user
|
||||||
accepted repeated uphill runs while pressing into the stair sides. Evidence:
|
accepted repeated uphill runs while pressing into the stair sides. Evidence:
|
||||||
`docs/research/2026-07-31-271-stair-side-slide-capture.md`.
|
`docs/research/2026-07-31-271-stair-side-slide-capture.md`.
|
||||||
|
- **#272 complete and user-accepted 2026-07-31** —
|
||||||
|
`CACQualities::InqLoad` consumes enchantment-adjusted Strength through
|
||||||
|
`InqAttribute`, but Runtime movement and both retained burden displays read
|
||||||
|
raw Strength and did not share the enchantment invalidation edge. They now
|
||||||
|
consume `GetEffectiveAttribute(Strength)` and
|
||||||
|
`Spellbook.EnchantmentsChanged`, so buff, dispel, expiration, and death
|
||||||
|
purge recompute the same burden state immediately. Focused and full
|
||||||
|
Runtime/App tests pass.
|
||||||
|
|
||||||
Matrix rows accepted so far: speed parity, roof slide, downhill bounce,
|
Matrix rows accepted so far: speed parity, roof slide, downhill bounce,
|
||||||
flat pop, uphill landing, and #269's slope-stop feel
|
flat pop, uphill landing, and #269's slope-stop feel
|
||||||
|
|
|
||||||
|
|
@ -54,11 +54,16 @@ InqLoad(this, &loadOut):
|
||||||
return 1 // always succeeds for CACQualities (has vtable)
|
return 1 // always succeeds for CACQualities (has vtable)
|
||||||
```
|
```
|
||||||
|
|
||||||
This EXACTLY matches acdream's existing `IndicatorBarController.UpdateBurden()`
|
The property/capacity shape matches acdream's
|
||||||
/ `InventoryController.RefreshBurden()` pattern (Strength attribute + prop
|
`IndicatorBarController.UpdateBurden()` /
|
||||||
0xE6 aug + prop 5 EncumbranceVal, falling back to `SumCarriedBurden` when the
|
`InventoryController.RefreshBurden()` pattern (Strength attribute + prop 0xE6
|
||||||
wire value is absent) — already ported, already correct, already tested via
|
aug + prop 5 EncumbranceVal, falling back to `SumCarriedBurden` when the wire
|
||||||
the UI. **`AcDream.Core.Items.BurdenMath`
|
value is absent). A 2026-07-31 connected gate exposed one omitted retail
|
||||||
|
detail: `InqAttribute` returns the enchantment-adjusted attribute, while all
|
||||||
|
three acdream burden consumers still read raw `AttributeValue.Current`.
|
||||||
|
Issue #272 corrects them to `LocalPlayerState.GetEffectiveAttribute(Strength)`
|
||||||
|
and invalidates burden on the canonical `Spellbook.EnchantmentsChanged` edge.
|
||||||
|
**`AcDream.Core.Items.BurdenMath`
|
||||||
(`EncumbranceCapacity`/`LoadRatio`/`LoadModifier`) is the SAME formulas at
|
(`EncumbranceCapacity`/`LoadRatio`/`LoadModifier`) is the SAME formulas at
|
||||||
the SAME addresses.** P1's `EncumbranceSystem` (Physics-namespaced, for
|
the SAME addresses.** P1's `EncumbranceSystem` (Physics-namespaced, for
|
||||||
citation clarity next to `MovementSystem`) delegates to `BurdenMath` rather
|
citation clarity next to `MovementSystem`) delegates to `BurdenMath` rather
|
||||||
|
|
@ -317,8 +322,10 @@ Runtime (AcDream.Runtime, presentation-free):
|
||||||
- onSkillsUpdated callback -> character.Character.UpdateMovementSkillBase(...)
|
- onSkillsUpdated callback -> character.Character.UpdateMovementSkillBase(...)
|
||||||
- NEW: inventory.Objects.{ObjectAdded,ObjectUpdated,ObjectRemoved,ObjectMoved,
|
- NEW: inventory.Objects.{ObjectAdded,ObjectUpdated,ObjectRemoved,ObjectMoved,
|
||||||
ContainerContentsReplaced,Cleared} + LocalPlayer.AttributeChanged(Strength)
|
ContainerContentsReplaced,Cleared} + LocalPlayer.AttributeChanged(Strength)
|
||||||
|
+ Spellbook.EnchantmentsChanged
|
||||||
-> recompute burden (Strength + prop 0xE6 aug + prop 5 EncumbranceVal,
|
-> recompute burden (Strength + prop 0xE6 aug + prop 5 EncumbranceVal,
|
||||||
SAME shape as IndicatorBarController.UpdateBurden/InventoryController.RefreshBurden)
|
SAME shape as IndicatorBarController.UpdateBurden/InventoryController.RefreshBurden)
|
||||||
|
using effective/enchantment-adjusted Strength
|
||||||
-> character.Character.MovementSkills.UpdateBurden(ratio)
|
-> character.Character.MovementSkills.UpdateBurden(ratio)
|
||||||
- NEW: character.Character.LocalPlayer.Changed(VitalKind.Stamina)
|
- NEW: character.Character.LocalPlayer.Changed(VitalKind.Stamina)
|
||||||
-> character.Character.MovementSkills.UpdateStamina(current)
|
-> character.Character.MovementSkills.UpdateStamina(current)
|
||||||
|
|
|
||||||
|
|
@ -99,3 +99,12 @@ InputDispatcher / PlayerMovementController
|
||||||
5 skips. The user accepted repeated uphill stair-side runs on 2026-07-31.
|
5 skips. The user accepted repeated uphill stair-side runs on 2026-07-31.
|
||||||
See
|
See
|
||||||
`docs/research/2026-07-31-271-stair-side-slide-capture.md`.
|
`docs/research/2026-07-31-271-stair-side-slide-capture.md`.
|
||||||
|
- 2026-07-31: #272 burden invalidation. Retail `InqLoad @ 0x0058F130`
|
||||||
|
obtains Strength through `InqAttribute @ 0x00591A00`, so carry capacity
|
||||||
|
always uses the enchantment-adjusted value. Runtime movement, the indicator
|
||||||
|
bar, and the inventory meter must all consume
|
||||||
|
`LocalPlayerState.GetEffectiveAttribute(Strength)` and react to the same
|
||||||
|
`Spellbook.EnchantmentsChanged` event. That one event covers spell add,
|
||||||
|
remove, expiration, dispel, and death purge. Never refresh this through a
|
||||||
|
UI-only workaround or wait for a base-attribute packet. The connected
|
||||||
|
buff/death gate was user-accepted on 2026-07-31.
|
||||||
|
|
|
||||||
|
|
@ -614,9 +614,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
d.Character.Spellbook,
|
d.Character.Spellbook,
|
||||||
d.Inventory.Objects,
|
d.Inventory.Objects,
|
||||||
() => d.PlayerIdentity.ServerGuid,
|
() => d.PlayerIdentity.ServerGuid,
|
||||||
() => d.Character.LocalPlayer.GetAttribute(
|
() => d.Character.LocalPlayer.GetEffectiveAttribute(
|
||||||
LocalPlayerState.AttributeKind.Strength)
|
LocalPlayerState.AttributeKind.Strength),
|
||||||
is { } strength ? (int?)strength.Current : null,
|
|
||||||
() => late.Session.LinkStatus,
|
() => late.Session.LinkStatus,
|
||||||
d.ClientTime,
|
d.ClientTime,
|
||||||
() => late.Session.CurrentSession?.RequestLinkStatusPing(),
|
() => late.Session.CurrentSession?.RequestLinkStatusPing(),
|
||||||
|
|
@ -656,9 +655,9 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
() => d.PlayerIdentity.ServerGuid,
|
() => d.PlayerIdentity.ServerGuid,
|
||||||
iconComposer.GetIcon,
|
iconComposer.GetIcon,
|
||||||
iconComposer.GetDragIcon,
|
iconComposer.GetDragIcon,
|
||||||
() => d.Character.LocalPlayer.GetAttribute(
|
() => d.Character.LocalPlayer.GetEffectiveAttribute(
|
||||||
LocalPlayerState.AttributeKind.Strength)
|
LocalPlayerState.AttributeKind.Strength),
|
||||||
is { } strength ? (int?)strength.Current : null,
|
d.Character.Spellbook,
|
||||||
guid => late.Session.CurrentSession?.SendUse(guid),
|
guid => late.Session.CurrentSession?.SendUse(guid),
|
||||||
(item, container, placement) =>
|
(item, container, placement) =>
|
||||||
late.Session.CurrentSession?.SendPutItemInContainer(
|
late.Session.CurrentSession?.SendPutItemInContainer(
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ public sealed class IndicatorBarController : IRetainedPanelController
|
||||||
_miniGame.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.MiniGame);
|
_miniGame.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.MiniGame);
|
||||||
_endCharacterSession.OnClick = bindings.RequestEndCharacterSession;
|
_endCharacterSession.OnClick = bindings.RequestEndCharacterSession;
|
||||||
|
|
||||||
bindings.Spellbook.EnchantmentsChanged += UpdateEnchantments;
|
bindings.Spellbook.EnchantmentsChanged += OnEnchantmentsChanged;
|
||||||
bindings.Objects.ObjectAdded += OnObjectChanged;
|
bindings.Objects.ObjectAdded += OnObjectChanged;
|
||||||
bindings.Objects.ObjectUpdated += OnObjectChanged;
|
bindings.Objects.ObjectUpdated += OnObjectChanged;
|
||||||
bindings.Objects.ObjectRemoved += OnObjectChanged;
|
bindings.Objects.ObjectRemoved += OnObjectChanged;
|
||||||
|
|
@ -229,6 +229,11 @@ public sealed class IndicatorBarController : IRetainedPanelController
|
||||||
private void OnObjectMoved(ClientObjectMove _) => UpdateBurden();
|
private void OnObjectMoved(ClientObjectMove _) => UpdateBurden();
|
||||||
private void OnContainerContentsReplaced(uint _) => UpdateBurden();
|
private void OnContainerContentsReplaced(uint _) => UpdateBurden();
|
||||||
private void OnObjectsCleared() => UpdateBurden();
|
private void OnObjectsCleared() => UpdateBurden();
|
||||||
|
private void OnEnchantmentsChanged()
|
||||||
|
{
|
||||||
|
UpdateEnchantments();
|
||||||
|
UpdateBurden();
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateBurden()
|
private void UpdateBurden()
|
||||||
{
|
{
|
||||||
|
|
@ -253,7 +258,7 @@ public sealed class IndicatorBarController : IRetainedPanelController
|
||||||
{
|
{
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
_bindings.Spellbook.EnchantmentsChanged -= UpdateEnchantments;
|
_bindings.Spellbook.EnchantmentsChanged -= OnEnchantmentsChanged;
|
||||||
_bindings.Objects.ObjectAdded -= OnObjectChanged;
|
_bindings.Objects.ObjectAdded -= OnObjectChanged;
|
||||||
_bindings.Objects.ObjectUpdated -= OnObjectChanged;
|
_bindings.Objects.ObjectUpdated -= OnObjectChanged;
|
||||||
_bindings.Objects.ObjectRemoved -= OnObjectChanged;
|
_bindings.Objects.ObjectRemoved -= OnObjectChanged;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Numerics;
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Selection;
|
using AcDream.Core.Selection;
|
||||||
|
using AcDream.Core.Spells;
|
||||||
|
|
||||||
namespace AcDream.App.UI.Layout;
|
namespace AcDream.App.UI.Layout;
|
||||||
|
|
||||||
|
|
@ -43,6 +44,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
||||||
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds;
|
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds;
|
||||||
private readonly Func<ItemType, uint, uint, uint, uint, uint>? _dragIconIds;
|
private readonly Func<ItemType, uint, uint, uint, uint, uint>? _dragIconIds;
|
||||||
private readonly Func<int?> _strength;
|
private readonly Func<int?> _strength;
|
||||||
|
private readonly Spellbook? _burdenSpellbook;
|
||||||
private readonly Func<string>? _ownerName;
|
private readonly Func<string>? _ownerName;
|
||||||
|
|
||||||
private readonly UiItemList? _contentsGrid;
|
private readonly UiItemList? _contentsGrid;
|
||||||
|
|
@ -96,13 +98,15 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
||||||
Action<uint, uint>? notifyMergeAttempt,
|
Action<uint, uint>? notifyMergeAttempt,
|
||||||
ItemInteractionController? itemInteraction,
|
ItemInteractionController? itemInteraction,
|
||||||
Action? onClose,
|
Action? onClose,
|
||||||
StackSplitQuantityState? stackSplitQuantity)
|
StackSplitQuantityState? stackSplitQuantity,
|
||||||
|
Spellbook? burdenSpellbook)
|
||||||
{
|
{
|
||||||
_objects = objects;
|
_objects = objects;
|
||||||
_playerGuid = playerGuid;
|
_playerGuid = playerGuid;
|
||||||
_iconIds = iconIds;
|
_iconIds = iconIds;
|
||||||
_dragIconIds = dragIconIds;
|
_dragIconIds = dragIconIds;
|
||||||
_strength = strength;
|
_strength = strength;
|
||||||
|
_burdenSpellbook = burdenSpellbook;
|
||||||
_ownerName = ownerName;
|
_ownerName = ownerName;
|
||||||
_sendUse = sendUse;
|
_sendUse = sendUse;
|
||||||
_sendPutItemInContainer = sendPutItemInContainer;
|
_sendPutItemInContainer = sendPutItemInContainer;
|
||||||
|
|
@ -195,6 +199,8 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
||||||
_objects.ObjectUpdated += OnObjectChanged;
|
_objects.ObjectUpdated += OnObjectChanged;
|
||||||
_objects.Cleared += OnObjectsCleared;
|
_objects.Cleared += OnObjectsCleared;
|
||||||
_selection.Changed += OnSelectionChanged;
|
_selection.Changed += OnSelectionChanged;
|
||||||
|
if (_burdenSpellbook is not null)
|
||||||
|
_burdenSpellbook.EnchantmentsChanged += RefreshBurden;
|
||||||
if (_itemInteraction is not null)
|
if (_itemInteraction is not null)
|
||||||
{
|
{
|
||||||
_itemInteraction.StateChanged += OnInteractionStateChanged;
|
_itemInteraction.StateChanged += OnInteractionStateChanged;
|
||||||
|
|
@ -244,14 +250,15 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
||||||
ItemInteractionController? itemInteraction = null,
|
ItemInteractionController? itemInteraction = null,
|
||||||
Action? onClose = null,
|
Action? onClose = null,
|
||||||
StackSplitQuantityState? stackSplitQuantity = null,
|
StackSplitQuantityState? stackSplitQuantity = null,
|
||||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null)
|
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null,
|
||||||
|
Spellbook? burdenSpellbook = null)
|
||||||
=> new InventoryController(layout, objects, playerGuid, iconIds, dragIconIds, strength, selection,
|
=> new InventoryController(layout, objects, playerGuid, iconIds, dragIconIds, strength, selection,
|
||||||
ownerName, datFont,
|
ownerName, datFont,
|
||||||
contentsEmptySprite, sideBagEmptySprite, mainPackEmptySprite,
|
contentsEmptySprite, sideBagEmptySprite, mainPackEmptySprite,
|
||||||
sendUse, sendPutItemInContainer,
|
sendUse, sendPutItemInContainer,
|
||||||
sendStackableSplitToContainer, sendStackableMerge,
|
sendStackableSplitToContainer, sendStackableMerge,
|
||||||
notifyMergeAttempt, itemInteraction,
|
notifyMergeAttempt, itemInteraction,
|
||||||
onClose, stackSplitQuantity);
|
onClose, stackSplitQuantity, burdenSpellbook);
|
||||||
|
|
||||||
private void OnObjectChanged(ClientObject o)
|
private void OnObjectChanged(ClientObject o)
|
||||||
{
|
{
|
||||||
|
|
@ -944,6 +951,8 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
||||||
_objects.ObjectUpdated -= OnObjectChanged;
|
_objects.ObjectUpdated -= OnObjectChanged;
|
||||||
_objects.Cleared -= OnObjectsCleared;
|
_objects.Cleared -= OnObjectsCleared;
|
||||||
_selection.Changed -= OnSelectionChanged;
|
_selection.Changed -= OnSelectionChanged;
|
||||||
|
if (_burdenSpellbook is not null)
|
||||||
|
_burdenSpellbook.EnchantmentsChanged -= RefreshBurden;
|
||||||
if (_contentsGrid is not null)
|
if (_contentsGrid is not null)
|
||||||
{
|
{
|
||||||
_contentsGrid.PrimaryItemPressed = null;
|
_contentsGrid.PrimaryItemPressed = null;
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,7 @@ public sealed record InventoryRuntimeBindings(
|
||||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
||||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveDragIcon,
|
Func<ItemType, uint, uint, uint, uint, uint> ResolveDragIcon,
|
||||||
Func<int?> Strength,
|
Func<int?> Strength,
|
||||||
|
Spellbook Spellbook,
|
||||||
Action<uint>? SendUse,
|
Action<uint>? SendUse,
|
||||||
Action<uint, uint, int>? SendPutItemInContainer,
|
Action<uint, uint, int>? SendPutItemInContainer,
|
||||||
Action<uint, uint, uint, uint>? SendStackableSplitToContainer,
|
Action<uint, uint, uint, uint>? SendStackableSplitToContainer,
|
||||||
|
|
@ -1816,7 +1817,8 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
notifyMergeAttempt, b.ItemInteraction,
|
notifyMergeAttempt, b.ItemInteraction,
|
||||||
() => CloseWindow(WindowNames.Inventory),
|
() => CloseWindow(WindowNames.Inventory),
|
||||||
StackSplitQuantity,
|
StackSplitQuantity,
|
||||||
b.ResolveDragIcon);
|
b.ResolveDragIcon,
|
||||||
|
b.Spellbook);
|
||||||
InventoryPanelController = inventory;
|
InventoryPanelController = inventory;
|
||||||
PaperdollController paperdoll = PaperdollController.Bind(
|
PaperdollController paperdoll = PaperdollController.Bind(
|
||||||
layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.Selection, b.ItemInteraction,
|
layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.Selection, b.ItemInteraction,
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,10 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
|
||||||
if (kind == LocalPlayerState.AttributeKind.Strength)
|
if (kind == LocalPlayerState.AttributeKind.Strength)
|
||||||
RecomputeBurden(inventory, character);
|
RecomputeBurden(inventory, character);
|
||||||
});
|
});
|
||||||
|
SubscribeParameterless(
|
||||||
|
h => character.Character.Spellbook.EnchantmentsChanged += h,
|
||||||
|
h => character.Character.Spellbook.EnchantmentsChanged -= h,
|
||||||
|
() => RecomputeBurden(inventory, character));
|
||||||
|
|
||||||
// Current-stamina push — CACQualities::InqRunRate/InqJumpVelocity's
|
// Current-stamina push — CACQualities::InqRunRate/InqJumpVelocity's
|
||||||
// stamina==0 effective-skill-zeroing gate (pseudocode doc §5).
|
// stamina==0 effective-skill-zeroing gate (pseudocode doc §5).
|
||||||
|
|
@ -370,8 +374,8 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
|
||||||
{
|
{
|
||||||
uint player = inventory.PlayerGuid();
|
uint player = inventory.PlayerGuid();
|
||||||
ClientObject? playerObject = inventory.Objects.Get(player);
|
ClientObject? playerObject = inventory.Objects.Get(player);
|
||||||
int strength = (int)(character.Character.LocalPlayer
|
int strength = character.Character.LocalPlayer
|
||||||
.GetAttribute(LocalPlayerState.AttributeKind.Strength)?.Current ?? 0u);
|
.GetEffectiveAttribute(LocalPlayerState.AttributeKind.Strength) ?? 0;
|
||||||
int aug = playerObject?.Properties.GetInt(
|
int aug = playerObject?.Properties.GetInt(
|
||||||
(uint)PropertyInt.AugmentationIncreasedCarryingCapacity) ?? 0;
|
(uint)PropertyInt.AugmentationIncreasedCarryingCapacity) ?? 0;
|
||||||
int capacity = EncumbranceSystem.EncumbranceCapacity(strength, aug);
|
int capacity = EncumbranceSystem.EncumbranceCapacity(strength, aug);
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,25 @@ public sealed class IndicatorBarControllerTests
|
||||||
Assert.Equal(expectedState, button.ActiveRetailStateId);
|
Assert.Equal(expectedState, button.ActiveRetailStateId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EnchantmentChange_ReevaluatesBurdenFromEffectiveStrength()
|
||||||
|
{
|
||||||
|
var h = CreateHarness(strength: 10);
|
||||||
|
using IndicatorBarController controller = h.Controller;
|
||||||
|
h.Objects.UpdateIntProperty(Player, 5u, 1600);
|
||||||
|
UiButton button = h.Button(IndicatorBarController.BurdenButtonId);
|
||||||
|
Assert.Equal(IndicatorBarController.EncumberedState, button.ActiveRetailStateId);
|
||||||
|
|
||||||
|
h.Strength = 20;
|
||||||
|
h.Spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord(
|
||||||
|
42u, 1u, 60d, Player, Bucket: 1u));
|
||||||
|
Assert.Equal(IndicatorBarController.UnencumberedState, button.ActiveRetailStateId);
|
||||||
|
|
||||||
|
h.Strength = 10;
|
||||||
|
h.Spellbook.OnPurgeAll();
|
||||||
|
Assert.Equal(IndicatorBarController.EncumberedState, button.ActiveRetailStateId);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Burden_ClickOpensCharacterInformationPanel()
|
public void Burden_ClickOpensCharacterInformationPanel()
|
||||||
{
|
{
|
||||||
|
|
@ -205,7 +224,7 @@ public sealed class IndicatorBarControllerTests
|
||||||
spellbook,
|
spellbook,
|
||||||
objects,
|
objects,
|
||||||
() => Player,
|
() => Player,
|
||||||
() => strength,
|
() => harness.Strength,
|
||||||
() => harness.LinkStatus,
|
() => harness.LinkStatus,
|
||||||
() => harness.Time,
|
() => harness.Time,
|
||||||
harness.ToggledPanels.Add,
|
harness.ToggledPanels.Add,
|
||||||
|
|
@ -222,7 +241,7 @@ public sealed class IndicatorBarControllerTests
|
||||||
public ImportedLayout Layout { get; } = layout;
|
public ImportedLayout Layout { get; } = layout;
|
||||||
public Spellbook Spellbook { get; } = spellbook;
|
public Spellbook Spellbook { get; } = spellbook;
|
||||||
public ClientObjectTable Objects { get; } = objects;
|
public ClientObjectTable Objects { get; } = objects;
|
||||||
public int Strength { get; } = strength;
|
public int Strength { get; set; } = strength;
|
||||||
public List<uint> ToggledPanels { get; } = [];
|
public List<uint> ToggledPanels { get; } = [];
|
||||||
public double Time { get; set; }
|
public double Time { get; set; }
|
||||||
public LinkStatusSnapshot LinkStatus { get; set; } = new(true, 0d);
|
public LinkStatusSnapshot LinkStatus { get; set; } = new(true, 0d);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using AcDream.App.UI;
|
||||||
using AcDream.App.UI.Layout;
|
using AcDream.App.UI.Layout;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Selection;
|
using AcDream.Core.Selection;
|
||||||
|
using AcDream.Core.Spells;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.UI.Layout;
|
namespace AcDream.App.Tests.UI.Layout;
|
||||||
|
|
@ -63,10 +64,12 @@ public class InventoryControllerTests
|
||||||
Action? onClose = null,
|
Action? onClose = null,
|
||||||
SelectionState? selection = null,
|
SelectionState? selection = null,
|
||||||
StackSplitQuantityState? stackSplitQuantity = null,
|
StackSplitQuantityState? stackSplitQuantity = null,
|
||||||
ItemInteractionController? itemInteraction = null)
|
ItemInteractionController? itemInteraction = null,
|
||||||
|
Func<int?>? strengthProvider = null,
|
||||||
|
Spellbook? burdenSpellbook = null)
|
||||||
=> InventoryController.Bind(layout, objects, () => Player,
|
=> InventoryController.Bind(layout, objects, () => Player,
|
||||||
iconIds: (_, _, _, _, _) => 0u,
|
iconIds: (_, _, _, _, _) => 0u,
|
||||||
strength: () => strength, datFont: null,
|
strength: strengthProvider ?? (() => strength), datFont: null,
|
||||||
ownerName: ownerName is null ? null : () => ownerName,
|
ownerName: ownerName is null ? null : () => ownerName,
|
||||||
sendUse: uses is null ? null : g => uses.Add(g),
|
sendUse: uses is null ? null : g => uses.Add(g),
|
||||||
sendPutItemInContainer: puts is null ? null : (i, c, p) => puts.Add((i, c, p)),
|
sendPutItemInContainer: puts is null ? null : (i, c, p) => puts.Add((i, c, p)),
|
||||||
|
|
@ -78,7 +81,8 @@ public class InventoryControllerTests
|
||||||
onClose: onClose,
|
onClose: onClose,
|
||||||
selection: selection ?? new SelectionState(),
|
selection: selection ?? new SelectionState(),
|
||||||
stackSplitQuantity: stackSplitQuantity,
|
stackSplitQuantity: stackSplitQuantity,
|
||||||
itemInteraction: itemInteraction);
|
itemInteraction: itemInteraction,
|
||||||
|
burdenSpellbook: burdenSpellbook);
|
||||||
|
|
||||||
private static UiButton MakeButton(uint id)
|
private static UiButton MakeButton(uint id)
|
||||||
{
|
{
|
||||||
|
|
@ -238,6 +242,37 @@ public class InventoryControllerTests
|
||||||
Assert.Contains("50%", CaptionText(burdenText));
|
Assert.Contains("50%", CaptionText(burdenText));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EnchantmentChange_RefreshesBurdenFromEffectiveStrength()
|
||||||
|
{
|
||||||
|
var (layout, _, _, _, meter, burdenText, _, _) = BuildLayout();
|
||||||
|
var objects = new ClientObjectTable();
|
||||||
|
var props = new PropertyBundle();
|
||||||
|
props.Ints[5] = 1600;
|
||||||
|
objects.UpsertProperties(Player, props);
|
||||||
|
int effectiveStrength = 10;
|
||||||
|
var spellbook = new Spellbook();
|
||||||
|
using InventoryController controller = Bind(
|
||||||
|
layout,
|
||||||
|
objects,
|
||||||
|
strengthProvider: () => effectiveStrength,
|
||||||
|
burdenSpellbook: spellbook);
|
||||||
|
|
||||||
|
Assert.Equal(1600f / 1500f / 3f, meter.Fill() ?? -1f, 3);
|
||||||
|
Assert.Contains("106%", CaptionText(burdenText));
|
||||||
|
|
||||||
|
effectiveStrength = 20;
|
||||||
|
spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord(
|
||||||
|
42u, 1u, 60d, Player, Bucket: 1u));
|
||||||
|
Assert.Equal(1600f / 3000f / 3f, meter.Fill() ?? -1f, 3);
|
||||||
|
Assert.Contains("53%", CaptionText(burdenText));
|
||||||
|
|
||||||
|
effectiveStrength = 10;
|
||||||
|
spellbook.OnPurgeAll();
|
||||||
|
Assert.Equal(1600f / 1500f / 3f, meter.Fill() ?? -1f, 3);
|
||||||
|
Assert.Contains("106%", CaptionText(burdenText));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Captions_render_known_strings()
|
public void Captions_render_known_strings()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,74 @@ public sealed class LiveSessionEventRouterTests
|
||||||
router.Dispose();
|
router.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StrengthEnchantmentChange_RecomputesBurdenWithoutBaseAttributeUpdate()
|
||||||
|
{
|
||||||
|
using var session = NewSession();
|
||||||
|
const uint playerGuid = 0x50000001u;
|
||||||
|
var objects = new ClientObjectTable();
|
||||||
|
var character = new RuntimeCharacterState();
|
||||||
|
character.InstallSpellMetadata(SpellTable.LoadFromReader(new StringReader(
|
||||||
|
"Spell ID,Name,Flags [Hex]\n42,Strength Test,0x4\n")));
|
||||||
|
int movementStatsUpdated = 0;
|
||||||
|
|
||||||
|
var router = new LiveSessionEventRouter(
|
||||||
|
session,
|
||||||
|
NoOpEntitySink(),
|
||||||
|
NoOpEnvironmentSink(),
|
||||||
|
new LiveInventorySessionBindings(
|
||||||
|
objects,
|
||||||
|
PlayerGuid: () => playerGuid,
|
||||||
|
OnShortcuts: null,
|
||||||
|
OnUseDone: null,
|
||||||
|
ItemMana: new ItemManaState(),
|
||||||
|
ExternalContainers: new ExternalContainerState()),
|
||||||
|
new LiveCharacterSessionBindings(
|
||||||
|
new CombatState(),
|
||||||
|
character,
|
||||||
|
ResolveSkillFormulaBonus: null,
|
||||||
|
OnSkillsUpdated: null,
|
||||||
|
OnConfirmationRequest: null,
|
||||||
|
OnConfirmationDone: null,
|
||||||
|
ClientTime: () => 0d,
|
||||||
|
OnMovementStatsUpdated: () => movementStatsUpdated++),
|
||||||
|
NewSocialBindings());
|
||||||
|
|
||||||
|
router.Attach();
|
||||||
|
character.LocalPlayer.OnAttributeUpdate(
|
||||||
|
atType: 1u, ranks: 90u, start: 10u, xp: 0u);
|
||||||
|
var props = new PropertyBundle();
|
||||||
|
props.Ints[(uint)PropertyInt.EncumbranceVal] = 16500;
|
||||||
|
objects.UpsertProperties(playerGuid, props);
|
||||||
|
Assert.Equal(1.1f, character.MovementSkills.Burden, precision: 4);
|
||||||
|
|
||||||
|
int beforeBuff = movementStatsUpdated;
|
||||||
|
character.Spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord(
|
||||||
|
SpellId: 42u,
|
||||||
|
LayerId: 1u,
|
||||||
|
Duration: 60d,
|
||||||
|
CasterGuid: playerGuid,
|
||||||
|
StatModType: (uint)EnchantmentMath.EnchantmentTypeFlag.Attribute,
|
||||||
|
StatModKey: 1u,
|
||||||
|
StatModValue: 1.2f,
|
||||||
|
Bucket: 1u));
|
||||||
|
|
||||||
|
Assert.Equal(120, character.LocalPlayer.GetEffectiveAttribute(
|
||||||
|
LocalPlayerState.AttributeKind.Strength));
|
||||||
|
Assert.Equal(16500f / 18000f, character.MovementSkills.Burden, precision: 4);
|
||||||
|
Assert.Equal(beforeBuff + 1, movementStatsUpdated);
|
||||||
|
|
||||||
|
int beforePurge = movementStatsUpdated;
|
||||||
|
character.Spellbook.OnPurgeAll();
|
||||||
|
|
||||||
|
Assert.Equal(100, character.LocalPlayer.GetEffectiveAttribute(
|
||||||
|
LocalPlayerState.AttributeKind.Strength));
|
||||||
|
Assert.Equal(1.1f, character.MovementSkills.Burden, precision: 4);
|
||||||
|
Assert.Equal(beforePurge + 1, movementStatsUpdated);
|
||||||
|
|
||||||
|
router.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ObjectTablePropertyChange_RecomputesMovementSkillAugmentations()
|
public void ObjectTablePropertyChange_RecomputesMovementSkillAugmentations()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue