feat(physics): Campaign P P1 - stat-coupled movement (burden/stamina/vitae)
Ports the retail CACQualities/EncumbranceSystem/MovementSystem chain (named-retail decomp pc 256393/412901-414050/416169-416320/695958+) so PlayerWeenie's run rate, jump height, jump permission, and jump stamina cost are real functions of burden, current stamina, and vitae/skill enchantments instead of stubs. Core: - New EncumbranceSystem.cs (delegates to the already-verified BurdenMath formulas — one source of truth for the burden HUD and movement physics) and MovementSystem.cs (GetRunRate/GetJumpHeight/ JumpStaminaCost/GetJumpPower, decomp-cited; ACE cross-referenced where BN dropped the general-case arithmetic entirely). - PlayerWeenie rewritten as the CACQualities-shaped composition: CanJump gates on burden (<2.0 load, UN-8 — x87 polarity resolved by plausibility, Ghidra MCP unavailable this slice), JumpStaminaCost returns the real ceil((load+0.5)*power*8+2) cost and always affords it (matches decomp — retail's own function never refuses; "weak" jump comes entirely from the stamina==0 skill-zeroing gate inside InqRunRate/InqJumpVelocity, not a hard refusal), SetStamina wires a null="unknown, don't gate" sentinel preserving every pre-P1 test. - EnchantmentMath.GetMod gained an optional StatModType flag filter (GetSkillMod convenience wrapper) so the SAME vitae/family-stacking machinery already used for vital-max buffs now also answers "what's the vitae+skill-enchantment-adjusted Run/Jump skill" — reusing the M3 active-enchantment state, not a new engine. Runtime: - RuntimeCharacterState now stores the pre-EnchantSkill base run/jump skill and recomputes the adjusted value (vitae first, then matching Skill-flagged buffs, floor 0.5, truncate) on every base push AND on every Spellbook.EnchantmentsChanged notification — a vitae change alone moves the produced rate without a fresh PlayerDescription. - RuntimeMovementSkillState extended with Burden/CurrentStamina (RuntimeMovementSkillProjection.ApplyTo pushes both through the existing seam); LiveSessionEventRouter recomputes burden from the same Strength+aug-property+EncumbranceVal inputs the burden HUD already assembles (reacting to the same ClientObjectTable events) and pushes current stamina from LocalPlayerState vital updates. - Wires the previously dead-lettered ReportExhaustion() R3-W4 seam: LiveSessionRuntimeFactory's OnMovementStatsUpdated callback re- applies the current snapshot to the live controller and forces an immediate movement re-evaluation on any skill/burden/stamina change. Register: retires TS-5 (CanJump/JumpStaminaCost stubs) and AP-25 (no vitae in pushed skill). Adds AP-127 (two minor unmodeled retail bonus properties + the stamina-buff-adjusts-local-copy nuance, deliberately out of the bounded "run/jump query path only" scope) and UN-8 (the CanJump x87 polarity call, flagged for a future Ghidra MCP confirmation pass). Extends TS-23 (PlayerKillerStatus not parsed) to cover JumpStaminaCost's new pk parameter, hardcoded false pending P3. Full pseudocode + retail citations + the vitae/skill-level finding in docs/research/2026-07-30-stat-coupled-movement-pseudocode.md. Release suite: Core.Tests 3977/2 skips, Runtime.Tests 425/0 skips, App.Tests 3968/3 skips — all green. (One pre-existing, unrelated Debug- only flake in LandblockBuildOriginTests reproduces on the pre-P1 baseline and passes in Release; not touched here.) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3a4782048e
commit
9355ddcec6
20 changed files with 1714 additions and 74 deletions
|
|
@ -891,6 +891,30 @@ public sealed class PlayerMovementController
|
|||
_weenie.SetSkills(runSkill, jumpSkill);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign P Slice P1 (2026-07-30): pushes the retail
|
||||
/// <c>InqLoad</c>-equivalent burden ratio computed by Runtime (Strength
|
||||
/// + augmentation property 0xE6 + EncumbranceVal property 5) into the
|
||||
/// player's <see cref="PlayerWeenie"/> — wires the previously-dead
|
||||
/// <c>PlayerWeenie.SetBurden</c> setter (TS-5 retired).
|
||||
/// </summary>
|
||||
public void SetCharacterBurden(float burden)
|
||||
{
|
||||
_weenie.SetBurden(burden);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign P Slice P1 (2026-07-30): pushes the current-stamina vital
|
||||
/// reading. A negative value restores PlayerWeenie's "unknown, don't
|
||||
/// gate" sentinel; any non-negative value including 0 gates
|
||||
/// <c>InqRunRate</c>/<c>InqJumpVelocity</c>'s effective-skill-zeroing per
|
||||
/// retail's <c>CACQualities::InqRunRate</c>/<c>InqJumpVelocity</c>.
|
||||
/// </summary>
|
||||
public void SetCharacterStamina(int currentStamina)
|
||||
{
|
||||
_weenie.SetStamina(currentStamina < 0 ? null : (uint)currentStamina);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3-W2 (r3-port-plan.md §4): the player's <see cref="MotionInterpreter"/>
|
||||
/// — GameWindow binds the player sequencer's MotionDone seam to it so the
|
||||
|
|
|
|||
|
|
@ -43,11 +43,28 @@ public readonly record struct RuntimeCharacterOwnershipSnapshot(
|
|||
/// </summary>
|
||||
public sealed class RuntimeCharacterState : IDisposable
|
||||
{
|
||||
/// <summary>ACE Skill enum ordinal for Run (K-fix7 / pseudocode doc §5).</summary>
|
||||
public const uint RunSkillId = 24u;
|
||||
/// <summary>ACE Skill enum ordinal for Jump (K-fix7 / pseudocode doc §5).</summary>
|
||||
public const uint JumpSkillId = 22u;
|
||||
|
||||
private bool _disposed;
|
||||
private long _characterRevision;
|
||||
private long _spellbookRevision;
|
||||
private bool _internalSubscriptionsAttached;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign P Slice P1 (2026-07-30): the pre-<c>EnchantSkill</c> base
|
||||
/// run/jump skill (formulaBonus+init+ranks, as parsed from
|
||||
/// PlayerDescription) — kept so <see cref="RecomputeMovementSkills"/>
|
||||
/// can re-derive the vitae/enchantment-adjusted value purely from a
|
||||
/// spellbook change, without waiting for a fresh skill push. -1 =
|
||||
/// unknown (mirrors <see cref="RuntimeMovementSkillState"/>'s own
|
||||
/// sentinel convention).
|
||||
/// </summary>
|
||||
private int _runSkillBase = -1;
|
||||
private int _jumpSkillBase = -1;
|
||||
|
||||
public RuntimeCharacterState(SpellTable? spellTable = null)
|
||||
{
|
||||
Spellbook = new Spellbook(spellTable);
|
||||
|
|
@ -56,6 +73,7 @@ public sealed class RuntimeCharacterState : IDisposable
|
|||
MovementSkills = new RuntimeMovementSkillState();
|
||||
View = new CharacterView(this);
|
||||
Spellbook.StateChanged += OnSpellbookChanged;
|
||||
Spellbook.EnchantmentsChanged += OnEnchantmentsChangedForMovement;
|
||||
LocalPlayer.Changed += OnVitalChanged;
|
||||
LocalPlayer.AttributeChanged += OnAttributeChanged;
|
||||
LocalPlayer.CharacterChanged += OnCharacterChanged;
|
||||
|
|
@ -117,9 +135,66 @@ public sealed class RuntimeCharacterState : IDisposable
|
|||
&& options.Options2
|
||||
== RuntimeCharacterOptionsState.DefaultOptions2,
|
||||
MovementSkills.RunSkill == -1
|
||||
&& MovementSkills.JumpSkill == -1);
|
||||
&& MovementSkills.JumpSkill == -1
|
||||
&& MovementSkills.Burden == 0f
|
||||
&& MovementSkills.CurrentStamina == -1
|
||||
&& _runSkillBase == -1
|
||||
&& _jumpSkillBase == -1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign P Slice P1 (2026-07-30): stores the pre-<c>EnchantSkill</c>
|
||||
/// base run/jump skill (PlayerDescription's formulaBonus+init+ranks)
|
||||
/// and pushes the vitae/enchantment-adjusted result into
|
||||
/// <see cref="MovementSkills"/> — the SAME call shape
|
||||
/// <c>LiveSessionEventRouter</c>'s pre-P1 <c>onSkillsUpdated</c> callback
|
||||
/// already used (<c>MovementSkills.Update(runSkill, jumpSkill)</c>), now
|
||||
/// routed through the retail <c>CEnchantmentRegistry::EnchantSkill</c>
|
||||
/// chain. A value < 0 leaves that half's base untouched (matches
|
||||
/// <see cref="RuntimeMovementSkillState.Update"/>'s own "don't touch"
|
||||
/// convention for a missing half).
|
||||
/// </summary>
|
||||
public void UpdateMovementSkillBase(int runSkillBase, int jumpSkillBase)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (runSkillBase >= 0) _runSkillBase = runSkillBase;
|
||||
if (jumpSkillBase >= 0) _jumpSkillBase = jumpSkillBase;
|
||||
RecomputeMovementSkills();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-derives the adjusted run/jump skill from the stored base plus the
|
||||
/// CURRENT spellbook state (vitae + skill enchantments) — matching
|
||||
/// retail's <c>CEnchantmentRegistry::EnchantSkill</c> (0x005947b0):
|
||||
/// vitae multiplies first, then matching Skill-flagged mult/add
|
||||
/// enchantments, floored to 0 below 0.5, truncated to int. Fires on
|
||||
/// every base push AND on every <see cref="Spellbook.EnchantmentsChanged"/>
|
||||
/// notification (a vitae/buff change alone must move the produced rate
|
||||
/// without a fresh PlayerDescription).
|
||||
/// </summary>
|
||||
private void RecomputeMovementSkills()
|
||||
{
|
||||
int run = _runSkillBase >= 0
|
||||
? ApplySkillEnchantments(_runSkillBase, RunSkillId)
|
||||
: -1;
|
||||
int jump = _jumpSkillBase >= 0
|
||||
? ApplySkillEnchantments(_jumpSkillBase, JumpSkillId)
|
||||
: -1;
|
||||
MovementSkills.Update(run, jump);
|
||||
}
|
||||
|
||||
private int ApplySkillEnchantments(int baseSkill, uint skillId)
|
||||
{
|
||||
EnchantmentMath.VitalMod mod = Spellbook.GetSkillMod(skillId);
|
||||
float adjusted = baseSkill * mod.Multiplier + mod.Additive;
|
||||
// CEnchantmentRegistry::EnchantSkill pc 416240: floor to 0 below
|
||||
// 0.5, then truncate (retail _ftol2, a C-style cast).
|
||||
if (adjusted < 0.5f) adjusted = 0f;
|
||||
return (int)adjusted;
|
||||
}
|
||||
|
||||
private void OnEnchantmentsChangedForMovement() => RecomputeMovementSkills();
|
||||
|
||||
/// <summary>
|
||||
/// Installs immutable DAT metadata without transferring its ownership to
|
||||
/// Runtime. The content host may install one table after portal.dat opens.
|
||||
|
|
@ -241,6 +316,8 @@ public sealed class RuntimeCharacterState : IDisposable
|
|||
Try(Spellbook.Clear, ref failures);
|
||||
Try(LocalPlayer.Clear, ref failures);
|
||||
Try(Options.ResetSession, ref failures);
|
||||
_runSkillBase = -1;
|
||||
_jumpSkillBase = -1;
|
||||
Try(MovementSkills.ResetSession, ref failures);
|
||||
if (failures is not null)
|
||||
{
|
||||
|
|
@ -263,11 +340,14 @@ public sealed class RuntimeCharacterState : IDisposable
|
|||
Try(Spellbook.Clear, ref failures);
|
||||
Try(LocalPlayer.Clear, ref failures);
|
||||
Try(Options.ResetSession, ref failures);
|
||||
_runSkillBase = -1;
|
||||
_jumpSkillBase = -1;
|
||||
Try(MovementSkills.ResetSession, ref failures);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Spellbook.StateChanged -= OnSpellbookChanged;
|
||||
Spellbook.EnchantmentsChanged -= OnEnchantmentsChangedForMovement;
|
||||
LocalPlayer.Changed -= OnVitalChanged;
|
||||
LocalPlayer.AttributeChanged -= OnAttributeChanged;
|
||||
LocalPlayer.CharacterChanged -= OnCharacterChanged;
|
||||
|
|
@ -467,28 +547,42 @@ public sealed class RuntimeCharacterOptionsState
|
|||
public readonly record struct RuntimeMovementSkillSnapshot(
|
||||
int RunSkill,
|
||||
int JumpSkill,
|
||||
long Revision)
|
||||
long Revision,
|
||||
// Campaign P Slice P1 (2026-07-30): retail InqLoad-equivalent burden
|
||||
// ratio (0.0 unencumbered) and current stamina (-1 = unknown/don't-gate,
|
||||
// matching RunSkill/JumpSkill's own sentinel convention).
|
||||
float Burden = 0f,
|
||||
int CurrentStamina = -1)
|
||||
{
|
||||
public bool IsComplete => RunSkill >= 0 && JumpSkill >= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-authoritative run/jump values retained independently of any
|
||||
/// graphical movement controller. App applies this borrowed state whenever
|
||||
/// its presentation/physics controller exists or is rebuilt.
|
||||
/// Server-authoritative run/jump/burden/stamina values retained
|
||||
/// independently of any graphical movement controller. App applies this
|
||||
/// borrowed state whenever its presentation/physics controller exists or is
|
||||
/// rebuilt. Campaign P Slice P1 (2026-07-30) extended this beyond run/jump
|
||||
/// skill to the full stat-coupled-movement input set (burden, current
|
||||
/// stamina) — see the pseudocode doc §9. RunSkill/JumpSkill arrive here
|
||||
/// ALREADY vitae/enchantment-adjusted by
|
||||
/// <see cref="RuntimeCharacterState.RecomputeMovementSkills"/>.
|
||||
/// </summary>
|
||||
public sealed class RuntimeMovementSkillState
|
||||
{
|
||||
private int _runSkill = -1;
|
||||
private int _jumpSkill = -1;
|
||||
private float _burden;
|
||||
private int _currentStamina = -1;
|
||||
private long _revision;
|
||||
|
||||
public int RunSkill => Volatile.Read(ref _runSkill);
|
||||
public int JumpSkill => Volatile.Read(ref _jumpSkill);
|
||||
public float Burden => Volatile.Read(ref _burden);
|
||||
public int CurrentStamina => Volatile.Read(ref _currentStamina);
|
||||
public bool IsComplete => _runSkill >= 0 && _jumpSkill >= 0;
|
||||
public long Revision => Interlocked.Read(ref _revision);
|
||||
public RuntimeMovementSkillSnapshot Snapshot =>
|
||||
new(_runSkill, _jumpSkill, Revision);
|
||||
new(_runSkill, _jumpSkill, Revision, _burden, _currentStamina);
|
||||
|
||||
public void Update(int runSkill, int jumpSkill)
|
||||
{
|
||||
|
|
@ -507,10 +601,37 @@ public sealed class RuntimeMovementSkillState
|
|||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes a fresh retail <c>InqLoad</c>-equivalent burden ratio (Strength
|
||||
/// + augmentation property 0xE6 + EncumbranceVal property 5 — the same
|
||||
/// inputs the burden HUD already assembles). Any value, including 0,
|
||||
/// is a real reading.
|
||||
/// </summary>
|
||||
public void UpdateBurden(float burden)
|
||||
{
|
||||
if (Burden == burden) return;
|
||||
Volatile.Write(ref _burden, burden);
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the current-stamina vital reading. Any non-negative value,
|
||||
/// including 0 (exhausted — zeroes the effective run/jump skill), is
|
||||
/// real; pass a negative value only to restore the "unknown" sentinel.
|
||||
/// </summary>
|
||||
public void UpdateStamina(int currentStamina)
|
||||
{
|
||||
if (CurrentStamina == currentStamina) return;
|
||||
Volatile.Write(ref _currentStamina, currentStamina);
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
|
||||
public void ResetSession()
|
||||
{
|
||||
Volatile.Write(ref _runSkill, -1);
|
||||
Volatile.Write(ref _jumpSkill, -1);
|
||||
Volatile.Write(ref _burden, 0f);
|
||||
Volatile.Write(ref _currentStamina, -1);
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ public static class RuntimeMovementSkillProjection
|
|||
controller.SetCharacterSkills(
|
||||
snapshot.RunSkill,
|
||||
snapshot.JumpSkill);
|
||||
// Campaign P Slice P1 (2026-07-30): burden/stamina ride the SAME
|
||||
// seam run/jump skill already used — see the pseudocode doc §9.
|
||||
controller.SetCharacterBurden(snapshot.Burden);
|
||||
controller.SetCharacterStamina(snapshot.CurrentStamina);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ using AcDream.Core.Combat;
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Player;
|
||||
using AcDream.Core.Properties;
|
||||
using AcDream.Core.Social;
|
||||
using AcDream.Core.Spells;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
|
@ -44,7 +46,14 @@ public sealed record LiveCharacterSessionBindings(
|
|||
Action<int, int>? OnSkillsUpdated,
|
||||
Action<GameEvents.CharacterConfirmationRequest>? OnConfirmationRequest,
|
||||
Action<GameEvents.CharacterConfirmationDone>? OnConfirmationDone,
|
||||
Func<double>? ClientTime);
|
||||
Func<double>? ClientTime,
|
||||
// Campaign P Slice P1 (2026-07-30): fires after MovementSkills' burden,
|
||||
// stamina, OR (vitae/enchantment-adjusted) skill values change mid-
|
||||
// session — the reactive re-apply-to-the-live-controller seam, mirroring
|
||||
// OnSkillsUpdated's existing shape. Optional/nullable so every existing
|
||||
// caller (including Headless's OnSkillsUpdated: null pattern) compiles
|
||||
// unchanged.
|
||||
Action? OnMovementStatsUpdated = null);
|
||||
|
||||
public sealed record LiveSocialSessionBindings(
|
||||
ChatLog Chat,
|
||||
|
|
@ -153,10 +162,15 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
|
|||
social.TurbineChat,
|
||||
onSkillsUpdated: (runSkill, jumpSkill) =>
|
||||
{
|
||||
character.Character.MovementSkills.Update(
|
||||
// Campaign P Slice P1 (2026-07-30): route the PD/skill
|
||||
// base through the vitae/enchantment-adjusted recompute
|
||||
// (CEnchantmentRegistry::EnchantSkill) instead of writing
|
||||
// MovementSkills directly — see the pseudocode doc §9.
|
||||
character.Character.UpdateMovementSkillBase(
|
||||
runSkill,
|
||||
jumpSkill);
|
||||
character.OnSkillsUpdated?.Invoke(runSkill, jumpSkill);
|
||||
character.OnMovementStatsUpdated?.Invoke();
|
||||
},
|
||||
resolveSkillFormulaBonus: character.ResolveSkillFormulaBonus,
|
||||
onShortcuts: inventory.OnShortcuts,
|
||||
|
|
@ -174,6 +188,52 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
|
|||
externalContainers: inventory.ExternalContainers,
|
||||
accepting: IsAccepting));
|
||||
ConstructionCheckpoint();
|
||||
|
||||
// Campaign P Slice P1 (2026-07-30): burden recompute triggers —
|
||||
// the SAME event set IndicatorBarController.UpdateBurden already
|
||||
// reacts to (Strength + augmentation property 0xE6 +
|
||||
// EncumbranceVal property 5, falling back to SumCarriedBurden).
|
||||
// See the pseudocode doc §9.
|
||||
SubscribeToRecompute<ClientObject>(
|
||||
h => inventory.Objects.ObjectAdded += h,
|
||||
h => inventory.Objects.ObjectAdded -= h,
|
||||
() => RecomputeBurden(inventory, character));
|
||||
SubscribeToRecompute<ClientObject>(
|
||||
h => inventory.Objects.ObjectUpdated += h,
|
||||
h => inventory.Objects.ObjectUpdated -= h,
|
||||
() => RecomputeBurden(inventory, character));
|
||||
SubscribeToRecompute<ClientObject>(
|
||||
h => inventory.Objects.ObjectRemoved += h,
|
||||
h => inventory.Objects.ObjectRemoved -= h,
|
||||
() => RecomputeBurden(inventory, character));
|
||||
SubscribeToRecompute<ClientObjectMove>(
|
||||
h => inventory.Objects.ObjectMoved += h,
|
||||
h => inventory.Objects.ObjectMoved -= h,
|
||||
() => RecomputeBurden(inventory, character));
|
||||
SubscribeToRecompute<uint>(
|
||||
h => inventory.Objects.ContainerContentsReplaced += h,
|
||||
h => inventory.Objects.ContainerContentsReplaced -= h,
|
||||
() => RecomputeBurden(inventory, character));
|
||||
SubscribeParameterless(
|
||||
h => inventory.Objects.Cleared += h,
|
||||
h => inventory.Objects.Cleared -= h,
|
||||
() => RecomputeBurden(inventory, character));
|
||||
Subscribe<LocalPlayerState.AttributeKind>(
|
||||
h => character.Character.LocalPlayer.AttributeChanged += h,
|
||||
h => character.Character.LocalPlayer.AttributeChanged -= h,
|
||||
kind =>
|
||||
{
|
||||
if (kind == LocalPlayerState.AttributeKind.Strength)
|
||||
RecomputeBurden(inventory, character);
|
||||
});
|
||||
|
||||
// Current-stamina push — CACQualities::InqRunRate/InqJumpVelocity's
|
||||
// stamina==0 effective-skill-zeroing gate (pseudocode doc §5).
|
||||
Subscribe<LocalPlayerState.VitalKind>(
|
||||
h => character.Character.LocalPlayer.Changed += h,
|
||||
h => character.Character.LocalPlayer.Changed -= h,
|
||||
kind => RecomputeStamina(kind, character));
|
||||
|
||||
_subscriptions.Add(new CombatChatTranslator(
|
||||
character.Combat,
|
||||
social.Chat,
|
||||
|
|
@ -259,6 +319,89 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
|
|||
ConstructionCheckpoint();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign P Slice P1 (2026-07-30): a payload-typed event whose ONLY
|
||||
/// job is "something relevant changed, recompute" — thin wrapper over
|
||||
/// <see cref="Subscribe{T}"/> that discards the payload.
|
||||
/// </summary>
|
||||
private void SubscribeToRecompute<T>(
|
||||
Action<Action<T>> attach,
|
||||
Action<Action<T>> detach,
|
||||
Action recompute) =>
|
||||
Subscribe(attach, detach, (T _) => recompute());
|
||||
|
||||
/// <summary>
|
||||
/// Campaign P Slice P1 (2026-07-30): the parameterless-event analogue of
|
||||
/// <see cref="Subscribe{T}"/> (<c>ClientObjectTable.Cleared</c> carries
|
||||
/// no payload).
|
||||
/// </summary>
|
||||
private void SubscribeParameterless(
|
||||
Action<Action> attach,
|
||||
Action<Action> detach,
|
||||
Action sink)
|
||||
{
|
||||
Action handler = () =>
|
||||
{
|
||||
if (Volatile.Read(ref _accepting) != 0)
|
||||
sink();
|
||||
};
|
||||
|
||||
attach(handler);
|
||||
_subscriptions.Add(() => detach(handler));
|
||||
|
||||
ConstructionCheckpoint();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign P Slice P1 (2026-07-30): retail <c>CACQualities::InqLoad</c>
|
||||
/// equivalent (Strength + augmentation property 0xE6 + EncumbranceVal
|
||||
/// property 5, falling back to the summed carried burden) — the SAME
|
||||
/// input assembly <c>IndicatorBarController.UpdateBurden</c> /
|
||||
/// <c>InventoryController.RefreshBurden</c> already use for the burden
|
||||
/// HUD. See the pseudocode doc §2/§9.
|
||||
/// </summary>
|
||||
private static void RecomputeBurden(
|
||||
LiveInventorySessionBindings inventory,
|
||||
LiveCharacterSessionBindings character)
|
||||
{
|
||||
uint player = inventory.PlayerGuid();
|
||||
ClientObject? playerObject = inventory.Objects.Get(player);
|
||||
int strength = (int)(character.Character.LocalPlayer
|
||||
.GetAttribute(LocalPlayerState.AttributeKind.Strength)?.Current ?? 0u);
|
||||
int aug = playerObject?.Properties.GetInt(
|
||||
(uint)PropertyInt.AugmentationIncreasedCarryingCapacity) ?? 0;
|
||||
int capacity = EncumbranceSystem.EncumbranceCapacity(strength, aug);
|
||||
int burden = playerObject is not null
|
||||
&& playerObject.Properties.Ints.TryGetValue(
|
||||
(uint)PropertyInt.EncumbranceVal, out int wireBurden)
|
||||
? wireBurden
|
||||
: inventory.Objects.SumCarriedBurden(player);
|
||||
float load = EncumbranceSystem.Load(capacity, burden);
|
||||
character.Character.MovementSkills.UpdateBurden(load);
|
||||
character.OnMovementStatsUpdated?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign P Slice P1 (2026-07-30): pushes current-stamina vital
|
||||
/// changes into <see cref="RuntimeMovementSkillState"/> — feeds
|
||||
/// <c>CACQualities::InqRunRate</c>/<c>InqJumpVelocity</c>'s stamina==0
|
||||
/// effective-skill-zeroing gate (pseudocode doc §5).
|
||||
/// </summary>
|
||||
private static void RecomputeStamina(
|
||||
LocalPlayerState.VitalKind kind,
|
||||
LiveCharacterSessionBindings character)
|
||||
{
|
||||
if (kind != LocalPlayerState.VitalKind.Stamina) return;
|
||||
if (character.Character.LocalPlayer.Get(LocalPlayerState.VitalKind.Stamina)
|
||||
is not LocalPlayerState.VitalSnapshot stamina)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
character.Character.MovementSkills.UpdateStamina((int)stamina.Current);
|
||||
character.OnMovementStatsUpdated?.Invoke();
|
||||
}
|
||||
|
||||
private void ConstructionCheckpoint() =>
|
||||
_constructionCheckpoint?.Invoke(++_constructionStep);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue