Skip to content

Instantly share code, notes, and snippets.

@ThatMapleDev
Created February 23, 2026 18:20
Show Gist options
  • Select an option

  • Save ThatMapleDev/c01f486dec55caa21a00ccdb8fadc88f to your computer and use it in GitHub Desktop.

Select an option

Save ThatMapleDev/c01f486dec55caa21a00ccdb8fadc88f to your computer and use it in GitHub Desktop.
Player Physics

Physics Pseudocode — All Functions, All Paths (Audit-Verified)

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).


1. CMovePath_UpdatePhysics (0x5BC3B5) — Main Dispatch

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

2. CUserLocal_DoJump (0x5BC5F7) — Jump Initiation

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)

3. CMovePath_ApplyAccelClamped (0x5BD345)

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

4. CMovePath_ComputeMaxVelocity (0x5BC9A2)

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

5. CMovePath_ApplyAirborneVelocity (0x5BD3B6)

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)

6. CMovePath_GroundedVelocityUpdate (0x5BCBB6)

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)

7. CMovePath_HandleGroundedMovement (0x5C03EE)

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

8. CMovePath_AirbornePhysicsUpdate (0x5BD96C) — Collision

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

9. CMovePath_DetachFromFoothold (0x5BE7D9)

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

10. CMovePath_LadderAction (0x5BC9EE)

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

11. CMovePath_LadderClimb (0x5C4B27)

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

12. CMovePath_SegmentIntersectionTest (0x5BE231)

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

13. CMovePath_SetFootholdState (0x5BEC40)

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)

14. CMovePath_ClampToBounds (0x5BE8D8)

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.

15. CMovePath_CalculatePositionFromT (0x5BC066)

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)

16. CMovePath_IsAirborne (0x59D0CE)

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?)

17. CMovePath_RecordMovement (0x5BED09)

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment