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
|
||||
|
||||
**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:
|
||||
`CTransition::validate_transition` now performs retail's non-OK-only
|
||||
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
|
||||
accepted repeated uphill runs while pressing into the stair sides. Evidence:
|
||||
`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,
|
||||
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)
|
||||
```
|
||||
|
||||
This EXACTLY matches acdream's existing `IndicatorBarController.UpdateBurden()`
|
||||
/ `InventoryController.RefreshBurden()` pattern (Strength attribute + prop
|
||||
0xE6 aug + prop 5 EncumbranceVal, falling back to `SumCarriedBurden` when the
|
||||
wire value is absent) — already ported, already correct, already tested via
|
||||
the UI. **`AcDream.Core.Items.BurdenMath`
|
||||
The property/capacity shape matches acdream's
|
||||
`IndicatorBarController.UpdateBurden()` /
|
||||
`InventoryController.RefreshBurden()` pattern (Strength attribute + prop 0xE6
|
||||
aug + prop 5 EncumbranceVal, falling back to `SumCarriedBurden` when the wire
|
||||
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
|
||||
the SAME addresses.** P1's `EncumbranceSystem` (Physics-namespaced, for
|
||||
citation clarity next to `MovementSystem`) delegates to `BurdenMath` rather
|
||||
|
|
@ -317,8 +322,10 @@ Runtime (AcDream.Runtime, presentation-free):
|
|||
- onSkillsUpdated callback -> character.Character.UpdateMovementSkillBase(...)
|
||||
- NEW: inventory.Objects.{ObjectAdded,ObjectUpdated,ObjectRemoved,ObjectMoved,
|
||||
ContainerContentsReplaced,Cleared} + LocalPlayer.AttributeChanged(Strength)
|
||||
+ Spellbook.EnchantmentsChanged
|
||||
-> recompute burden (Strength + prop 0xE6 aug + prop 5 EncumbranceVal,
|
||||
SAME shape as IndicatorBarController.UpdateBurden/InventoryController.RefreshBurden)
|
||||
using effective/enchantment-adjusted Strength
|
||||
-> character.Character.MovementSkills.UpdateBurden(ratio)
|
||||
- NEW: character.Character.LocalPlayer.Changed(VitalKind.Stamina)
|
||||
-> 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.
|
||||
See
|
||||
`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.Inventory.Objects,
|
||||
() => d.PlayerIdentity.ServerGuid,
|
||||
() => d.Character.LocalPlayer.GetAttribute(
|
||||
LocalPlayerState.AttributeKind.Strength)
|
||||
is { } strength ? (int?)strength.Current : null,
|
||||
() => d.Character.LocalPlayer.GetEffectiveAttribute(
|
||||
LocalPlayerState.AttributeKind.Strength),
|
||||
() => late.Session.LinkStatus,
|
||||
d.ClientTime,
|
||||
() => late.Session.CurrentSession?.RequestLinkStatusPing(),
|
||||
|
|
@ -656,9 +655,9 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
() => d.PlayerIdentity.ServerGuid,
|
||||
iconComposer.GetIcon,
|
||||
iconComposer.GetDragIcon,
|
||||
() => d.Character.LocalPlayer.GetAttribute(
|
||||
LocalPlayerState.AttributeKind.Strength)
|
||||
is { } strength ? (int?)strength.Current : null,
|
||||
() => d.Character.LocalPlayer.GetEffectiveAttribute(
|
||||
LocalPlayerState.AttributeKind.Strength),
|
||||
d.Character.Spellbook,
|
||||
guid => late.Session.CurrentSession?.SendUse(guid),
|
||||
(item, container, placement) =>
|
||||
late.Session.CurrentSession?.SendPutItemInContainer(
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ public sealed class IndicatorBarController : IRetainedPanelController
|
|||
_miniGame.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.MiniGame);
|
||||
_endCharacterSession.OnClick = bindings.RequestEndCharacterSession;
|
||||
|
||||
bindings.Spellbook.EnchantmentsChanged += UpdateEnchantments;
|
||||
bindings.Spellbook.EnchantmentsChanged += OnEnchantmentsChanged;
|
||||
bindings.Objects.ObjectAdded += OnObjectChanged;
|
||||
bindings.Objects.ObjectUpdated += OnObjectChanged;
|
||||
bindings.Objects.ObjectRemoved += OnObjectChanged;
|
||||
|
|
@ -229,6 +229,11 @@ public sealed class IndicatorBarController : IRetainedPanelController
|
|||
private void OnObjectMoved(ClientObjectMove _) => UpdateBurden();
|
||||
private void OnContainerContentsReplaced(uint _) => UpdateBurden();
|
||||
private void OnObjectsCleared() => UpdateBurden();
|
||||
private void OnEnchantmentsChanged()
|
||||
{
|
||||
UpdateEnchantments();
|
||||
UpdateBurden();
|
||||
}
|
||||
|
||||
private void UpdateBurden()
|
||||
{
|
||||
|
|
@ -253,7 +258,7 @@ public sealed class IndicatorBarController : IRetainedPanelController
|
|||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_bindings.Spellbook.EnchantmentsChanged -= UpdateEnchantments;
|
||||
_bindings.Spellbook.EnchantmentsChanged -= OnEnchantmentsChanged;
|
||||
_bindings.Objects.ObjectAdded -= OnObjectChanged;
|
||||
_bindings.Objects.ObjectUpdated -= OnObjectChanged;
|
||||
_bindings.Objects.ObjectRemoved -= OnObjectChanged;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Numerics;
|
|||
using AcDream.App.UI;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.Spells;
|
||||
|
||||
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>? _dragIconIds;
|
||||
private readonly Func<int?> _strength;
|
||||
private readonly Spellbook? _burdenSpellbook;
|
||||
private readonly Func<string>? _ownerName;
|
||||
|
||||
private readonly UiItemList? _contentsGrid;
|
||||
|
|
@ -96,13 +98,15 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
Action<uint, uint>? notifyMergeAttempt,
|
||||
ItemInteractionController? itemInteraction,
|
||||
Action? onClose,
|
||||
StackSplitQuantityState? stackSplitQuantity)
|
||||
StackSplitQuantityState? stackSplitQuantity,
|
||||
Spellbook? burdenSpellbook)
|
||||
{
|
||||
_objects = objects;
|
||||
_playerGuid = playerGuid;
|
||||
_iconIds = iconIds;
|
||||
_dragIconIds = dragIconIds;
|
||||
_strength = strength;
|
||||
_burdenSpellbook = burdenSpellbook;
|
||||
_ownerName = ownerName;
|
||||
_sendUse = sendUse;
|
||||
_sendPutItemInContainer = sendPutItemInContainer;
|
||||
|
|
@ -195,6 +199,8 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
_objects.ObjectUpdated += OnObjectChanged;
|
||||
_objects.Cleared += OnObjectsCleared;
|
||||
_selection.Changed += OnSelectionChanged;
|
||||
if (_burdenSpellbook is not null)
|
||||
_burdenSpellbook.EnchantmentsChanged += RefreshBurden;
|
||||
if (_itemInteraction is not null)
|
||||
{
|
||||
_itemInteraction.StateChanged += OnInteractionStateChanged;
|
||||
|
|
@ -244,14 +250,15 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
ItemInteractionController? itemInteraction = null,
|
||||
Action? onClose = 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,
|
||||
ownerName, datFont,
|
||||
contentsEmptySprite, sideBagEmptySprite, mainPackEmptySprite,
|
||||
sendUse, sendPutItemInContainer,
|
||||
sendStackableSplitToContainer, sendStackableMerge,
|
||||
notifyMergeAttempt, itemInteraction,
|
||||
onClose, stackSplitQuantity);
|
||||
onClose, stackSplitQuantity, burdenSpellbook);
|
||||
|
||||
private void OnObjectChanged(ClientObject o)
|
||||
{
|
||||
|
|
@ -944,6 +951,8 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
_objects.ObjectUpdated -= OnObjectChanged;
|
||||
_objects.Cleared -= OnObjectsCleared;
|
||||
_selection.Changed -= OnSelectionChanged;
|
||||
if (_burdenSpellbook is not null)
|
||||
_burdenSpellbook.EnchantmentsChanged -= RefreshBurden;
|
||||
if (_contentsGrid is not 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> ResolveDragIcon,
|
||||
Func<int?> Strength,
|
||||
Spellbook Spellbook,
|
||||
Action<uint>? SendUse,
|
||||
Action<uint, uint, int>? SendPutItemInContainer,
|
||||
Action<uint, uint, uint, uint>? SendStackableSplitToContainer,
|
||||
|
|
@ -1816,7 +1817,8 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
notifyMergeAttempt, b.ItemInteraction,
|
||||
() => CloseWindow(WindowNames.Inventory),
|
||||
StackSplitQuantity,
|
||||
b.ResolveDragIcon);
|
||||
b.ResolveDragIcon,
|
||||
b.Spellbook);
|
||||
InventoryPanelController = inventory;
|
||||
PaperdollController paperdoll = PaperdollController.Bind(
|
||||
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)
|
||||
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
|
||||
// stamina==0 effective-skill-zeroing gate (pseudocode doc §5).
|
||||
|
|
@ -370,8 +374,8 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
|
|||
{
|
||||
uint player = inventory.PlayerGuid();
|
||||
ClientObject? playerObject = inventory.Objects.Get(player);
|
||||
int strength = (int)(character.Character.LocalPlayer
|
||||
.GetAttribute(LocalPlayerState.AttributeKind.Strength)?.Current ?? 0u);
|
||||
int strength = character.Character.LocalPlayer
|
||||
.GetEffectiveAttribute(LocalPlayerState.AttributeKind.Strength) ?? 0;
|
||||
int aug = playerObject?.Properties.GetInt(
|
||||
(uint)PropertyInt.AugmentationIncreasedCarryingCapacity) ?? 0;
|
||||
int capacity = EncumbranceSystem.EncumbranceCapacity(strength, aug);
|
||||
|
|
|
|||
|
|
@ -89,6 +89,25 @@ public sealed class IndicatorBarControllerTests
|
|||
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]
|
||||
public void Burden_ClickOpensCharacterInformationPanel()
|
||||
{
|
||||
|
|
@ -205,7 +224,7 @@ public sealed class IndicatorBarControllerTests
|
|||
spellbook,
|
||||
objects,
|
||||
() => Player,
|
||||
() => strength,
|
||||
() => harness.Strength,
|
||||
() => harness.LinkStatus,
|
||||
() => harness.Time,
|
||||
harness.ToggledPanels.Add,
|
||||
|
|
@ -222,7 +241,7 @@ public sealed class IndicatorBarControllerTests
|
|||
public ImportedLayout Layout { get; } = layout;
|
||||
public Spellbook Spellbook { get; } = spellbook;
|
||||
public ClientObjectTable Objects { get; } = objects;
|
||||
public int Strength { get; } = strength;
|
||||
public int Strength { get; set; } = strength;
|
||||
public List<uint> ToggledPanels { get; } = [];
|
||||
public double Time { get; set; }
|
||||
public LinkStatusSnapshot LinkStatus { get; set; } = new(true, 0d);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using AcDream.App.UI;
|
|||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.Spells;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
|
@ -63,10 +64,12 @@ public class InventoryControllerTests
|
|||
Action? onClose = null,
|
||||
SelectionState? selection = null,
|
||||
StackSplitQuantityState? stackSplitQuantity = null,
|
||||
ItemInteractionController? itemInteraction = null)
|
||||
ItemInteractionController? itemInteraction = null,
|
||||
Func<int?>? strengthProvider = null,
|
||||
Spellbook? burdenSpellbook = null)
|
||||
=> InventoryController.Bind(layout, objects, () => Player,
|
||||
iconIds: (_, _, _, _, _) => 0u,
|
||||
strength: () => strength, datFont: null,
|
||||
strength: strengthProvider ?? (() => strength), datFont: null,
|
||||
ownerName: ownerName is null ? null : () => ownerName,
|
||||
sendUse: uses is null ? null : g => uses.Add(g),
|
||||
sendPutItemInContainer: puts is null ? null : (i, c, p) => puts.Add((i, c, p)),
|
||||
|
|
@ -78,7 +81,8 @@ public class InventoryControllerTests
|
|||
onClose: onClose,
|
||||
selection: selection ?? new SelectionState(),
|
||||
stackSplitQuantity: stackSplitQuantity,
|
||||
itemInteraction: itemInteraction);
|
||||
itemInteraction: itemInteraction,
|
||||
burdenSpellbook: burdenSpellbook);
|
||||
|
||||
private static UiButton MakeButton(uint id)
|
||||
{
|
||||
|
|
@ -238,6 +242,37 @@ public class InventoryControllerTests
|
|||
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]
|
||||
public void Captions_render_known_strings()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -233,6 +233,74 @@ public sealed class LiveSessionEventRouterTests
|
|||
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]
|
||||
public void ObjectTablePropertyChange_RecomputesMovementSkillAugmentations()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue