24 KiB
Campaign P — P1 stat-coupled movement: pseudocode + retail chain
Filed 2026-07-30 ahead of the P1 implementation (burden/stamina/vitae feeding
run rate, jump height, jump permission, jump stamina cost). All addresses are
from docs/research/named-retail/acclient_2013_pseudo_c.txt (Sept 2013 EoR
build) unless marked ACE-cross-reference. Ghidra MCP was unavailable for this
slice (operator note); ACE (references/ACE/Source/ACE.Server/Physics/) is
the tiebreaker wherever BN's x87 mush drops a branch, called out explicitly
below.
1. The call chain (top to bottom)
CMotionInterp (our MotionInterpreter.cs, unchanged this slice)
jump_is_allowed / ChargeJump / JumpChargeIsAllowed
-> WeenieObj.CanJump(extent) [IWeenieObject +0x3C]
-> WeenieObj.JumpStaminaCost(extent, out cost) [IWeenieObject +0x44]
GetJumpVZ -> WeenieObj.InqJumpVelocity(extent, out vz) [+0x30]
apply_run_to_command -> WeenieObj.InqRunRate(out rate) [+0x34]
ACCWeenieObject (thin delegation, pc 406512+)
CanJump/JumpStaminaCost/InqRunRate/InqJumpVelocity/InqMaxRunRate all
gate on IsThePlayer() first (0058c400/40/520/560/5a0) — NPCs/monsters/
remote players never reach m_pQualities for these queries. Confirms P1
is scoped correctly to PlayerWeenie only; RemoteWeenie is untouched.
CACQualities (the "qualities DB" == our PlayerWeenie, pc 412901-414050)
InqLoad 0x0058f130 (pc 409756) — burden/load ratio
CanJump 0x00591b50 — burden hard-gate
JumpStaminaCost 0x00591b90 — stamina cost + PK flag
InqRunRate 0x00592800 — full skill+vitae chain
InqJumpVelocity 0x00592980 — mirrors InqRunRate for Jump
MovementSystem (pure formulas, pc 695958+)
GetRunRate 0x006b0950
GetJumpHeight 0x006b09b0
JumpStaminaCost 0x006b0a40
EncumbranceSystem (pure formulas, pc 256393+)
EncumbranceCapacity 0x004fcc00
Load 0x004fcc40
LoadMod 0x004fcc70
2. InqLoad (0x0058f130, pc 409756) — FULLY READABLE
InqLoad(this, &loadOut):
strength = InqAttribute(this, ATTRIBUTE_STRENGTH=1) // default 0xa if absent
aug = InqInt(this, PROPERTY_INT_AUGMENTATION_INCREASED_CARRYING_CAPACITY=0xE6 /*230*/)
capacity = EncumbranceSystem::EncumbranceCapacity(strength, aug)
burden = InqInt(this, PROPERTY_INT_ENCUMBRANCE_VAL=5) // default 0 if absent
*loadOut = EncumbranceSystem::Load(capacity, burden)
return 1 // always succeeds for CACQualities (has vtable)
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
than re-deriving — one source of truth, no drift between the burden HUD and
movement physics.
3. CanJump (0x00591b50, pc 412907) — X87 MUSH, POLARITY RESOLVED BY PLAUSIBILITY
CanJump(this, extent):
load = 0
if (InqLoad(this, &load) != 0):
p = <fcompp load, 2.0f; fnstsw; test ah,0x05> // "load < 2.0" per BN's own
// asserted C0 subexpression
if (!p) return 1
return 0
Literal BN reading: if (!p) return 1 = "if load is NOT < 2.0 (i.e. >= 2.0),
return CAN-jump; otherwise CANNOT". That is backwards from every other
retail-movement fact we have (LoadMod's own floor sits at 2.0; the campaign's
connected-matrix acceptance is "≥200% barely moves/jumps", not "can only jump
when overloaded"). This is the documented BN "bitfield mush" artifact class
(feedback_bn_decomp_field_names.md) — the flag-synthesis is unreliable for
the FOLLOWING test ah,mask interpretation even when the preceding
subexpression is trustworthy.
Resolution (register row UN-8, see §6): CanJump returns load < 2.0
(can jump under 200% burden; refused at/above it) — the polarity a normal
AC player's lived experience requires, and the one that makes CanJump's own
threshold coincide with LoadMod's floor. ACE gives no tiebreaker
(WeenieObject.CanJump is an unconditional return true stub — never
ported burden gating at all). Ghidra MCP was down for this slice; flagged
for a future confirmation pass, not blocking this port.
4. JumpStaminaCost (0x00591b90, pc 412949) — FULLY READABLE
CACQualities::JumpStaminaCost(this, extent, &costOut):
load = 0
if (InqLoad(this, &load) == 0) return 0
pk = 0
pkStatus = InqInt(this, PROPERTY_INT_PLAYER_KILLER_STATUS=0x86, default=8)
if (pkStatus == 4 || pkStatus == 0x40): // PK / PKLite
pkTimestamp = InqFloat(this, PROPERTY_FLOAT_LAST_PK_ATTACK_TIMESTAMP=0x91)
if (pkTimestamp is present && !(pkTimestamp + 20.0 < Timer::cur_time)):
pk = 1 // PK timer active (<20s since last PK act)
*costOut = MovementSystem::JumpStaminaCost(extent, load, pk)
return 1 // ALWAYS true once InqLoad succeeds — no affordability
// check lives in this function.
Key finding: retail's CanQualities::JumpStaminaCost NEVER returns false
(except when InqLoad itself fails, which doesn't happen for a real player).
jump_is_allowed's if (!WeenieObj.JumpStaminaCost(...)) return 0x47 branch
(the "refusal" path our own MotionInterpreter.cs already ports verbatim,
W0-pins.md A2) is real retail machinery, but CACQualities never actually
exercises the refusing side of it. "Refused jump" does not happen via this
mechanism in retail — only "weak jump" (see §5). P1 ports
JumpStaminaCost to always return true with the REAL computed cost
(retiring the TS-5 zero-cost stub), matching this decomp exactly.
The pk flag is PlayerKillerStatus/LastPkAttackTimestamp — TS-23's
exact scope (P3, not P1). P1 hardcodes pk: false at the one new call site
(PlayerWeenie.JumpStaminaCost) and documents the dependency against TS-23
rather than re-implementing PK parsing here.
5. InqRunRate (0x00592800, pc 413824) / InqJumpVelocity (0x00592980, pc 413902) — FULLY READABLE
Both functions share one shape (Run uses skill id 0x18=24, Jump uses 0x16=22):
InqRunRate(this, &rateOut):
load = 1.0
if (InqLoad(this, &load) == 0) return 0
currentStamina = 0
if (AttributeCache::InqAttribute2nd(attribCache, ATTR2ND_STAMINA=4, ¤tStamina) == 0)
return 0
EnchantAttribute2nd(this, 4, ¤tStamina) // vital-buff adjusts the LOCAL COPY only
// (not the wire "current stamina" state)
skill = InqSkillBaseLevel(this, SKILL_RUN=0x18) // formula-bonus + init + ranks
skill += max(PropertyInt 0x16D, 0) // LumAugAllSkills
skill += matching category augmentation ? 10 : 0 // 0x12C melee / 0x12D missile /
// 0x12E magic; exact skill-id switch
EnchantSkill(this, 0x18, &skill) // vitae * skill-enchantments, floor@0.5, truncate
if (PropertyInt 0x146 > 0) skill += 5 // Jack of All Trades
if (skill is specialized) skill += 2 * max(PropertyInt 0x158, 0)
if (currentStamina == 0) skill = 0 // THE stamina-gates-movement mechanism
*rateOut = MovementSystem::GetRunRate(load, skill, 1.0)
return 1
InqJumpVelocity is identical but for skill id 0x16=22, and finishes with
sqrt(MovementSystem::GetJumpHeight(load, skill, extent, 1.0) * 19.6) (pc
413975, matching GetJumpVZ's existing sqrt call already in
MotionInterpreter.cs/PlayerWeenie.cs — unchanged).
Answering the plan's question — "which skill level does retail feed?"
Neither raw base nor a separately-cached value: retail re-derives, on every
query, EnchantSkill(baseSkill) where EnchantSkill (CEnchantmentRegistry:: EnchantSkill 0x005947b0, pc 416240, FULLY READABLE) is:
EnchantSkill(registry, skillId, &valueInOut):
value = *valueInOut // base skill (formulaBonus+init+ranks)
if (registry._vitae != null):
value = Enchant(registry._vitae, value) // vitae multiplier FIRST
matching = CullEnchantmentsFromList(mult_list, category=SKILL=0x10, skillId)
++ CullEnchantmentsFromList(add_list, category=SKILL=0x10, skillId)
for each e in matching: value = Enchant(e, value) // per-record mult OR add
if (value < 0.5) value = 0 // floor
*valueInOut = (int)value // truncate (ftol2)
return ...
CEnchantmentRegistry::EnchantAttribute2nd (0x00594670, pc 416169, the
vitals path our EnchantmentMath.GetMod already ports for
LocalPlayerState.GetMaxApprox) applies _vitae in the identical
position (first, before the mult/add lists) — confirming our existing vitae
representation (ActiveEnchantmentRecord.Bucket == 4, a StatModType Vitae
flag 0x00800000 classified in GameEventWiring.ClassifyLiveEnchantmentBucket)
is the right vehicle: P1 reuses it unmodified, adding a sibling
EnchantmentMath.GetSkillMod (filtered by StatModType & Skill(0x10) != 0
instead of the vitals' implicit attribute2nd filter) rather than inventing a
new vitae channel. This satisfies "vitae/enchant-adjusted effective run/jump
skill... reading vitae + relevant skill enchantments from the M3
active-effect state" without a general effective-skill engine — the only new
code is the type-flag filter and the skill-id key.
2026-07-31 #268 closeout: the previously bounded augmentation terms are
now ported in shared PlayerSkillMath, after a complete read of
CACQualities::InqSkill @ 0x00592660. The exact order matters:
- intrinsic formula/init/ranks;
- positive property 0x16D plus the exact category +10 switch;
EnchantSkill;- property 0x146 contributes +5 when positive;
- specialized skills receive
2 × max(property 0x158, 0).
The character panel and Runtime movement both consume this one Core calculation. AP-127 is retired. The apparent current-stamina-copy residual does not create an independently reachable effect for ordinary stat enchantments: current and maximum stamina use distinct secondary-attribute keys, and a max-stamina enchantment cannot turn zero current stamina nonzero.
6. GetRunRate / GetJumpHeight / JumpStaminaCost formula bodies (MovementSystem, pc 695958+)
GetRunRate (0x006b0950) and the arg3!=0 (PK) branch of JumpStaminaCost
(0x006b0a40) have their GENERAL-CASE arithmetic entirely dropped by BN (only
the EncumbranceSystem::LoadMod/800-skill-compare calls and the arg3==0
ceil expression survive uncollapsed — the same information-loss class as the
x87 mush, just total rather than partial). ACE is the cross-reference
tiebreaker for those two spots (references/ACE/Source/ACE.Server/Physics/ Animation/MovementSystem.cs), matching this exact acdream port's ORIGINAL
citation style (PlayerWeenie.cs's pre-P1 doc comments already said
"decompiled + ACE MovementSystem" for these two formulas — nothing new here,
just now with a named-decomp address alongside):
GetRunRate(load, skill, scaling) = skill==800 ? 18/4 : ((LoadMod(load) * (skill/(skill+200)*11) + 4) / scaling) / 4— §12c correction (#266, 2026-07-30): the 800 branch is EXACT EQUALITY, not>=. Raw byte decode of 0x006b0950 (PDB-paired binary):fild skill; fcom [0x00803b94 = 800f]; fnstsw ax; test ah, 0x44; jp 0x6b097f— the C2/C3 parity idiom in whichjp(general path) fires for<,>, AND unordered; thefld [18f]; fdiv [4f]; retfall-through executes only when C3=1/C2=0, i.e. skill == 800 exactly. The general path decodes instruction-by-instruction to(LoadMod(load) * (skill/(skill+200)*11) + 4) / scaling / 4(constants 200f @0x00803b8c, 11f @0x00803b88, 4f @0x007c6174, /scaling from[esp+0xc], final /4f @0x00803b80). ACE's>= 800"max run speed?" reading is a misread of the same mush and must not be used as a tiebreaker here — it flat-lined every maxed character at 4.5 (retail general formula gives ~3.70) and erased the vitae speed differential (#266: 33%-vitae +Acdream visibly outran 5%-vitae +Je in acdream while retail runs them within ~0.4%).InqMaxRunRate's skill=9999 probe gets the general formula (~3.6961), not 4.5. The true retail signature carries a 3rdscalingarg (confirmed by the decomp's own function signature), and every known call site (InqMaxRunRate,InqRunRate) passes1f.GetJumpHeight(load, skill, extent, scaling)— BN's extent-clamp micro-branch (pc 006b09b0-006b09ca) is the SAME x87-mush pattern as §3; ACE'sMath.Clamp(extent, 0, 1)is the tiebreaker (matches the EXISTING acdream code, which already does this — unchanged).= LoadMod(load) * (skill/(skill+1300)*22.2 + 0.05) * clampedExtent / scaling, floored at 0.35 — matches acdream's pre-existing formula exactly.JumpStaminaCost(power, load, pk):pk==0:ceil((load + 0.5) * power * 8 + 2)— the campaign plan's own shorthand ("ceil((power+0.5)load8+2)") has the+0.5term on the wrong operand; the verbatim decomp (fully readable, no mush) is(load + 0.5) * power, confirmed against ACE's identical(burden + 0.5f) * power.pk!=0: BN drops the body entirely (bare_ftol2()tailcall, no operands survive); ACE's(int)((power + 1.0f) * 100.0f)is the tiebreaker. Unused by P1 (pkis hardcodedfalse— see §4), ported anyway for signature completeness/citation.
EncumbranceSystem::{EncumbranceCapacity, Load, LoadMod}(0x004fcc00/40/70, pc 256393+) — already verbatim-ported asAcDream.Core.Items.BurdenMath; P1'sEncumbranceSystemdelegates (see §2).
7. What "weak jump" actually is (no hard refusal exists)
Given §4 (JumpStaminaCost never refuses) and §5 (stamina==0 zeroes the
EFFECTIVE skill, not the extent), the retail zero-stamina jump is:
GetJumpHeight(load, skill=0, extent, 1) = LoadMod(load) * 0.05 * extent,
floored to the 0.35 m minimum by the function's own clamp — i.e. every
jump attempt, however exhausted, still produces at least the 0.35 m floor
hop. There is no code path in CACQualities that makes jump_is_allowed
return GeneralMovementFailure due to low stamina. The campaign plan's
"weak/refused jump" acceptance phrasing is satisfied by "weak" (the floor
hop); "refused" does not occur via burden/stamina in this chain and P1 does
not invent it.
8. ReportExhaustion — wiring the dead R3-W4 seam
ReportExhaustion() (MotionInterpreter.cs:1619, already a full verbatim
port of CMotionInterp::ReportExhaustion 0x005288d0) has ZERO callers
anywhere in the codebase today. Retail's caller chain is
CPhysicsObj::report_exhaustion (0x0050fdd0) →
MovementManager::ReportExhaustion (0x00524360), both outside
CMotionInterp's scope and not yet located precisely in the decomp
(out of P1's bounded scope to hunt down the exact upstream trigger site).
What we DO know precisely: its effect is "re-apply current movement through
the SAME dual-dispatch predicate as apply_current_movement" — i.e. force a
fresh WeenieObj.InqRunRate/InqJumpVelocity query against the CURRENT
physics/interpreted state, with no new input event.
That is exactly the primitive needed to make a live burden/stamina/vitae
change visible immediately (mid-run, mid-charge) instead of waiting for the
next keypress. P1 wires ReportExhaustion() as the "re-evaluate movement
now" trampoline any time Runtime pushes a fresh burden, stamina, or
vitae-adjusted-skill value into the active PlayerMovementController —
plausible given ReportExhaustion's documented purpose, and the least
speculative real consumer available for a seam that otherwise never fires.
9. Design: where each input is computed and pushed
Runtime (AcDream.Runtime, presentation-free):
RuntimeCharacterState
- Spellbook (existing) -- vitae + skill enchantments live here
- MovementSkills: RuntimeMovementSkillState (existing, EXTENDED)
RunSkill / JumpSkill -- now the ADJUSTED (EnchantSkill'd) values
Burden (float, new) -- InqLoad's load ratio
CurrentStamina (int, new, -1 sentinel = unknown/don't-gate)
- _runSkillBase / _jumpSkillBase (new, private) -- pre-EnchantSkill values
- UpdateMovementSkillBase(runBase, jumpBase) -- stores base, recomputes+pushes adjusted
- RecomputeMovementSkills() -- base * EnchantmentMath.GetSkillMod(skillId), floor/round
- wired: Spellbook.EnchantmentsChanged -> RecomputeMovementSkills (vitae/buff changes
recompute WITHOUT a fresh PD skill push)
LiveSessionEventRouter.Attach() (cross-owner wiring hub; already the home
of the existing onSkillsUpdated -> MovementSkills.Update plumbing)
- 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)
- all three trigger points additionally invoke the new
LiveCharacterSessionBindings.OnMovementStatsUpdated callback
RuntimeMovementSkillProjection.ApplyTo(skills, controller) (existing seam,
called at construction AND reactively from OnSkillsUpdated/OnMovementStatsUpdated)
- SetCharacterSkills(run, jump) (existing)
- NEW: SetCharacterBurden(burden), SetCharacterStamina(stamina)
App (LiveSessionRuntimeFactory) / Headless (HeadlessSessionHost):
- OnMovementStatsUpdated: App wires ApplyTo(...) + controller.Motion.ReportExhaustion()
(mirrors the existing OnSkillsUpdated body, which P1 ALSO extends with
the ReportExhaustion() call for consistency); Headless passes null,
matching its existing OnSkillsUpdated: null (headless bots don't need
live mid-session re-apply to a controller that may not exist yet).
Core (AcDream.Core.Physics, presentation-free, pure):
EncumbranceSystem -- EncumbranceCapacity/Load/LoadMod, delegates to BurdenMath
MovementSystem -- GetRunRate/GetJumpHeight/JumpStaminaCost/GetJumpPower
PlayerWeenie (CACQualities-shaped)
_burden (float), _currentStamina (int?, null=unknown) -- pushed via
SetBurden/SetStamina (SetBurden already existed, wires the dead setter)
_runSkill/_jumpSkill (int) -- pushed via SetSkills, ALREADY vitae/enchant-
adjusted by Runtime before it arrives here (PlayerWeenie itself stays
a pure formula consumer -- no Spellbook/enchantment dependency, keeping
it trivially testable)
CanJump(extent) -> _burden < 2.0 (UN-8 polarity, §3)
JumpStaminaCost(extent, out cost)
-> cost = MovementSystem.JumpStaminaCost(extent, _burden, pk:false);
return true; (§4 -- always true, TS-23 owns pk)
InqRunRate(out rate) -> effSkill = _currentStamina == 0 ? 0 : _runSkill;
rate = MovementSystem.GetRunRate(_burden, effSkill, 1f);
InqJumpVelocity(extent, out vz)
-> effSkill = _currentStamina == 0 ? 0 : _jumpSkill;
vz = sqrt(MovementSystem.GetJumpHeight(_burden, effSkill, extent, 1f) * 19.6f);
_currentStamina == null (never set — matches every existing test /
call site that doesn't call SetStamina) never zeroes the skill, preserving
every pre-P1 PlayerWeenieTests.cs expectation unchanged.
10. Register bookkeeping (same commit as the port)
- Delete TS-5 (
CanJumpalways true /JumpStaminaCostzero-cost stub) — retired: both now real, decomp-cited. - Delete AP-25 (run/jump skill = attributeBonus+init+ranks only, no
vitae) — retired: vitae now flows through
EnchantmentMath.GetSkillMod. - TS-21 untouched — still valid (pre-PD fallback defaults 200/300 are a separate divergence, not addressed by P1).
- TS-23 extended (not a new row) — its "PlayerKillerStatus not parsed"
scope now also covers the new
MovementSystem.JumpStaminaCostpkparameter, hardcodedfalseat thePlayerWeeniecall site pending P3. - AP-127 retired 2026-07-31 (#268) — the complete 0x16D/category/ 0x146/0x158 chain is shared by panel and movement (§5 closeout).
- New UN-8 —
CACQualities::CanJump's x87 comparison polarity resolved by domain plausibility rather than a literal BN read (§3); Ghidra MCP confirmation is the retire path.
11. Test plan
MovementSystemTests(new, Core): golden tables forGetRunRate(0/200/ 800 skill, load knees),GetJumpHeight(extent 0/0.5/1, 0.35 floor, load knees),JumpStaminaCost(ceil rounding, load/power sweep),GetJumpPower(inverse sanity, not consumed by P1 but ported for signature completeness / future charge-meter work).EncumbranceSystemTests(new, Core): capacity at 100%/200% aug clamp, Load ratio, LoadMod knees — cross-checked 1:1 against the EXISTINGBurdenMathtests (same formulas, must agree bit-for-bit).PlayerWeenieTests(extend): CanJump refusal at load>=2.0 / allowed below; JumpStaminaCost real nonzero cost; InqRunRate/InqJumpVelocity zero at stamina==0 (skill forced to 0, still floors at 0.35 m for jump); ALL pre-existing tests must stay green unmodified (no SetStamina call -> null sentinel -> no gating, exactly today's behavior).EnchantmentMathTests(extend):GetSkillModtype-flag filtering (Skill-flagged records match; vital-only records with a colliding numeric key do NOT), vitae-first-then-mult-then-add ordering.RuntimeCharacterStateTests/RuntimeMovementSkillStateTests(Runtime): burden/stamina push + revision bump;RecomputeMovementSkillsfires onSpellbook.EnchantmentsChangedwithout a fresh base push; ResetSession convergence includes Burden==0/CurrentStamina==-1.LiveSessionEventRouterTests(Runtime, if a harness exists) or a focused new test: ObjectTable burden-trigger events recompute and push burden; Stamina vital change pushes CurrentStamina.
12. P1 Opus-review addenda (2026-07-30, post-implementation)
12a. UN-8 RETIRED — CanJump polarity byte-proven
Raw bytes of CACQualities::CanJump @ 0x00591b50 in the PDB-paired
v11.4186 binary (technique: reference_pe_byte_decode):
e8 ca d5 ff ff call InqLoad (0x0058f130)
85 c0 / 74 1a test eax,eax; jz return0 ; load unknowable -> 0
d9 44 24 00 fld dword [esp] ; st0 = load
d8 1d 24 5e 7c 00 fcomp dword [0x007c5e24] ; vs 2.0f (verified read)
df e0 fnstsw ax
f6 c4 05 test ah, 0x05 ; C0|C2
7a 09 jp return0 ; PF=1 on {neither, both}
b8 01 00 00 00 mov eax, 1 ; fall-through: C0 only
test ah,5 result parity: 0x00 (load ≥ 2.0, incl. ==) → PF=1 → 0;
0x01 (load < 2.0) → PF=0 → 1; 0x05 (unordered) → PF=1 → 0.
CanJump = (load < 2.0f); NaN/unordered refuses. The shipped
_burden < CanJumpLoadThreshold matches exactly, including the NaN edge.
12b. PK-timer jump-cost semantics (for Slice P3 / TS-23)
CACQualities::JumpStaminaCost @ 0x00591b90 (pc 412934-412968), fully
readable: the pk flag passed to MovementSystem::JumpStaminaCost is
pk = InqInt(0x86 /*134 PlayerKillerStatus*/, default 8) in {4 /*PK*/, 0x40 /*PKLite*/}
&& InqFloat(0x91 /*145*/) succeeded
&& (that_float + 20.0) >= Timer::cur_time
i.e. PK/PKLite status AND a 20-second recency window on PropertyFloat
0x91. The P3 implementer should plumb exactly this pair alongside the
mover-flag work; MovementSystem.JumpStaminaCost's pk branch
((int)((power + 1) * 100), ACE-derived — the retail branch is an
elided _ftol2 tailcall) is already in place.