Companion to physics_engine.md
Important
All paths cross-verified against raw IDA decompilation on 2026-02-16. DoJump path ordering was corrected (originally had ladder/foothold swapped). HandleGroundedMovement and ClampToBounds were added (previously missing).
UpdatePhysics(this, timeMs):
if this[86]: // jumpRequested
CUserLocal_DoJump(this)
if this[88]: // ladderActionRequested
CMovePath_LadderAction(this)
// ═══ GROUNDED PATH: this[68] != 0 (has foothold) ═══
if this[68]:
savedT = copy(this+56, 48bytes) // 12 DWORDs: T-pos (encrypted) + T-vel (encrypted)
CMovePath_GroundedVelocityUpdate(timeMs)
// vtable[8] = HandleGroundedMovement (0x5C03EE)
if vtable[8](this, savedT, &timeMs) AND timeMs > 0:
dt = timeMs * 0.001
if this[68] == 0: // fell off foothold during move → now airborne
savedFull = copy(this+8, 96bytes)
// Trapezoidal position integration (using OLD velocity from savedFull)
posX = Decrypt(this+8) + Decrypt(savedFull+12) * dt
posY = Decrypt(this+14) + Decrypt(savedFull+18) * dt
// vtable[12] = AirbornePhysicsUpdate (0x5BD96C)
vtable[12](this, savedFull, &timeMs, 0)
else: // still grounded
savedT = copy(this+56, 48bytes)
T = Decrypt(this+56) + Decrypt(savedT+6) * dt
Store T
vtable[8](this, savedT, &timeMs) // continue grounded
// ═══ AIRBORNE PATH: this[68] == 0 ═══
else:
savedFull = copy(this+8, 96bytes)
CMovePath_ApplyAirborneVelocity(timeMs)
// vtable[12] = AirbornePhysicsUpdate with mode=1 (first attempt)
if vtable[12](this, savedFull, &timeMs, 1) AND timeMs > 0:
dt = timeMs * 0.001
if this[68] == 0: // still airborne (no landing)
vtable[12](this, savedFull, &timeMs, 0) // retry with mode=0
else: // landed on foothold
savedT = copy(this+56, 48bytes)
T = Decrypt(this+56) + Decrypt(savedT+6) * dt
Store T
vtable[8](this, savedT, &timeMs)
vtable[20](this, timeMs) // timer/animation update
CRITICAL: Path ordering verified against IDA. First check = currentLadder, second check = currentFoothold.
DoJump(this):
// Determine jump type using stat blocks
isShortJump = (GetStat(this[376]+24) < 0.0 || GetStat(this[384]+108) > 0.0)
// Decrypt currentLadder pointer from ZtlSecureTear at this+280
ladderDecrypted = sub_4CDD42(this+280, this[72]) // this+70 in DWORD = byte 280
this[344/4] = 0 // this[86] = jumpRequested = clear
// ═══ PATH 1: ON LADDER/ROPE (ladderDecrypted != NULL) ═══
if ladderDecrypted:
hInput = GetSecureValue(this+320, this[82]) // horizontal input
if hInput != 0:
// Detach from ladder → become airborne
vtable[16](this, 0, 0, 0) // SetFootholdState(NULL, NULL, 0) clears state
modifier = isShortJump ? 0.3 : 0.5 // dbl_62BC60 / dbl_6295C8
walkSpeedStat = GetStat(this[376]+36) // jumpStatBlock
// Jump vertical: stat-based with modifier
velY = GetStat(this[384]+72) * (-555.0) / walkSpeedStat * modifier
this[120/4] = Encrypt(velY) // this[30] = velY
// Jump horizontal: stat-based with hInput direction
hInput = GetSecureValue(this+320, this[82])
velX = GetStat(this[384]+36) * hInput * 162.5
this[96/4] = Encrypt(velX) // this[24] = velX
// ═══ PATH 2: ON FOOTHOLD (this[68] != 0, no ladder) ═══
elif this[68]:
foothold = this[68]
// Detach from foothold (clamps T, calcs position, sets Y=floor(Y-1))
CMovePath_DetachFromFoothold(this)
// Base jump velocity (NO modifier applied here)
walkSpeedStat = GetStat(this[376]+36)
velY = GetStat(this[384]+72) * (-555.0) / walkSpeedStat
this[30] = Encrypt(velY)
// ★ Short jump: multiply velY by 0.7 (dbl_62BCF8)
if isShortJump:
currentVelY = Decrypt(this+104, this[30])
velY = currentVelY * 0.7
this[30] = Encrypt(velY)
// ─── Horizontal velocity (if pressing left/right) ───
hInput = GetSecureValue(this+320, this[82])
if hInput != 0:
cosTheta = foothold[+56] // slopeDy of foothold
direction = (hInput * cosTheta >= 0.0) ? 1 : -1
maxVel = CMovePath_ComputeMaxVelocity(foothold, this[384], direction)
currentVelX = Decrypt(this+80, this[24])
// Gate: only add horizontal velocity if below 80% of maxVel
if maxVel * 0.8 > hInput * currentVelX: // dbl_6295C0 = 0.8
currentVelX = Decrypt(this+80, this[24])
velX = hInput * maxVel * 0.8 + currentVelX
this[24] = Encrypt(velX)
// Clamp: don't exceed maxVel in movement direction
currentVelX = Decrypt(this+80, this[24])
if hInput * currentVelX > maxVel:
velX = hInput * maxVel
this[24] = Encrypt(velX)
else:
// No hInput → just decrypt velX (no change, just read)
Decrypt(this+80, this[24])
// Play jump sound (StringPool #1655) if map has no BGM
if !(***(vtable)(this[20])): // check BGM
play_game_sound("Jump")
// ═══ PATH 3: AIRBORNE (no ladder, no foothold) ═══
else:
if !isShortJump: return // Can only air-jump when short-jump conditions met
absVInput = |GetStat(this[376]+24)|
vInputStat = GetStat(this[376]+24)
if vInputStat >= 0.0: // pressing UP or neutral
velY = GetStat(this[384]+120) * absVInput * 200.0 // dbl_62E340
else: // pressing DOWN
velY = GetStat(this[384]+96) * absVInput * 500.0 // dbl_62E348
velY = -velY // negate (jump = upward = negative in screen coords)
this[30] = Encrypt(velY)
RecordMovement(type=1) // sub_59539C(1)
ApplyAccelClamped(velocity*, force, mass, maxVel, dt):
if maxVel < 0.0: return // guard — negative maxVel = skip
if force > 0.0:
if *velocity < maxVel:
*velocity += force / mass * dt
if *velocity > maxVel:
*velocity = maxVel
elif force < 0.0:
if *velocity > -maxVel:
*velocity += force / mass * dt
if *velocity < -maxVel:
*velocity = -maxVel
// force == 0.0 → do nothing
ComputeMaxVelocity(foothold, statsPtr, direction):
speedFactor = GetStat(foothold[+40]+12) // speedSubStruct[+12]
walkStat = GetStat(statsPtr+36) // walkSpeedStat
result = speedFactor * walkStat * 125.0 // dbl_62E360
if direction != 0:
sinTheta = foothold[+56] // slopeDy
result *= (sinTheta² * direction + 1.0)
// direction=+1 (downhill): result *= (sin²+1) = boost
// direction=-1 (uphill): result *= (1-sin²) = cos²(θ) = slower
return result
ApplyAirborneVelocity(this, timeMs):
mass = GetStat(this[96]+12) // walkSpeedStat mass
oldVelX = Decrypt(this+20) // this[24] (DWORD offset)
oldVelY = Decrypt(this+26) // this[30]
gravMaxForce = GetStat(this[94]+12) * 100000.0 // dbl_62E3B0
gravBase = GetStat(this[94]+12) * 10000.0 // dbl_62E3A8
dt = timeMs * 0.001
// ════════════════════════════════════════
// AIRBORNE PATH (CMovePath_IsAirborne returns true)
// IsAirborne = (this[68]==0 AND stat[94]+24 >= 0 AND stat[96]+108 <= 0)
// ════════════════════════════════════════
if CMovePath_IsAirborne(this):
termVel = GetStat(this[94]+36) * 670.0 // dbl_62E3A0
clampVel = gravBase * 0.000893 // dbl_62E398 ≈ 8.93
// ─── (A) Vertical: friction toward terminal velocity ───
// Case: velY > 0 AND termVel >= 0 AND velY > termVel
if velY > 0.0:
if termVel >= 0.0:
if velY > termVel:
velY -= gravMaxForce / mass * dt
if velY < termVel: velY = termVel
else:
// Do NOT apply friction here — velY is below terminal
negTermVel = -termVel
if velY < negTermVel:
velY += gravMaxForce / mass * dt
if velY > negTermVel: velY = negTermVel
// (if termVel < 0, skip friction entirely — handled by gravity below)
// ─── (B) Horizontal: hInput-based or friction ───
if hInput != 0:
hForce = hInput * gravBase * 2.0 // = dir * 20000 at default stats
ApplyAccelClamped(&velX, hForce, mass, clampVel, dt)
else:
// ★ DUAL FRICTION based on fall speed ★
if velY < termVel: // rising or slow fall
weakF = gravBase * 0.01 / mass * dt // dbl_62B6D0 = 0.01 → ≈1.0/frame
else: // fast falling
weakF = gravBase / mass * dt // ≈100/frame
// Apply friction toward zero
if velX > 0.0:
velX -= weakF; if velX < 0.0: velX = 0.0
elif velX < 0.0:
velX += weakF; if velX > 0.0: velX = 0.0
// ─── (C) Main gravity via ApplyAccelClamped ───
ApplyAccelClamped(&velY, GetStat(this[94]+36) * mass * 2000.0, mass, termVel, dt)
// ↑ This is the MAIN downward acceleration toward terminal velocity
// ─── (D) Vertical input (pressing down = fall faster) ───
if vInput >= 0:
// Not pressing down: apply normal gravity as secondary force
ApplyAccelClamped(&velY, gravBase * mass / absVInput * clampVelY, mass, termVel, dt)
else:
// Pressing down: cap velY at termVel * 0.3 (dbl_62BC60)
reducedCap = termVel * 0.3
if velY > reducedCap:
velY -= (120000.0 / mass / absVInput * clampVelY) * dt
clamp to reducedCap
elif velY < reducedCap:
velY += (120000.0 / mass / absVInput * clampVelY) * dt * 0.5
clamp to reducedCap
// ════════════════════════════════════════
// NON-AIRBORNE PATH (on rope/ladder within airborne velocity update)
// IsAirborne returned false → on rope/ladder
// ════════════════════════════════════════
else:
absVInput = |GetStat(this[94]+24)| // vertical input stat
vInputDir = GetStat(this[94]+24) // raw value (sign = direction)
if vInputDir >= 0.0: // climbing up or neutral
clampVelY = GetStat(this[96]+108) // stat: rope climb speed up
hMultiplier = GetStat(this[96]+120) * absVInput * 200.0 // dbl_62E340
else: // climbing down
clampVelY = GetStat(this[96]+84) // stat: rope climb speed down
hMultiplier = GetStat(this[96]+96) * absVInput * 100.0 // dbl_62B6E8
hForceBase = hInput * clampVelY * absVInput * 120000.0 // dbl_62E388
// ─── Clamp velX toward ±hMultiplier ───
if hMultiplier >= 0.0:
if velX > hMultiplier:
velX -= gravMaxForce / mass * dt; clamp to hMultiplier
if velX < -hMultiplier:
velX += gravMaxForce / mass * dt; clamp to -hMultiplier
// ─── Horizontal force: hInput or friction ───
if hInput != 0:
ApplyAccelClamped(&velX, hForceBase, mass, hMultiplier, dt)
else:
// Friction toward zero (for horizontal when no input)
if velX > 0.0:
velX -= gravMaxForce / mass * dt; clamp to 0.0
elif velX < 0.0:
velX += gravMaxForce / mass * dt; clamp to 0.0
// ─── Clamp velY toward ±hMultiplier ───
if hMultiplier >= 0.0:
if velY > hMultiplier:
velY -= gravMaxForce / mass * dt; clamp
if velY < -hMultiplier:
velY += gravMaxForce / mass * dt; clamp
// ─── Vertical: different handling based on vInput direction ───
vertForce = 120000.0 / mass / absVInput * clampVelY
if vInput >= 0:
ApplyAccelClamped(&velY, vertForce * mass, mass, hMultiplier, dt)
else:
reducedCap = hMultiplier * 0.3 // dbl_62BC60
if velY > reducedCap:
velY -= vertForce * dt; clamp to reducedCap
elif velY < reducedCap:
velY += vertForce * dt * 0.5; clamp to reducedCap
// ═══ POSITION INTEGRATION (both paths) ═══
newPosX = Decrypt(posX) // this+8 current
newPosY = Decrypt(posY) // this+14 current
savedPosX = Decrypt(saved+8)
savedPosY = Decrypt(saved+14)
posX = (oldVelX + velX) * dt * 0.5 + savedPosX // trapezoidal
posY = (oldVelY + velY) * dt * 0.5 + savedPosY
Store posX, posY, velX, velY (all encrypted)
GroundedVelocityUpdate(this, timeMs):
foothold = this[68]
speedSub = foothold[+40] // speed sub-structure pointer
mass = GetStat(this[96]+12) // walkSpeedStat
absSin = |foothold[+56]| // |sin(θ)|
sinSq = absSin² // sin²(θ)
// Note: fabs(foothold[+48]) is also computed (|cos(θ)|) but unused
direction = (foothold[+56] >= 0.0) ? -1 : +1 // REVERSED: positive slopeDy → direction=-1
tVel = Decrypt(this+62, this[66]) // current T-velocity
// ─── Speed factor from foothold speedSubStruct ───
isDefault = (GetStat(speedSub+24) == 1.0 AND GetStat(speedSub+36) == 0.0)
if isDefault:
speedFactor = GetStat(this[96]+24) // player's natural speed factor
else:
speedFactor = sub_5BEDAB(1.0) // allocate a new SecureTear with 1.0
// (must be freed later — cleanup_flag is set)
// ─── Base acceleration force ───
sliderStat = GetStat(this[94]) // movementStats base
baseAccelForce = speedSub[+12] * speedFactor * sliderStat * 140000.0 // dbl_62E380
hInput = GetSecureValue(this+80, this[82])
inputAccel = hInput * baseAccelForce
slopeDyForce = GetStat(speedSub+36) // WZ drag force from foothold
// ─── INPUT ACCELERATION MODIFIER ───
if slopeDyForce == 0.0:
// No WZ drag → if input opposes current momentum, set inputAccel=0
if hInput * inputAccel <= 0.0:
inputAccel = 0.0
// Actually: if slopeDyForce==0 AND hInput==0, inputAccel was already 0
elif hInput != 0:
// ★ Key slope modifier: adjusts input force based on slopeDy ★
absDyForce = |slopeDyForce|
// Complex flag comparison (v14/v15 are FPU condition codes):
if (hInput direction matches slopeDy sign): // moving UPHILL
factor = 2 * absDyForce // double the drag
else: // moving DOWNHILL
factor = 0.2 / absDyForce // dbl_62BC78 — reduced force
inputAccel = factor * inputAccel
else:
// No hInput but has slopeDyForce → pure slope drag
inputAccel = slopeDyForce * baseAccelForce
// ─── DIRECTION FACTOR ───
if direction <= 0: // going "with" slope
dirFactor = sinSq + 1.0 // boost (> 1)
else: // going "against" slope
dirFactor = 1.0 - sinSq // reduce (= cos²(θ))
finalAccel = dirFactor * inputAccel
// ─── BASE MAX VELOCITY (direction=0 → no angular adjustment) ───
baseMaxVel = ComputeMaxVelocity(foothold, this[96], 0)
// ─── FRICTION SPEED FACTOR ───
if slopeDyForce != 0.0:
if hInput != 0:
absDyF = |slopeDyForce|
if (sign match):
frictionMult = 2 * absDyF
else:
frictionMult = 0.2 / absDyF
else:
frictionMult = |slopeDyForce|
else:
frictionMult = 1.0 // no slope drag
baseMaxVel *= frictionMult // adjust maxVel by slope drag
// ─── Friction sub-structure handling ───
if isDefault:
frictionFactor = GetStat(this[96]+48)
else:
frictionFactor = sub_5BEDAB(1.0)
sliderStat2 = GetStat(speedSub[+12]) // same slider
speedFactorVal = speedSub[+12] * frictionFactor * sliderStat2
speedFactorVal = clamp(speedFactorVal, 0.05, 2.0) // dbl_62BC70 / dbl_62BDA8
if speedFactorVal < 1.0:
speedFactorVal *= 0.5 // halve it (dbl_6295C8)
friction = speedFactorVal * 80000.0 // dbl_62E378
flatFriction = (speedFactorVal == 0.0) ? 16000.0 : friction // dbl_62E370
dt = timeMs * 0.001
// ════════════════════════════════════════════
// OVER-SPEED FRICTION: clamp tVel if |tVel| > baseMaxVel
// ════════════════════════════════════════════
if baseMaxVel >= 0.0:
if tVel > baseMaxVel:
tVel -= flatFriction / mass * dt
if tVel < baseMaxVel: tVel = baseMaxVel
elif tVel < -baseMaxVel:
tVel += flatFriction / mass * dt
if tVel > -baseMaxVel: tVel = -baseMaxVel
// ════════════════════════════════════════════
// STEEP SLOPE: |sin(θ)| > playerSlideThreshold
// playerSlideThreshold = GetStat(this[96]+60)
// ════════════════════════════════════════════
if GetStat(this[96]+60) < absSin:
slopeGrav = absSin * 60000.0 * (-direction) // dbl_62E368 — gravity along slope
slopeMaxVel = absSin * 120.0 // dbl_62DEA0
if direction * hInput <= 0: // pressing downhill or no input
if hInput != 0 OR slopeDyForce != 0.0: // active force present
finalForce = slopeGrav + finalAccel
finalMaxVel = slopeMaxVel + (sinSq+1)*baseMaxVel
else: // pure slide (no input, no WZ drag)
finalForce = slopeGrav
finalMaxVel = slopeMaxVel
else: // pressing uphill
finalForce = slopeGrav * 0.5 // dbl_6295C8 = 0.5
finalMaxVel = slopeMaxVel * 0.5
// ─── Friction when opposing slope direction ───
if direction * tVel > 0.0: // moving WITH slope
// Decelerate toward zero
if tVel > 0.0:
tVel -= flatFriction / mass * dt
if tVel < 0.0: tVel = 0.0
elif tVel < 0.0:
tVel += flatFriction / mass * dt
if tVel > 0.0: tVel = 0.0
// Cross-zero clamp
if tVel crossed sign: tVel = 0.0
ApplyAccelClamped(&tVel, finalForce, mass, finalMaxVel, dt)
// ════════════════════════════════════════════
// GENTLE SLOPE OR FLAT: |sin(θ)| <= playerSlideThreshold
// ════════════════════════════════════════════
else:
// speedFactorVal==0 → special: add slope gravity into accel
if speedFactorVal == 0.0:
finalAccel -= baseMaxVel * 60000.0 * direction // dbl_62E368
if hInput != 0 OR speedFactorVal == 0.0 OR slopeDyForce != 0.0:
// ─── Active force: accel toward maxVel ───
if direction * finalAccel <= 0.0: // downhill
accelMaxVel = (sinSq + 1.0) * baseMaxVel
else: // uphill
accelMaxVel = baseMaxVel // no sin² adjustment
// (note: this branch is (1.0 - sinSq) in the "not-steep" else path
// but for gentle slopes sin²→0 so effectively just baseMaxVel)
ApplyAccelClamped(&tVel, finalAccel, mass, accelMaxVel, dt)
else:
// ─── NO INPUT → pure friction to zero ───
if tVel > 0.0:
tVel -= friction / mass * dt
if tVel < 0.0: tVel = 0.0
elif tVel < 0.0:
tVel += friction / mass * dt
if tVel > 0.0: tVel = 0.0
// ─── T-PARAMETER INTEGRATION ───
oldT = Decrypt(this+56, this[60]) // old T-position
T = oldT + (oldTVel + tVel) * dt * 0.5 // trapezoidal integration
Store T, tVel back (encrypted)
Was entirely missing from previous pseudocode. This is vtable[8].
HandleGroundedMovement(this, a2_savedT, a3_walkStat, a4_timeMs,
a5_velX, a6_velY, a7_friction):
// Ask parent for move speed info
moveSpeed = this[5]->vtable[12](&v57, a3, a2)
baseFriction = (double)a7 // v54 = friction force
// ═══ GROUNDED ON FOOTHOLD (this[68] != 0) ═══
if this[68]:
this[66] = Encrypt((double)a5) // store T-velocity tag
CMovePath_CalculatePositionFromT(this+8, this+56, this[68])
else:
// Not on foothold (airborne/ladder within grounded update)
this[24] = Encrypt((double)a5) // velX
this[30] = Encrypt((double)a6) // velY
sub_59539C(2) // Record movement type=2
if a4_timeMs <= 0: return
// ═══ MAIN SUB-STEPPING LOOP (30ms max per step) ═══
remaining = a4_timeMs
while remaining > 0:
step = min(remaining, 30) // 30ms max per sub-step
leftover = remaining - step
// ─── PATH A: ON FOOTHOLD ───
if this[68]:
savedTVel = Decrypt(this+62, this[66]) // current T-velocity
dt_sec = (double)step * 0.001
mass = GetStat(moveSpeed + 12)
// Friction: decelerate T-velocity toward zero
if savedTVel > 0.0:
savedTVel -= baseFriction / mass * dt_sec
if savedTVel < 0.0: savedTVel = 0.0
elif savedTVel < 0.0:
savedTVel += baseFriction / mass * dt_sec
if savedTVel > 0.0: savedTVel = 0.0
this[66] = Encrypt(savedTVel)
// T-position integration (trapezoidal using saved old T-vel)
oldT = Decrypt(this+56, this[60])
oldTVel_saved = Decrypt(savedT+6, savedT[10]) // from saved state
T = (oldTVel_saved + savedTVel) * (double)step * 0.0005 + oldT // dbl_62E4A8
this[60] = Encrypt(T)
// ─── FOOTHOLD TRANSITION: T < 0 → move to prev foothold ───
if T < 0.0:
prevFh = this[68][+76] // previous foothold pointer
if prevFh == NULL OR prevFh[+48] <= 0.0: // no prev or it's a wall
// Clamp: T=0, TVel=0 (stop at edge)
this[60] = Encrypt(0.0)
this[66] = Encrypt(0.0)
CMovePath_CalculatePositionFromT(this+8, this+56, this[68])
else:
// Wrap T to previous foothold
newT = prevFh[+64] + T // length + negative_T
if newT < 0.0: newT = 0.0
this[60] = Encrypt(newT)
CMovePath_CalculatePositionFromT(this+8, this+56, prevFh)
SetFootholdState(this, prevFh, NULL, step) // transition
continue // to next sub-step
// ─── FOOTHOLD TRANSITION: T > length → move to next foothold ───
elif T > this[68][+64]: // T exceeds foothold length
nextFh = this[68][+80] // next foothold pointer
if nextFh == NULL OR nextFh[+48] <= 0.0: // no next or wall
// Clamp: T=length, TVel=0
this[60] = Encrypt(this[68][+64])
this[66] = Encrypt(0.0)
CMovePath_CalculatePositionFromT(this+8, this+56, this[68])
else:
// Wrap T to next foothold
newT = T - this[68][+64]
if newT > nextFh[+64]: newT = nextFh[+64]
this[60] = Encrypt(newT)
CMovePath_CalculatePositionFromT(this+8, this+56, nextFh)
SetFootholdState(this, nextFh, NULL, step)
continue
else:
CMovePath_CalculatePositionFromT(this+8, this+56, this[68])
vtable[20](this, step) // timer update
// ─── PATH B: AIRBORNE/LADDER (this[68]==0 AND state==3) ───
elif this[130] == 3: // movement state = ladder
savedState = copy(this+8, 96bytes)
savedVelX = Decrypt(this+20, this[24])
dt_dbl = (double)step
dt_sec = dt_dbl * 0.001
mass = GetStat(moveSpeed + 12)
// Friction on velX
if savedVelX > 0.0:
savedVelX -= baseFriction / mass * dt_sec
if savedVelX < 0.0: savedVelX = 0.0
elif savedVelX < 0.0:
savedVelX += baseFriction / mass * dt_sec
if savedVelX > 0.0: savedVelX = 0.0
this[24] = Encrypt(savedVelX)
// Same for velY
savedVelY = Decrypt(this+26, this[30])
// (identical friction logic for Y axis)
this[30] = Encrypt(savedVelY)
// Trapezoidal position integration
posX += (oldVelX_saved + newVelX) * dt_dbl * 0.0005 // dbl_62E4A8
posY += (oldVelY_saved + newVelY) * dt_dbl * 0.0005
CMovePath_ClampToBounds(savedState, 0)
else:
vtable[20](this, step) // just timer update for other states
remaining = leftover
AirbornePhysicsUpdate(this_ebp, savedState, &timeMs, mode):
v5 = this_ebp[2] // pointer to CMovePath object
v4 = this_ebp[3] // pointer to timeMs storage
remaining = CMovePath_ClampToBounds(v5, *v4)
// ─── Round positions to integers with ±0.5 offset ───
posX = Decrypt(v5+0, v5[4])
if posX < 0: oldX = (int)(posX - 0.4999...) // dbl_62A498
else: oldX = (int)(posX + 0.5) // dbl_6295C8
posY = Decrypt(v5+6, v5[10])
if posY < 0: oldY = (int)(posY - 0.4999...)
else: oldY = (int)(posY + 0.5)
// Same for new position (at v5+32 = velocity update block)
newPosX = Decrypt(v5+32, v5[48])
if newPosX < 0: newX = (int)(newPosX - 0.4999...)
else: newX = (int)(newPosX + 0.5)
newPosY = Decrypt(v5+56, v5[72])
if newPosY < 0: newY = (int)(newPosY - 0.4999...)
else: newY = (int)(newPosY + 0.5)
dx = newX - oldX
dy = newY - oldY
if dx == 0 AND dy == 0:
*v4 -= remaining
vtable[20](*v4)
return 0
// ─── AABB spatial query → linked list of foothold candidates ───
CWvsPhysicalSpace2D_GetCrossCandidate(oldX, oldY, newX, newY, &candidateList)
if candidateList is empty: goto NO_INTERSECTION
bestNum = 1, bestDen = 2 // t = bestDen/bestNum, init to t > 0.5 (far away)
bestCrossFh = NULL
bestIntersectFh = NULL
// ════════════════════════════════════════
// ITERATE ALL CANDIDATES
// ════════════════════════════════════════
for each candidate in candidateList:
fh = candidate.foothold
slopeDx = fh[+48] // cos(θ) of foothold
// ─── SLOPE-GATED INCLUSION ───
// If slopeDx > 0 (floor): ALWAYS include
// If slopeDx <= 0 (wall): require group match
if slopeDx <= 0: // wall
if fh[+32] != currentFh.groupID AND fh[+32] != this[296]:
continue // different collision group → skip
// ─── 4-CROSS PRODUCT TEST (exact integer math) ───
fhX1,fhY1,fhX2,fhY2 = fh[+12,+16,+20,+24]
fhDx = fhX2-fhX1
fhDy = fhY2-fhY1
cross1 = fhDx*(oldY-fhY1) - fhDy*(oldX-fhX1) // old pos vs fh line
cross2 = fhDx*(newY-fhY1) - fhDy*(newX-fhX1) // new pos vs fh line
// Must cross from right-to-left (cross1<=0 AND cross2>=0)
if cross1 > 0: continue // old pos on wrong side
if cross2 < 0: continue // new pos on wrong side
if cross1==0 AND cross2==0: continue // collinear
cross3 = dx*(fhY1-oldY) - dy*(fhX1-oldX) // fh.P1 vs movement line
cross4 = dx*(fhY2-oldY) - dy*(fhX2-oldX) // fh.P2 vs movement line
if cross3 * cross4 > 0: continue // both endpoints same side → no intersection
// ─── VERTEX HANDLING (cross3 or cross4 == 0) ───
if cross3 == 0: // trajectory hits P1 exactly
prevFh = fh[+76]
if prevFh AND SegmentIntersectionTest(prevFh, fh, newX, newY):
crossFh = prevFh; intersectFh = fh
else: continue
elif cross4 == 0: // trajectory hits P2 exactly
nextFh = fh[+80]
if nextFh AND SegmentIntersectionTest(fh, nextFh, newX, newY):
crossFh = fh; intersectFh = nextFh
else: continue
else: // normal mid-segment intersection
crossFh = fh; intersectFh = fh
// ─── T-PARAMETER COMPARISON (integer cross-multiply for exact ordering) ───
absDenom = |dx*fhDy - dy*fhDx| // denominator of intersection T
absNumer = |cross3*fhDx + cross1*dx| // simplified numerator
// Compare: is this intersection closer than best?
// Using integer cross-multiply to avoid float division:
// current_t < best_t ⟺ bestDen * absNumer < bestNum * absDenom
newIsBetter = (bestDen * absNumer) < (bestNum * absDenom) // 64-bit multiply
if newIsBetter:
bestNum = absDenom
bestDen = absNumer
bestCrossFh = crossFh
bestIntersectFh = intersectFh
// Compute exact intersection coordinate (float division)
intersectCoord = 64bit_divide(numerator, denominator)
if dx != 0:
intersectY = (intersectCoord - fhX1) * fhDy / fhDx + fhY1
else:
intersectY = (intersectCoord - oldX) * dy / dx + oldY
elif exact tie (bestDen*absNumer == bestNum*absDenom):
// TIE-BREAKING: prefer foothold with "tighter" cross-product
// Tests direction of intersection relative to candidate footholds
if cross_product test says this is tighter: update bestCrossFh
if cross_product test says this is tighter: update bestIntersectFh
// ════════════════════════════════════════
// NO INTERSECTION
// ════════════════════════════════════════
if bestCrossFh == NULL OR bestIntersectFh == NULL:
NO_INTERSECTION:
*v4 -= remaining
vtable[20](*v4)
return 0
// ════════════════════════════════════════
// INTERSECTION FOUND → Landing/Wall hit
// ════════════════════════════════════════
t_fraction = (double)bestDen / (double)bestNum
*v4 -= (int)(remaining * t_fraction)
// Interpolate velocity at collision point
velX_at_t = (savedVelX_end - savedVelX_start) * t_fraction + savedVelX_start
velY_at_t = (savedVelY_end - savedVelY_start) * t_fraction + savedVelY_start
// Determine landing foothold
fh = bestCrossFh
if bestCrossFh != bestIntersectFh:
// vertex intersection: choose which foothold to land on
dotA = fh_A[+48]*velX_at_t + fh_A[+56]*velY_at_t // dot with fhA
dotB = fh_B[+48]*velX_at_t + fh_B[+56]*velY_at_t // dot with fhB
if dotA > 0.001 AND dotB > 0.001: // both positive: use lower
fh = (fhA.x1 >= fhA.x2) ? fhB : fhA
elif dotA >= -0.001: // dotA near zero: use B
fh = fhB
else: // dotB near zero or negative: use A
fh = fhA
// ─── Compute T on landing foothold ───
dotProduct = fh[+48]*velX_at_t + fh[+56]*velY_at_t
this_T_vel = Encrypt(dotProduct) // v82[40]
if |fh[+48]| > 0.5: // mostly horizontal foothold
T = (intersectX - fh[+12]) / fh[+48]
else: // mostly vertical
T = (intersectY - fh[+16]) / fh[+56]
T = clamp(T, 0, fh[+64]) // clamp to [0, length]
this_T_pos = Encrypt(T) // v82[16]
// ─── FLOOR LANDING (slopeDx > 0) ───
if fh[+48] > 0.0:
// H-gate for T-velocity
hInput = GetSecureValue(this+320, this[82])
tVelY = Decrypt(v82+24, v82[40])
if hInput * tVelY >= 0.0:
T_velocity = tVelY * 0.5 // carry momentum (dbl_6295C8)
else:
T_velocity = 0.0 // opposing → stop
v82[40] = Encrypt(T_velocity)
CMovePath_CalculatePositionFromT(posStruct, tStruct, fh)
SetFootholdState(this, fh, NULL, *v4) // vtable[16]
// ─── WALL HIT (slopeDx <= 0) ───
else:
if hInput != 0: // a1[4] = horizontal input flag
// Project velocity onto wall surface
velX_interp = Decrypt(posStruct+12, posStruct[16])
velY_interp = Decrypt(posStruct+18, posStruct[22])
dotProduct = fh[+48]*velX_interp + fh[+56]*velY_interp
// New velocity = dotProduct projected back onto wall direction
newVelX = dotProduct * fh[+48]
newVelY = dotProduct * fh[+56]
Store newVelX, newVelY (encrypted)
// Position: trapezoidal integration along the wall
dt_remaining = (double)*v4 * 0.001
posX = fh[+48]*T + fh[+12] + (velX_proj + newVelX) * dt_remaining * 0.5
posY = fh[+56]*T + fh[+16] + (velY_proj + newVelY) * dt_remaining * 0.5
Store posX, posY (encrypted)
else:
// No hInput: just snap position from T
CMovePath_CalculatePositionFromT(posStruct, tStruct, fh)
vtable[20](this) // timer update only (NO SetFootholdState for walls)
return 1
DetachFromFoothold(this):
fh = this[68]
if fh == NULL: return
length = fh[+64]
T = Decrypt(this+56, this[60])
T = clamp(T, 0.0, length)
this[60] = Encrypt(T)
CMovePath_CalculatePositionFromT(this+8, this+56, fh)
// Clamp posX within foothold x-bounds
posX = Decrypt(this+8, this[12])
x1 = (double)fh[+12]
x2 = (double)fh[+20]
posX = clamp(posX, x1, x2)
this[12] = Encrypt(posX)
// Position Y = floor(Y - 1.0) — 1 pixel above foothold surface
posY = Decrypt(this+14, this[18])
posY = floor(posY - 1.0) // dbl_629640 = 1.0
this[18] = Encrypt(posY)
vtable[16](this, 0, 0, 0) // SetFootholdState(NULL, NULL, 0) — clear foothold
LadderAction(this):
targetVelX = *(double*)(this+90) // byte offset 360 (this[90])
targetVelY = *(double*)(this+92) // byte offset 368 (this[92])
this[88] = 0 // clear ladderActionRequested flag
if this[68] != 0: // on foothold
CMovePath_DetachFromFoothold(this)
else: // airborne
ladder = sub_4CDD42(this+280, this[72]) // decrypt currentLadder
if ladder:
vtable[16](this, 0, 0, 0) // SetFootholdState(NULL) — clear
// ─── Adjust velX toward targetVelX ───
currentVelX = Decrypt(this+80, this[24])
if targetVelX >= 0.0:
if currentVelX <= targetVelX:
// skip (already at or below target)
else: // too fast, reduce
// Wait, actually the logic is the inverse...
// (Actually: approaches targetVelX by adding it, then clamping)
if targetVelX >= 0.0 OR currentVelX <= targetVelX:
if targetVelX <= 0.0 OR currentVelX >= targetVelX:
// Already within range, skip
else:
newVelX = currentVelX + targetVelX
if targetVelX < newVelX: newVelX = targetVelX
this[24] = Encrypt(newVelX)
else:
newVelX = currentVelX + targetVelX
if newVelX < targetVelX: newVelX = targetVelX
this[24] = Encrypt(newVelX)
// ─── Same logic for velY ───
currentVelY = Decrypt(this+104, this[30])
// (identical clamping logic toward targetVelY)
// ─── Truncate to integer ───
velX = (double)(int)(int64_t)Decrypt(this+80, this[24])
this[24] = Encrypt(velX)
velY = (double)(int)(int64_t)Decrypt(this+104, this[30])
this[30] = Encrypt(velY)
sub_59539C(2) // RecordMovement type=2
LadderClimb(this):
ladder = sub_4CDD42(this+70, this[72]) // decrypt currentLadder
if ladder == NULL: return vtable[20](this, 30)
posY = Decrypt(this+14, this[18])
vInput = GetSecureValue(this+83, this[85])
posY += vInput * 3.0 // dbl_62A4A0 = ladder climb speed
this[18] = Encrypt(posY)
// ─── EXIT AT TOP (climbing up past top boundary) ───
if vInput < 0 AND Decrypt(this+14) < (double)ladder[4]: // ladder.topY
if ladder[2]: // has exit platform
posY = (double)(ladder[4] - 5) // 5 pixels above top
this[18] = Encrypt(posY)
vtable[16](this, 0, 0, 0) // detach from ladder
return vtable[20](this, 30)
else:
posY = (double)*(int*)(sub_4CDD42(this+70, this[72]) + 16)
this[18] = Encrypt(posY) // clamp to ladder.topY2
// ─── EXIT AT BOTTOM (climbing down past bottom boundary) ───
if vInput > 0 AND Decrypt(this+14) > (double)ladder[5]: // ladder.bottomY
posY = (double)(ladder[5] + 1) // 1 pixel below bottom
this[18] = Encrypt(posY)
vtable[16](this, 0, 0, 0) // detach from ladder
return vtable[20](this, 30)
vtable[20](this, 30) // timer/animation update
SegmentIntersectionTest(fh1, fh2, newX, newY):
// Tests if newPos is blocked at the shared vertex between fh1 and fh2
// fh1.end == fh2.start (shared vertex)
// Corner convexity (cross product of the two foothold directions)
cornerCross = (fh2.y2-fh2.y1)*(fh1.x2-fh1.x1) - (fh2.x2-fh2.x1)*(fh1.y2-fh1.y1)
// Position relative to fh1 line
posCross = (newY-fh1.y1)*(fh1.x2-fh1.x1) - (newX-fh1.x1)*(fh1.y2-fh1.y1)
if cornerCross <= 0: // concave corner
return posCross > 0 // blocked if on "inside" of concave bend
else: // convex corner
if posCross <= 0: return false // on outside of fh1 → not blocked
// Also check relative to fh2 line
posCross2 = (newY-fh2.y1)*(fh2.x2-fh2.x1) - (newX-fh2.x1)*(fh2.y2-fh2.y1)
return posCross2 > 0 // blocked only if inside BOTH fh lines
SetFootholdState(this, foothold, ladder, timeMs):
vtable[20](this, timeMs)
oldLayer = this[73]; oldGroup = this[74]
newLayer = oldLayer; newGroup = oldGroup
if foothold:
newLayer = foothold[+28]; newGroup = foothold[+32]
this[69] = foothold // previousFoothold
elif ladder:
newLayer = ladder[+24]; newGroup = 0
if newLayer != oldLayer OR newGroup != oldGroup:
this[74] = newGroup; this[73] = newLayer
this[5]->vtable[8](this[5], this) // notify parent of layer/group change
this[68] = foothold // may be NULL to clear
ZtlSecureTear_Store(this+70, ladder)
hInput = GetSecureValue(this+83, this[85])
vInput = GetSecureValue(this+80, this[82])
this[79] = this[5]->vtable[4](vInput, hInput, this[79], this)
CMovePath_RecordMovement(this)
Was entirely missing from previous pseudocode.
ClampToBounds(this, a2_savedState, a3_timeMs):
remaining = a3_timeMs
// Read current and saved positions
currentPosX = Decrypt(this+8, this[12])
savedPosX = Decrypt(a2, a2[4])
deltaX = currentPosX - savedPosX
currentPosY = Decrypt(this+14, this[18])
savedPosY = Decrypt(a2+6, a2[10])
deltaY = currentPosY - savedPosY
// ─── X < minX clamp ───
minX = (double)this[75]
if currentPosX < minX:
this+8 -> posX = Encrypt(minX)
this+20 -> velX = Encrypt(0.0) // zero velocity on clamp
if deltaX != 0.0:
remaining = (int)((minX - savedPosX) / deltaX * a3_timeMs)
// ─── X > maxX clamp ───
maxX = (double)this[77]
if currentPosX > maxX:
this+8 -> posX = Encrypt(maxX)
this+20 -> velX = Encrypt(0.0)
if deltaX != 0.0:
remaining = (int)((maxX - savedPosX) / deltaX * a3_timeMs)
remaining = clamp(remaining, 0, a3_timeMs)
// ─── Y < minY clamp ───
minY = (double)this[76]
if currentPosY < minY:
this+14 -> posY = Encrypt(minY)
this+26 -> velY = Encrypt(0.0)
if deltaY != 0.0:
yRemaining = (int)((minY - savedPosY) / deltaY * a3_timeMs)
yRemaining = max(yRemaining, 0)
if yRemaining < remaining:
remaining = yRemaining
return remaining
// Note: Y has NO maxY clamp (only min). Player falls off bottom of map.
CalculatePositionFromT(posStruct, tStruct, foothold):
T = Decrypt(tStruct, tStruct[16/4])
posX = foothold[+48] * T + (double)foothold[+12] // cos(θ)*T + x1
posY = foothold[+56] * T + (double)foothold[+16] // sin(θ)*T + y1
Store posX, posY (encrypted)
TVel = Decrypt(tStruct+24, tStruct[40/4])
velX = TVel * foothold[+48] // TVel * cos(θ)
velY = TVel * foothold[+56] // TVel * sin(θ)
Store velX, velY (encrypted)
IsAirborne(this):
return (this[68] == 0) // no foothold attached
AND (GetStat(this[94]+24) >= 0.0) // movementStats check (not on rope?)
AND (GetStat(this[96]+108) <= 0.0) // speedStats check (not climbing?)
RecordMovement(this):
lastAction = this[79]
velY = Decrypt(this+26)
velX_ft = Decrypt(this+20) // foothold T-velocity
velX = Decrypt(this+14) // position velocity
posX = Decrypt(this+8)
ladder = sub_4CDD42(this+70, this[72])
sub_4D7EE8(0, this[68], ladder, posX, velX, velX_ft, velY, lastAction)
// This sends a movement element to the movement packet buffer