namespace AcDream.Core.Physics;
///
/// IWeenieObject implementation for the local player — the C# analogue of
/// retail's CACQualities "qualities DB" composition (named-retail
/// decomp pc 412901-414050; ACCWeenieObject's own CanJump/JumpStaminaCost/
/// InqRunRate/InqJumpVelocity/InqMaxRunRate are thin delegations to exactly
/// this object, gated on IsThePlayer() — pc 406512+, confirming this
/// query family only ever reaches the LOCAL player's weenie).
///
///
/// Campaign P Slice P1 (2026-07-30): burden, current stamina, and the
/// vitae/enchantment-adjusted run/jump skill are now real, pushed inputs —
/// see docs/research/2026-07-30-stat-coupled-movement-pseudocode.md.
/// _runSkill/_jumpSkill arrive ALREADY EnchantSkill-adjusted
/// (vitae + skill enchantments folded in by
/// AcDream.Runtime.Gameplay.RuntimeCharacterState.RecomputeMovementSkills)
/// — this class stays a pure formula consumer with no Spellbook/enchantment
/// dependency, matching retail's OWN split between CACQualities (the
/// query surface) and CEnchantmentRegistry (the buff aggregator it
/// calls into internally).
///
///
///
/// Formulas from decompiled acclient.exe ( /
/// ), cross-referenced against ACE
/// MovementSystem.GetRunRate/GetJumpHeight.
///
///
public sealed class PlayerWeenie : IWeenieObject
{
///
/// Retail CACQualities::CanJump's hard burden gate (0x00591b50,
/// pc 412907) — polarity PROVEN by raw byte decode of the PDB-paired
/// binary (P1 Opus review, 2026-07-30): fld load; fcomp [0x007c5e24
/// = 2.0f]; fnstsw; test ah,5; jp → return 0, i.e. return 1 iff
/// load < 2.0 (≥ 2.0 or unordered/NaN refuses). Former register row
/// UN-8 retired on this evidence; see the byte decode in
/// docs/research/2026-07-30-stat-coupled-movement-pseudocode.md §12.
///
public const float CanJumpLoadThreshold = 2.0f;
private int _runSkill;
private int _jumpSkill;
private float _burden;
///
/// TS-23 §12b (Campaign P Slice P3, 2026-07-30): raw
/// PropertyInt.PlayerKillerStatus (0x86). null = never
/// pushed (matches every pre-P3 caller — no PK-timer bump, today's
/// behavior unchanged).
///
private int? _playerKillerStatus;
///
/// TS-23 §12b: raw PropertyFloat.LastPkAttackTimestamp (0x91).
/// null = never pushed / property absent.
///
private float? _lastPkAttackTimestamp;
///
/// Retail AttributeCache::InqAttribute2nd(ATTR2ND_STAMINA=4)'s
/// current-stamina reading, consulted by InqRunRate/
/// InqJumpVelocity to zero the effective skill when exhausted
/// (pc 413824-413898, 413902-413979). null = never pushed
/// (matches every pre-P1 caller/test — no gating, today's behavior
/// unchanged); any non-negative value including 0 is a real reading.
///
private uint? _currentStamina;
public PlayerWeenie(int runSkill = 0, int jumpSkill = 0, float burden = 0f)
{
_runSkill = runSkill;
_jumpSkill = jumpSkill;
_burden = burden;
}
public void SetSkills(int runSkill, int jumpSkill)
{
_runSkill = runSkill;
_jumpSkill = jumpSkill;
}
///
/// Pushes the retail InqLoad-equivalent burden/capacity ratio
/// (0.0 unencumbered .. ~3.0 severely overloaded). Runtime computes this
/// from Strength + augmentation property 0xE6 + EncumbranceVal property
/// 5 (the same inputs IndicatorBarController.UpdateBurden already
/// assembles) and pushes it — see the pseudocode doc §9.
///
public void SetBurden(float burden) => _burden = burden;
///
/// Pushes the current-stamina reading feeding the zero-skill gate in
/// /. Pass
/// null to return to "unknown, don't gate" (matches construction
/// default).
///
public void SetStamina(uint? currentStamina) => _currentStamina = currentStamina;
///
/// TS-23 §12b (Campaign P Slice P3, 2026-07-30): pushes the raw
/// PropertyInt.PlayerKillerStatus (0x86) / PropertyFloat.
/// LastPkAttackTimestamp (0x91) pair CACQualities::
/// JumpStaminaCost (0x00591b90) reads for its 20-second PK-timer
/// jump-stamina-cost bump. Both null restores "never pushed" (no
/// bump, matching every pre-P3 caller). The 20-second recency window
/// itself is evaluated fresh at call time
/// against a live clock, not cached here — the window's EXPIRY has no
/// wire event to re-push on.
///
public void SetPlayerKillerStatus(int? playerKillerStatus, float? lastPkAttackTimestamp)
{
_playerKillerStatus = playerKillerStatus;
_lastPkAttackTimestamp = lastPkAttackTimestamp;
}
public bool InqRunRate(out float rate)
{
int effectiveSkill = _currentStamina == 0 ? 0 : _runSkill;
rate = MovementSystem.GetRunRate(_burden, effectiveSkill);
return true;
}
public bool InqJumpVelocity(float extent, out float vz)
{
int effectiveSkill = _currentStamina == 0 ? 0 : _jumpSkill;
float height = MovementSystem.GetJumpHeight(_burden, effectiveSkill, extent);
vz = MathF.Sqrt(height * 19.6f);
return true;
}
///
/// Retail CACQualities::CanJump (0x00591b50): refuses only past
/// (200% load) — see the class doc's
/// UN-8 note. TS-5 retired: this was previously an unconditional
/// true.
///
public bool CanJump(float extent) => _burden < CanJumpLoadThreshold;
///
/// R3-W3 (W0-pins.md A3): the local player's weenie is THE player.
/// Feeds W4's apply_current_movement/ReportExhaustion
/// dual-dispatch gate.
///
public bool IsThePlayer() => true;
///
/// Retail CACQualities::JumpStaminaCost (0x00591b90, pc 412949,
/// FULLY READABLE): computes the real cost via
/// and returns true
/// unconditionally (once burden is knowable, which it always is for the
/// local player) — retail's own function never exercises the "can't
/// afford" false path; see the pseudocode doc §4/§7. TS-5 retired: this
/// was previously a zero-cost stub.
///
///
/// TS-23 §12b (2026-07-30): pk is retail's own formula —
/// PlayerKillerStatus (0x86) in {4 (PK), 0x40 (PKLite)} AND
/// (LastPkAttackTimestamp + 20.0) >= Timer::cur_time. Both
/// defaulting to 8 (retail's own
/// InqInt default) and an absent timestamp evaluate to
/// pk = false — bit-identical to the pre-P3 hardcoded value for
/// every non-PK/PKLite character, which is every ACE default-created
/// character (the invariant this port must not break).
///
/// "Now" uses (process-uptime
/// milliseconds), NOT an absolute wall-clock epoch. A first attempt
/// used 's Unix-epoch seconds and a
/// conformance test caught the bug it produces: LastPkAttackTimestamp
/// is a wire PropertyFloat (32-bit, ~7 significant decimal
/// digits) — at a ~1.7-billion-second Unix epoch magnitude, float
/// precision only resolves to roughly ±128 seconds, so a 20-second
/// recency window is entirely swallowed by rounding error (a `now − 30s`
/// timestamp computed the SAME cost as `now`). Retail's own
/// Timer::cur_time almost certainly is NOT an absolute epoch for
/// exactly this reason — a small process/session-relative counter is
/// the only magnitude a 32-bit float can hold with sub-second precision
/// for a meaningful session length. This is a documented, evidence-based
/// clock CHOICE (a precision bug the test caught, not a guess dressed up
/// as fact) — the exact retail epoch/basis was not independently
/// confirmed (ACE does not model either property server-side, so this
/// branch is inert against every local-ACE test scenario regardless of
/// the clock's exact basis; flagged for cdb confirmation if a real PK
/// server is ever tested against).
///
///
public bool JumpStaminaCost(float extent, out int cost)
{
bool pk = false;
int pkStatus = _playerKillerStatus ?? 8; // retail InqInt default when the property is absent
if ((pkStatus == 4 || pkStatus == 0x40) && _lastPkAttackTimestamp is { } ts)
{
float now = Environment.TickCount64 / 1000f;
pk = (ts + 20.0f) >= now;
}
cost = MovementSystem.JumpStaminaCost(extent, _burden, pk);
return true;
}
///
/// RunRate = (burdenMod * (runSkill / (runSkill + 200)) * 11 + 4) / 4.
/// Returns 4.5 only for the retail skill == 800 sentinel (byte-decoded
/// exact-equality branch, #266 — NOT a cap). Thin forwarder to
/// — kept for source
/// compatibility with existing golden-value tests.
///
public static float GetRunRate(float burden, int runSkill) =>
MovementSystem.GetRunRate(burden, runSkill);
///
/// JumpHeight = burdenMod * (jumpSkill / (jumpSkill + 1300) * 22.2 + 0.05) * extent,
/// clamped to minimum 0.35m. Thin forwarder to
/// — kept for source
/// compatibility with existing golden-value tests.
///
public static float GetJumpHeight(float burden, int jumpSkill, float extent) =>
MovementSystem.GetJumpHeight(burden, jumpSkill, extent);
///
/// Encumbrance modifier: 1.0 when unloaded, linearly decreasing to 0 at
/// 200%. Thin forwarder to —
/// kept for source compatibility with existing golden-value tests.
///
public static float GetBurdenMod(float burden) => EncumbranceSystem.LoadMod(burden);
}