//===========================================================================//
// File:	mech2.cpp                                                           //
// Project: BattleTech           Brick: Entity Manager                        //
// Contents: Mech locomotion / gait animation -- second implementation slice  //
//---------------------------------------------------------------------------//
//   Date    Who  Modification                                               //
// --------  ---  ---------------------------------------------------------- //
//  --/--/95  ??   Initial coding.                                            //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
//                      PROPRIETARY AND CONFIDENTIAL                         //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary (Ghidra pseudo-C in
// all/part_012.c, cluster 0x4a5028-0x4a7400) cross-referenced with the
// surviving animation-name string table at .data:0050cfe8 and the
// CLASSMAP shared-base offset layout.  The decompiler tagged exactly four
// functions in this window as file=bt/mech2.cpp:
//
//      @004a5028  Mech::AdvanceLegAnimation        (1543 bytes)
//      @004a5678  Mech::AdvanceBodyAnimation       (1333 bytes)
//      @004a5bf8  Mech::AdvanceBodyAnimationAirborne(1792 bytes)
//      @004a71f4  Mech::AdvanceLegAnimationAirborne (1840 bytes)
//
// The embedded assert path on every one of them is
//      "d:\tesla\bt\bt\MECH2.CPP"
// (strings @0050d7e4 / 0050d81a / 0050d850 / 0050d886 / 0050d8bc) at source
// lines 0xD3, 0x13B, 0x206, 0x2E8 and 0x672 respectively -- confirming the
// attribution and the original source ordering.
//
// These four functions are Mech METHODS.  The Mech class declaration is owned
// by mech.cpp / mech.hpp (the first slice); this file therefore declares no
// header of its own.  The member offsets it touches are documented in the
// "Mech animation member map" block below so the mech.hpp owner can fold them
// into the class definition (see report).  Field offsets noted in comments are
// the byte offsets observed in the decompiled object (e.g. "@0x348").
//
//---------------------------------------------------------------------------//
// THE TWO ANIMATION CHANNELS
//
// A Mech carries two parallel gait animators, each a SequenceController plus a
// small state machine driven by an AlarmIndicator that stores the current
// animation enumerant:
//
//   Channel A ("leg" / locally-simulated gait)
//       legAnimation        @0x65c   SequenceController
//       legStateAlarm       @0x39c   AlarmIndicator (current state readable @0x3b0)
//       legCycleSpeed       @0x348   smoothed leg-cycle speed
//       Input: the *live* commanded speed from the controls subsystem
//              ( *(*(this+0x128)) + 0x128 ).
//
//   Channel B ("body" / displayed-motion gait, integrated by Mech::IntegrateMotion
//       @004ab1c8 to advance the world transform)
//       bodyAnimation       @0x6bc   SequenceController
//       bodyStateAlarm      @0x714   AlarmIndicator (current state readable @0x728)
//       bodyCycleSpeed      @0x6b8   smoothed body-cycle speed
//       bodyTargetSpeed     @0x6b4   target speed snapshot (dead-reckoned / net)
//
// Each channel ships in two flavours selected per-frame by movementMode @0x40
// (see Mech::IntegrateMotion @004ab1c8, which picks the airborne body updater
// when (movementMode==3 || movementMode==4) && jumpActive @0x580):
//       "ground"   -- 0x5028 (leg) / 0x5678 (body)
//       "airborne" -- 0x71f4 (leg) / 0x5bf8 (body):  adds the FallForward /
//                     FallBackward jump-jet states 0x18/0x19 and clamps the
//                     commanded speed to the jump-speed limits.
//
// The exact A=leg / B=body role split is best-effort; what is certain from the
// decomp is the (channelA,channelB) x (ground,airborne) 2x2 grouping and the
// data each reads.  Flagged inline as TODO where uncertain.
//
//---------------------------------------------------------------------------//
// Helper / engine routine name mapping used below:
//      FUN_0042790c  SequenceController::Advance(increment, loop) -> Scalar
//                    (advances the active clip, returns cycle distance covered)
//      FUN_004277a8  SequenceController::SelectSequence(clip, dbg...)  (via setters)
//      FUN_004283b8  SequenceController::Reset(loop)
//      FUN_0041bbd8  AlarmIndicator::SetLevel(n)            (== heat.cpp mapping)
//      FUN_00408440  Vector/string Assign(dst, src)         (clears @0x598 to "")
//      FUN_0040385c  Verify()/assert(msg,file,line)         (== heat.cpp mapping)
//      FUN_004dbb24  DebugStream::operator<<(stream,str)    (error message build)
//      FUN_004d9c38  DebugStream::flush/emit
//      FUN_0049fb54  Mech::IsDisabled()  (true => mask action-request bits)
//      FUN_004a7fc4  Mech::SetLegAnimation(state)           (cluster helper, see below)
//      FUN_004a800c  Mech::SetBodyAnimation(state)          (cluster helper, see below)
//      FUN_004a4c54  Mech::RequestActionFlags(bits)         (cluster helper, see below)
//
// Read-only constants resolved from CODE literal pools (all == 0.0f):
//      _DAT_004a5674 = _DAT_004a5bf4 = _DAT_004a6340 = _DAT_004a796c = 0.0f
//      &DAT_004e0f74 = ""  (empty string; 0050cfe0 byte at 0x4e0f74 == 0)
//

#include <bt.hpp>
#pragma hdrstop

#if !defined(MECH_HPP)
#	include <mech.hpp>		// Mech class -- owned by mech.cpp slice
#endif
#if !defined(MECHMPPR_HPP)
#	include <mechmppr.hpp>	// MechControlsMapper -- the leg channel's LIVE speed source
#endif
#if !defined(APP_HPP)
#	include <app.hpp>
#endif

//
// Speed comparisons in the original use a literal-pool 0.0f as a "moving at
// all" threshold; reverse/gimp cycle ratios are reflected through it so a
// backward cycle plays its clip with a positive increment.
//
static const Scalar ZeroSpeed = 0.0f;	// _DAT_004a5674 / 5bf4 / 6340 / 796c


//###########################################################################
//#####################   Mech gait animation enum   ########################
//###########################################################################
//
// Recovered verbatim from the 0x3c-byte-stride name table at .data:0050cfe8
// (used by the "Unsupported mech animation" assert).  This enum belongs in
// mech.hpp; reproduced here for clarity of the switches below.
//
enum MechAnimationState
{
	StandingAnimation				= 0x00,
	RightStandToWalkAnimation		= 0x01,
	RightWalkForwardAnimation		= 0x02,
	LeftWalkForwardAnimation		= 0x03,
	RightWalkToStandAnimation		= 0x04,
	LeftWalkToStandAnimation		= 0x05,
	RightWalkToRunAnimation			= 0x06,
	LeftWalkToRunAnimation			= 0x07,
	RightRunAnimation				= 0x08,
	LeftRunAnimation				= 0x09,
	RightRunToWalkAnimation			= 0x0a,
	LeftRunToWalkAnimation			= 0x0b,
	RightStandToReverseAnimation	= 0x0c,
	LeftStandToReverseAnimation		= 0x0d,
	RightReverseAnimation			= 0x0e,
	LeftReverseAnimation			= 0x0f,
	RightReverseToStandAnimation	= 0x10,
	LeftReverseToStandAnimation		= 0x11,
	LeftWalkToGimpAnimation			= 0x12,
	RightWalkToGimpAnimation		= 0x13,
	LeftGimpAnimation				= 0x14,
	RightGimpAnimation				= 0x15,
	LeftGimpToStandAnimation		= 0x16,
	RightGimpToStandAnimation		= 0x17,
	FallForwardAnimation			= 0x18,
	FallBackwardAnimation			= 0x19,
	FallLeftAnimation				= 0x1a,
	FallRightAnimation				= 0x1b,
	CrashAnimation					= 0x1c,
	// 0x1d-0x20 are valid clip slots (death variants) handled by the
	// "advance normally" group but are past the named table; AnimationCount
	// is the table sentinel at index 0x1d.
	AnimationCount					= 0x1d
};


//###########################################################################
//#################   Mech animation member map (offsets)   #################
//###########################################################################
//
// For the mech.hpp owner.  Names below are used in the bodies; "?" flags an
// uncertain semantic.  All are Scalar unless noted.
//
//   @0x18  actionRequestFlags   (Word)   pending-action bitfield (RequestActionFlags)
//   @0x40  movementMode         (int)    gait/death selector: 3=Run,4=Walk,5-8=fall/death? (?)
//   @0x128 controlSource        (ptr)    handle; *(*(this+0x128))+0x128 == commandedSpeed
//   @0x344 forwardCycleRate              leg/body speed slew rate (set from 0x5b8/0x5bc)
//   @0x348 legCycleSpeed                 channel A current cycle speed
//   @0x34c reverseStrideLength           clip length for the Reverse cycle
//   @0x350 gimpStrideLength              clip length for the Gimp cycle (stored negative)
//   @0x39c legStateAlarm        (AlarmIndicator)
//   @0x3b0 legAnimationState    (int)    == legStateAlarm.level
//   @0x52c gimpSpeedMax                  speed cap for the Gimp cycle (?)
//   @0x530 standSpeed                    "at rest" / minimum move speed threshold
//   @0x534 walkStrideLength              forward walk/run clip length (also speed cap)
//   @0x538 reverseSpeedMax               speed cap while decelerating into Reverse (?)
//   @0x53c jumpRunSpeedMax               airborne speed cap, movementMode==3
//   @0x540 jumpWalkSpeedMax              airborne speed cap, otherwise
//   @0x544 jumpRunStrideLength           airborne clip length, movementMode==3
//   @0x548 jumpWalkStrideLength          airborne clip length, otherwise
//   @0x598 motionEventName      (string) cleared to "" on fall/reset
//   @0x5a4 motionEventArmed     (int)    reset to 0 on fall/reset
//   @0x5a8 globalTimeScale               multiplies every clip increment
//   @0x5ac idleStrideScale               extra scale used only in the idle/transition group
//   @0x5b0 gimpCycleRate                 speed slew rate while in a Gimp cycle
//   @0x5cc animationClips[]     (ptr[])  clip handle per MechAnimationState (this+0x5cc + state*4)
//   @0x650 deathAnimationLatched(int)    one-shot latch for movementMode 5-8 death anims
//   @0x654 legResetLatch        (int)    cleared on fall/reset (channel A)
//   @0x658 bodyResetLatch       (int)    cleared on fall/reset (channel B)
//   @0x65c legAnimation         (SequenceController)
//   @0x6b4 bodyTargetSpeed               channel B target cycle speed
//   @0x6b8 bodyCycleSpeed                channel B current cycle speed
//   @0x6bc bodyAnimation        (SequenceController)
//   @0x714 bodyStateAlarm       (AlarmIndicator)
//   @0x728 bodyAnimationState   (int)    == bodyStateAlarm.level
//   @0x7a0 reverseSpeedMax2              speed cap while accelerating into Reverse (?)
//


//###########################################################################
//###########################################################################
//					Cluster helpers (attribution "?" in the decomp, but they
//					live inside the mech2 window and are required to read the
//					four methods).  BEST-EFFORT; the mech.cpp owner should
//					reconcile their final home.
//###########################################################################
//###########################################################################

//
// @004a7fc4 -- bind the channel-A (leg) SequenceController to the clip for
// 'state' and record the new state in the leg alarm.
//
void
	Mech::SetLegAnimation(int state)
{
	legAnimation.SelectSequence(					// FUN_004277a8(this+0x65c, ...)
		animationClips[state],						// *(this+0x5cc + state*4)
		// The real leg finished-callback PTR_LAB_0050d6f0 == FUN_004a6928
		// (resolved from .data + disassembled; == Mech::LegClipFinished below).
		// cbArg2/3 = 0 (the binary's DAT_0050d6f4/d6f8).
		(void *)&Mech::LegClipFinished, 0, 0);
	legStateAlarm.SetLevel(state);					// FUN_0041bbd8(this+0x39c, state)
}

//
// @004a800c -- channel-B (body) equivalent of SetLegAnimation.
//
void
	Mech::SetBodyAnimation(int state)
{
	bodyAnimation.SelectSequence(					// FUN_004277a8(this+0x6bc, ...)
		animationClips[state],
		// The real body finished-callback PTR_LAB_0050d6fc == FUN_004a6d8c (resolved from the
		// binary + reconstructed as Mech::BodyClipFinished): the gait-state TRANSITION + leg
		// alternation.  SequenceController::Advance calls it at end-of-clip; it re-arms the next
		// state's clip (with this same callback) + advances the carryover.  cbArg2/3 = 0 (the
		// binary's DAT_0050d700/704).
		(void *)&Mech::BodyClipFinished, 0, 0);
	bodyStateAlarm.SetLevel(state);					// FUN_0041bbd8(this+0x714, state)
}

//
// @004a4c54 -- OR action-request bits into actionRequestFlags.  A disabled
// mech may only request a restricted subset (mask 0xFE03).
//
void
	Mech::RequestActionFlags(Word bits)
{
	if (IsDisabled())								// FUN_0049fb54(this)
	{
		bits &= 0xFE03;
	}
	actionRequestFlags |= bits;						// this+0x18
}


//###########################################################################
//###########################################################################
//					BodyTransition / BodyClipFinished  (end-of-clip gait transitions)
//
// The real body finished-callback FUN_004a6d8c (== PTR_LAB_0050d6fc, resolved from the
// binary .data at 0x50d6fc).  SequenceController::Advance invokes it when a body clip
// finishes; it dispatches on bodyAnimationState (the jump table @0x4a6e0a), compares the
// commanded speed (bodyTargetSpeed) to the loaded caps (standSpeed/walkStrideLength/
// reverseSpeedMax) to pick the next gait state, re-arms it (SetBodyAnimation -> re-binds the
// clip with THIS same callback so the cycle keeps transitioning), and recursively advances
// the leftover (carryover) time -- returning the extra distance covered.  Reconstructed
// byte-for-byte from the disassembly (handlers 0x4a6f11 walk-R / 0x4a6e36 walk-L / 0x4a7041 /
// 0x4a6fc7 run, shared tail 0x4a6e66 / 0x4a6ed1 / 0x4a7001).
//###########################################################################
//###########################################################################

// Shared tail (0x4a6e66 etc.): bind the next state's clip, advance the carryover, return dist.
Scalar
	Mech::BodyTransition(int next_state, Scalar adv_time, int move_joints)
{
	SetBodyAnimation(next_state);						// call 0x4a800c
	return bodyAnimation.Advance(adv_time, move_joints);	// call 0x42790c
}

Scalar
	Mech::BodyClipFinished(Mech *m, unsigned /*a2*/, Scalar carryover, int mj)
{
	// airborne branch (movementMode 3/4 && jumpCapable) -> FUN_004a6344 (deferred; safe
	// no-op while grounded -- the test mech never jumps).
	if ((m->movementMode == 3 || m->movementMode == 4) && m->jumpCapable)
		return 0.0f;

	const Scalar fcr = m->forwardCycleRate;				// 0x344
	const Scalar gts = m->globalTimeScale;				// 0x5a8
	const Scalar cyc = m->bodyCycleSpeed;				// 0x6b8
	const Scalar tgt = m->bodyTargetSpeed;				// 0x6b4
	const Scalar Tscale = carryover * gts;				// 0x4a6e66 tail time

	switch (m->bodyAnimationState)						// 0x728 (jump table @0x4a6e0a)
	{
	// slot0 (0x4a71e9): standing / idle / reset-idle -- no transition, distance 0.
	case 0: case 1: case 22: case 23: case 24: case 25: case 26: case 27:
		return 0.0f;
	// state 2 (0x4a6fb1): bodyStateAlarm.SetLevel(1).
	case 2:
		m->bodyStateAlarm.SetLevel(1); return 0.0f;
	// slot9 state 4 (0x4a71d8) + slot2 (0x4a6f85) + state 32 (0x4a6f9b): SetLevel(0)
	// (transition-END clips fall back to standing).
	case 3: case 4: case 8: case 9: case 20: case 21:
	case 28: case 29: case 30: case 31: case 32:
		m->bodyStateAlarm.SetLevel(0); return 0.0f;

	// -- walk-R handler (0x4a6f11): states 5 (swr-end),6 (wwr),14 --
	case 5: case 6: case 14:
	{
		bool cont = (tgt >= m->standSpeed) || ((cyc - fcr * carryover) >= m->standSpeed);
		if (!cont)												// 0x4a6f11 -> next 9 (wsl, walk->stand)
			return m->BodyTransition(9, Tscale, mj);
		bool up = (tgt > m->walkStrideLength) && ((cyc + fcr * carryover) > m->walkStrideLength);
		if (up)													// 0x4a6f46 -> next 0xb (11, toward run)
			return m->BodyTransition(0xb, Tscale, mj);
		return m->BodyTransition(7, carryover * cyc * gts / m->walkStrideLength, mj);	// 0x4a6f7b alt -> wwl
	}
	// -- walk-L handler (0x4a6e36): states 7 (wwl),15 --
	case 7: case 15:
	{
		bool cont = (tgt >= m->standSpeed) || ((cyc - fcr * carryover) >= m->standSpeed);
		if (!cont)												// 0x4a6e36 -> next 8 (wsr)
			return m->BodyTransition(8, Tscale, mj);
		bool up = (tgt > m->walkStrideLength) && ((cyc + fcr * carryover) > m->walkStrideLength);
		if (up)													// 0x4a6e9a -> next 0xa (10)
			return m->BodyTransition(0xa, Tscale, mj);
		return m->BodyTransition(6, carryover * cyc * gts / m->walkStrideLength, mj);	// 0x4a6ecc alt -> wwr
	}
	// -- run/reverse-A handler (0x4a7041): states 10,12 --
	case 10: case 12:
	{
		bool cont = (tgt >= m->reverseSpeedMax) || ((cyc - fcr * carryover) >= m->reverseSpeedMax);
		if (!cont)												// 0x4a7041 -> next 0xf (15)
			return m->BodyTransition(0xf, Tscale, mj);
		return m->BodyTransition(0xd, carryover * cyc * gts / m->reverseStrideLength, mj);	// 0x4a7076 alt -> 13
	}
	// -- run/reverse-B handler (0x4a6fc7): states 11,13 --
	case 11: case 13:
	{
		bool cont = (tgt >= m->reverseSpeedMax) || ((cyc - fcr * carryover) >= m->reverseSpeedMax);
		if (!cont)												// 0x4a6fc7 -> next 0xe (14)
			return m->BodyTransition(0xe, Tscale, mj);
		return m->BodyTransition(0xc, carryover * cyc * gts / m->reverseStrideLength, mj);	// 0x4a6ffc alt -> 12
	}
	// -- REVERSE handlers (0x4a707d states 16,18 / 0x4a712c states 17,19): the body
	//    mirror of the leg's reverse cases (0x4a6c17/0x4a6cc4).  The earlier "gimp,
	//    not decoded -> fall back to standing" reading was wrong twice over: these
	//    are the REVERSE gait (16/17 = sbr/sbl entry, 18/19 = bbr/bbl back cycle,
	//    0x14/0x15 = bsr/bsl back->stand exit -- the slot map is binary-verified),
	//    and the stand fallback made the body loop stand->reverse-entry forever
	//    ("lingers in 16": slow reverse + the screwy backward->forward exit).
	//    The alt tails 0x70b2/0x7161 are the BodyTransition(0x13/0x12) cycle folds,
	//    by exact structural symmetry with the leg jump table (every previously
	//    decoded body case mirrors its leg twin).  While reversing (demand below
	//    gimpSpeedMax) the cycle alternates 0x12<->0x13; a forward demand exits
	//    through 0x15/0x14 (back->stand), then Standing self-arms the forward walk.
	case 16: case 18:
	{
		bool up = (tgt > m->gimpSpeedMax) && ((m->gimpCycleRate * carryover + cyc) > m->gimpSpeedMax);
		if (up)
			return m->BodyTransition(0x15, Tscale, mj);
		Scalar t = carryover * cyc * gts / m->gimpStrideLength;	// gimpStride stored NEGATIVE
		if (t <= 0.0f)	t = -t;									// sign fold (leg: 0x4a6c6e/0x4a6d3d)
		return m->BodyTransition(0x13, t, mj);
	}
	case 17: case 19:
	{
		bool up = (tgt > m->gimpSpeedMax) && ((m->gimpCycleRate * carryover + cyc) > m->gimpSpeedMax);
		if (up)
			return m->BodyTransition(0x14, Tscale, mj);
		Scalar t = carryover * cyc * gts / m->gimpStrideLength;
		if (t <= 0.0f)	t = -t;
		return m->BodyTransition(0x12, t, mj);
	}

	default:												// 0x4a71e9 (state > 0x20 or unmapped)
		return 0.0f;
	}
}

// Bring-up loop for the inline cutover path (BT_GAIT_CUTOVER without BT_GAIT_SM): re-arm the
// current body clip at frame 0 and advance the carryover (the SequenceController's own clip
// keeps playing).  NOT the authentic transition path -- that is BodyClipFinished above.
Scalar
	Mech::LoopBodyClip(Mech *m, unsigned /*a2*/, Scalar carryover, int move_joints)
{
	m->bodyAnimation.currentFrame   = 0;
	m->bodyAnimation.currentTime    = 0.0f;
	m->bodyAnimation.keyframeCursor = m->bodyAnimation.keyframeBase;
	return m->bodyAnimation.Advance(carryover, move_joints);
}


//###########################################################################
//###########################################################################
//					LegTransition / LegClipFinished  (LEG-channel end-of-clip)
//
// The real leg finished-callback FUN_004a6928 (== PTR_LAB_0050d6f0, resolved from
// the binary .data at 0x50d6f0 and capstone-disassembled: jump table byte idx
// @0x4a6989, dword targets @0x4a69aa -- the same 33-state shape as the body's).
// Differences from BodyClipFinished, all verified in the disassembly:
//   - the speed compared is the LIVE commanded speed *(subsystemArray[0])+0x128
//     == the controls mapper's speedDemand (typed mirror: controlsMapper), not
//     the snapshot bodyTargetSpeed;
//   - the cycle speed slewed/scaled is legCycleSpeed@0x348 (not bodyCycleSpeed);
//   - re-arm via SetLegAnimation + legAnimation.Advance (0x65c, alarm@0x39c);
//   - the GIMP cycle alternates 0x12<->0x13 with |ratio| (gimpStrideLength is
//     stored negative; the 0x4a6c6e sign fold takes the magnitude).
//###########################################################################
//###########################################################################

// Shared tail (0x4a6a06 / 0x4a6a6f / 0x4a6b9d): bind next state's clip, advance carryover.
Scalar
	Mech::LegTransition(int next_state, Scalar adv_time, int move_joints)
{
	SetLegAnimation(next_state);						// call 0x4a7fc4
	return legAnimation.Advance(adv_time, move_joints);	// call 0x42790c
}

Scalar
	Mech::LegClipFinished(Mech *m, unsigned /*a2*/, Scalar carryover, int mj)
{
	// airborne branch (movementMode 3/4 && jumpCapable) -> FUN_004a7970 (deferred).
	if ((m->movementMode == 3 || m->movementMode == 4) && m->jumpCapable)
		return 0.0f;

	// The binary reads edx = *(mech+0x128) then [edx]+0x128: subsystemArray[0]
	// (the roster's ControlsMapper slot) -> speedDemand.  controlsMapper is the
	// typed mirror of that slot; null (no mapper) reads demand 0 -> the mech idles.
	const Scalar spd = (m->controlsMapper != 0) ? m->controlsMapper->speedDemand : 0.0f;
	const Scalar fcr = m->forwardCycleRate;				// 0x344
	const Scalar gts = m->globalTimeScale;				// 0x5a8
	const Scalar cyc = m->legCycleSpeed;				// 0x348
	const Scalar T   = carryover * gts;					// 0x4a6a06 tail time

	switch (m->legAnimationState)						// 0x3b0 (jump table @0x4a69aa)
	{
	// slot0 (0x4a6d7f): standing / idle -- no transition, distance 0.
	case 0: case 1: case 22: case 23: case 24: case 25: case 26: case 27:
		return 0.0f;
	// slot10 state 2 (0x4a6b37): SetLevel(1).
	case 2:
		m->legStateAlarm.SetLevel(1); return 0.0f;
	// slot2 (0x4a6b21) + slot1 state 32 (0x4a6b4d) + slot9 state 4 (0x4a6d6e): SetLevel(0).
	case 3: case 4: case 8: case 9: case 20: case 21:
	case 28: case 29: case 30: case 31: case 32:
		m->legStateAlarm.SetLevel(0); return 0.0f;

	// -- walk-R handler (0x4a6aad): states 5,6,14 --
	case 5: case 6: case 14:
	{
		bool cont = (spd >= m->standSpeed) || ((cyc - fcr * carryover) >= m->standSpeed);
		if (!cont)											// -> 9 (walk->stand L)
			return m->LegTransition(9, T, mj);
		bool up = (spd > m->walkStrideLength) && ((cyc + fcr * carryover) > m->walkStrideLength);
		if (up)												// 0x4a6ae2 -> 0xb (toward run)
			return m->LegTransition(0xb, T, mj);
		return m->LegTransition(7, carryover * cyc * gts / m->walkStrideLength, mj);	// alt -> walk-L
	}
	// -- walk-L handler (0x4a69d6): states 7,15 --
	case 7: case 15:
	{
		bool cont = (spd >= m->standSpeed) || ((cyc - fcr * carryover) >= m->standSpeed);
		if (!cont)											// -> 8 (walk->stand R)
			return m->LegTransition(8, T, mj);
		bool up = (spd > m->walkStrideLength) && ((cyc + fcr * carryover) > m->walkStrideLength);
		if (up)												// 0x4a6a38 -> 0xa
			return m->LegTransition(0xa, T, mj);
		return m->LegTransition(6, carryover * cyc * gts / m->walkStrideLength, mj);	// alt -> walk-R
	}
	// -- run handler (0x4a6bdb): states 10,12 --
	case 10: case 12:
	{
		bool cont = (spd >= m->reverseSpeedMax) || ((cyc - fcr * carryover) >= m->reverseSpeedMax);
		if (!cont)											// -> 0xf (15)
			return m->LegTransition(0xf, T, mj);
		return m->LegTransition(0xd, carryover * cyc * gts / m->reverseStrideLength, mj);	// alt -> 13
	}
	// -- run handler (0x4a6b63): states 11,13 --
	case 11: case 13:
	{
		bool cont = (spd >= m->reverseSpeedMax) || ((cyc - fcr * carryover) >= m->reverseSpeedMax);
		if (!cont)											// -> 0xe (14)
			return m->LegTransition(0xe, T, mj);
		return m->LegTransition(0xc, carryover * cyc * gts / m->reverseStrideLength, mj);	// alt -> 12
	}
	// -- gimp handler (0x4a6c17): states 16,18 -> alt 0x13; up 0x15 --
	case 16: case 18:
	{
		bool up = (spd > m->gimpSpeedMax) && ((m->gimpCycleRate * carryover + cyc) > m->gimpSpeedMax);
		if (up)
			return m->LegTransition(0x15, T, mj);
		Scalar t = carryover * cyc * gts / m->gimpStrideLength;	// gimpStride stored NEGATIVE
		if (t <= 0.0f)	t = -t;								// 0x4a6c6e/0x4a6d3d sign fold
		return m->LegTransition(0x13, t, mj);
	}
	// -- gimp handler (0x4a6cc4): states 17,19 -> alt 0x12; up 0x14 --
	case 17: case 19:
	{
		bool up = (spd > m->gimpSpeedMax) && ((m->gimpCycleRate * carryover + cyc) > m->gimpSpeedMax);
		if (up)
			return m->LegTransition(0x14, T, mj);
		Scalar t = carryover * cyc * gts / m->gimpStrideLength;
		if (t <= 0.0f)	t = -t;
		return m->LegTransition(0x12, t, mj);
	}

	default:											// state > 0x20 / unmapped
		return 0.0f;
	}
}


//###########################################################################
//###########################################################################
//					AdvanceLegAnimation  (channel A, ground)
//
// @004a5028  (MECH2.CPP:0xD3, 0x13B)
//
// Per-frame update of the locally-simulated leg gait.  Reads the live
// commanded speed from the controls subsystem, slews legCycleSpeed toward it,
// drives the walk/run/reverse/gimp state machine, advances the leg clip and
// returns the cycle distance covered this frame.
//###########################################################################
//###########################################################################
Scalar
	Mech::AdvanceLegAnimation(Scalar time_slice)
{
	// commandedSpeed = *(*(this+0x128)) + 0x128 in the binary: subsystemArray[0]
	// (the roster's ControlsMapper slot) -> speedDemand, read LIVE each frame.
	// RECONCILED: the old draft double-deref'd the never-initialized controlSource
	// alias (an AV); controlsMapper is the typed mirror of roster slot 0.  A mech
	// with no mapper reads demand 0 (idles) -- matching a zeroed binary roster.
	Scalar		 commandedSpeed	=
		(controlsMapper != 0) ? controlsMapper->speedDemand : 0.0f;
	Scalar		 distance		= 0.0f;

	// binary: legAnimationState@0x3b0 IS legStateAlarm's level (one field; the
	// recon split them) -- re-sync so SetLegAnimation's level reaches the switch.
	legAnimationState = (int)legStateAlarm.GetLevel();

	//
	// One-shot: when movementMode selects a death/fall (5..8), latch the
	// matching crash clip (0x1c..0x1f) exactly once.
	//
	if (!deathAnimationLatched)
	{
		switch (movementMode)							// this+0x40
		{
		case 5:	SetLegAnimation(0x1c); deathAnimationLatched = 1; break;
		case 6:	SetLegAnimation(0x1d); deathAnimationLatched = 1; break;
		case 7:	SetLegAnimation(0x1e); deathAnimationLatched = 1; break;
		case 8:	SetLegAnimation(0x1f); deathAnimationLatched = 1; break;
		}
	}

	//
	// Once the leg cycle has wound down (legCycleSpeed <= 0) while in a
	// run/run-to-walk transition, drop the gait alarm to "standing".
	//
	{
		int state = legAnimationState;					// this+0x3b0
		if (legCycleSpeed <= ZeroSpeed					// this+0x348
			&& (state == 6 || state == 7 || state == 8 || state == 9))
		{
			legStateAlarm.SetLevel(0);					// FUN_0041bbd8(this+0x39c,0)
			legResetLatch = 1;							// this+0x654
		}
	}

	switch (legAnimationState)							// this+0x3b0
	{
	case StandingAnimation:								// 0
		// RAW (part_012.c FUN_004a5028 case 0): standSpeed < commandedSpeed ->
		// begin WALKING (state 5); 0 <= commanded < standSpeed -> stay standing;
		// commanded < 0 -> stand-to-reverse (0x10).  (The earlier draft had the
		// first comparison INVERTED -> a commanded mech never left Standing.)
		if (standSpeed < commandedSpeed)				// this+0x530 < live demand
		{
			SetLegAnimation(5);							// stand -> walk
		}
		else
		{
			distance = 0.0f;
			// TURN-IN-PLACE entry (bring-up TRIGGER, real machinery): the trn
			// clip/state-4 advance/exit are binary-verified, but the authentic
			// entry DISPATCHER isn't yet located in the decomp -- enter from
			// Standing on a live turn demand with no speed demand.  The clip's
			// finished-callback drops back to stand; while the demand persists
			// this trigger re-arms it (a looping pivot step).
			if (hasCrashSet != 0 && controlsMapper != 0
				&& ZeroSpeed <= commandedSpeed
				&& (controlsMapper->turnDemand > 0.05f
					|| controlsMapper->turnDemand < -0.05f))
			{
				SetLegAnimation(4);						// turn-in-place (trn)
				goto advance_normally;
			}
			if (ZeroSpeed <= commandedSpeed)
			{
				break;									// truly at rest
			}
			SetLegAnimation(0x10);						// reverse entry
		}
		// FALLTHROUGH into the "advance normally" group (state has just been
		// changed away from Standing by the setters above).

	case 2: case 3: case 5: case 8: case 9: case 10: case 0x0b:
	case 0x0e: case 0x0f: case 0x10: case 0x11: case 0x14: case 0x15:
	case 0x1c: case 0x1d: case 0x1e: case 0x1f: case 0x20:
	advance_normally:
		//
		// Standing must never reach here -- the fallthrough above always
		// retargets the alarm first.
		//
		if (legAnimationState == StandingAnimation)
		{
			Verify(False, "Standing Not Supported",
				"d:\\tesla\\bt\\bt\\MECH2.CPP", 0xD3);
		}
		distance = legAnimation.Advance(				// FUN_0042790c(this+0x65c, ...)
			time_slice * globalTimeScale * idleStrideScale,	// 0x5a8 * 0x5ac
			1);
		legCycleSpeed = distance / time_slice;			// this+0x348
		break;

	case 1:
		distance = 0.0f;
		break;

	case 4:												// TURN-IN-PLACE (trn clip)
		// BINARY-VERIFIED (raw FUN_004a5028 case 4, part_012.c:12013): state 4 is
		// the turn-in-place animation (animationClips[4] = the "trn" clip, loaded
		// under turnCapable@0x588).  It EXITS to stand when a real speed is
		// commanded (standSpeed < demand -- the earlier draft had this comparison
		// INVERTED, the same bug class as the Standing case) or when reverse is
		// commanded; otherwise it ADVANCES the turn clip while the mech pivots.
		if (standSpeed < commandedSpeed)
		{
			legStateAlarm.SetLevel(0);
			RequestActionFlags(8);						// FUN_004a4c54(this,8)
			break;
		}
		distance = 0.0f;
		if (commandedSpeed < ZeroSpeed)
		{
			legStateAlarm.SetLevel(0);
			RequestActionFlags(8);
			break;
		}
		goto advance_normally;

	case 6:	case 7:										// WalkToRun
		//
		// Slew legCycleSpeed toward commandedSpeed at forwardCycleRate,
		// clamped into [standSpeed .. walkStrideLength].
		//
		if (commandedSpeed <= legCycleSpeed)
		{
			if (commandedSpeed < legCycleSpeed)
			{
				legCycleSpeed -= forwardCycleRate * time_slice;	// 0x344
				if (legCycleSpeed < commandedSpeed)
				{
					legCycleSpeed = commandedSpeed;
				}
				if (legCycleSpeed < standSpeed)			// 0x530
				{
					legCycleSpeed = standSpeed;
				}
			}
		}
		else
		{
			legCycleSpeed += forwardCycleRate * time_slice;
			if (legCycleSpeed > commandedSpeed)
			{
				legCycleSpeed = commandedSpeed;
			}
			if (legCycleSpeed > walkStrideLength)		// 0x534 (used as cap)
			{
				legCycleSpeed = walkStrideLength;
			}
		}
		distance = legAnimation.Advance(
			time_slice * (legCycleSpeed / walkStrideLength) * globalTimeScale,
			1);
		break;

	case 0x0c: case 0x0d:								// StandToReverse
		if (commandedSpeed <= legCycleSpeed)
		{
			if (commandedSpeed < legCycleSpeed)
			{
				legCycleSpeed -= forwardCycleRate * time_slice;
				if (legCycleSpeed < commandedSpeed)
				{
					legCycleSpeed = commandedSpeed;
				}
				if (legCycleSpeed < reverseSpeedMax)	// 0x538
				{
					legCycleSpeed = reverseSpeedMax;
				}
			}
		}
		else
		{
			legCycleSpeed += forwardCycleRate * time_slice;
			if (legCycleSpeed > commandedSpeed)
			{
				legCycleSpeed = commandedSpeed;
			}
			if (legCycleSpeed > reverseSpeedMax2)		// 0x7a0
			{
				legCycleSpeed = reverseSpeedMax2;
			}
		}
		distance = legAnimation.Advance(
			time_slice * (legCycleSpeed / reverseStrideLength) * globalTimeScale,	// 0x34c
			1);
		break;

	case 0x12: case 0x13:								// WalkToGimp
		if (commandedSpeed <= legCycleSpeed)
		{
			if (commandedSpeed < legCycleSpeed)
			{
				legCycleSpeed -= gimpCycleRate * time_slice;	// 0x5b0
				if (legCycleSpeed < commandedSpeed)
				{
					legCycleSpeed = commandedSpeed;
				}
				if (legCycleSpeed < gimpStrideLength)	// 0x350
				{
					legCycleSpeed = gimpStrideLength;
				}
			}
		}
		else
		{
			legCycleSpeed += gimpCycleRate * time_slice;
			if (legCycleSpeed > commandedSpeed)
			{
				legCycleSpeed = commandedSpeed;
			}
			if (legCycleSpeed > gimpSpeedMax)			// 0x52c
			{
				legCycleSpeed = gimpSpeedMax;
			}
		}
		{
			// gimpStrideLength is stored negative; reflect the ratio through
			// ZeroSpeed so the clip plays with a positive increment.
			Scalar ratio = legCycleSpeed / gimpStrideLength;	// 0x350
			if (ratio <= ZeroSpeed)
			{
				ratio = -ratio;
			}
			distance = legAnimation.Advance(
				ratio * time_slice * globalTimeScale, 1);
		}
		break;

	case 0x16: case 0x17: case 0x18: case 0x19: case 0x1a: case 0x1b:
		//
		// Gimp-to-stand and the four fall directions: terminal poses.  Clear
		// the motion event, drop both reset latches, reset the leg clip.
		//
		Assign(this->motionEventName, "");				// FUN_00408440(this+0x598, &DAT_004e0f74)
		motionEventArmed		= 0;					// this+0x5a4
		legResetLatch			= 0;					// this+0x654
		deathAnimationLatched	= 0;					// this+0x650
		legStateAlarm.SetLevel(0);						// this+0x39c
		legAnimation.Reset(1);							// FUN_004283b8(this+0x65c, 1)
		break;

	default:
		// name table @0050cfe8, 0x3c-byte stride, indexed by state
		DebugStream << (AnimationNames + legAnimationState * 0x3c);
		DebugStream.Emit();
		Verify(False, "Unsupported mech animation!",
			"d:\\tesla\\bt\\bt\\MECH2.CPP", 0x13B);
	}

	return distance;
}


//###########################################################################
//###########################################################################
//					AdvanceBodyAnimation  (channel B, ground)
//
// @004a5678  (MECH2.CPP:0x206)
//
// Channel-B counterpart used by Mech::IntegrateMotion to advance the world
// transform.  Identical state machine to AdvanceLegAnimation except it slews
// bodyCycleSpeed toward the snapshot bodyTargetSpeed (not the live controls
// value), takes an explicit loop flag, and has no death-latch / "Standing Not
// Supported" guard.
//###########################################################################
//###########################################################################
Scalar
	Mech::AdvanceBodyAnimation(Scalar time_slice, int loop)
{
	Scalar distance = 0.0f;

	// In the binary `bodyAnimationState`@0x728 IS `bodyStateAlarm`'s level (one field);
	// the reconstruction split them, so SetBodyAnimation's `bodyStateAlarm.SetLevel(state)`
	// would not update the int the switch reads.  Re-sync from the alarm each frame (the
	// switch below dispatches on the pre-transition state exactly as the binary does).
	bodyAnimationState = (int)bodyStateAlarm.GetLevel();

	if (!deathAnimationLatched)							// this+0x650
	{
		switch (movementMode)							// this+0x40
		{
		case 5:	SetBodyAnimation(0x1c); deathAnimationLatched = 1; break;
		case 6:	SetBodyAnimation(0x1d); deathAnimationLatched = 1; break;
		case 7:	SetBodyAnimation(0x1e); deathAnimationLatched = 1; break;
		case 8:	SetBodyAnimation(0x1f); deathAnimationLatched = 1; break;
		}
	}

	switch (bodyAnimationState)							// this+0x728
	{
	case StandingAnimation:								// 0
		// RAW (FUN_004a5678 case 0): standSpeed < bodyTargetSpeed -> begin WALKING
		// (5); 0 <= target < standSpeed -> stay standing; target < 0 -> reverse
		// (0x10).  (The earlier draft had the comparison INVERTED.)
		distance = 0.0f;
		if (standSpeed < bodyTargetSpeed)				// 0x530 < 0x6b4
		{
			SetBodyAnimation(5);
		}
		else
		{
			if (ZeroSpeed <= bodyTargetSpeed)
			{
				break;
			}
			SetBodyAnimation(0x10);
		}
		// FALLTHROUGH

	case 2: case 3: case 4: case 5: case 8: case 9: case 10: case 0x0b:
	case 0x0e: case 0x0f: case 0x10: case 0x11: case 0x14: case 0x15:
	case 0x1c: case 0x1d: case 0x1e: case 0x1f: case 0x20:
		distance = bodyAnimation.Advance(				// FUN_0042790c(this+0x6bc, ...)
			time_slice * globalTimeScale * idleStrideScale, loop);
		bodyCycleSpeed = distance / time_slice;			// this+0x6b8
		break;

	case 1:
		distance = 0.0f;
		break;

	case 6: case 7:										// WalkToRun
		if (bodyTargetSpeed <= bodyCycleSpeed)
		{
			if (bodyTargetSpeed < bodyCycleSpeed)
			{
				bodyCycleSpeed -= forwardCycleRate * time_slice;
				if (bodyCycleSpeed < bodyTargetSpeed)	bodyCycleSpeed = bodyTargetSpeed;
				if (bodyCycleSpeed < standSpeed)		bodyCycleSpeed = standSpeed;
			}
		}
		else
		{
			bodyCycleSpeed += forwardCycleRate * time_slice;
			if (bodyCycleSpeed > bodyTargetSpeed)		bodyCycleSpeed = bodyTargetSpeed;
			if (bodyCycleSpeed > walkStrideLength)		bodyCycleSpeed = walkStrideLength;
		}
		distance = bodyAnimation.Advance(
			time_slice * (bodyCycleSpeed / walkStrideLength) * globalTimeScale, loop);
		break;

	case 0x0c: case 0x0d:								// StandToReverse
		if (bodyTargetSpeed <= bodyCycleSpeed)
		{
			if (bodyTargetSpeed < bodyCycleSpeed)
			{
				bodyCycleSpeed -= forwardCycleRate * time_slice;
				if (bodyCycleSpeed < bodyTargetSpeed)	bodyCycleSpeed = bodyTargetSpeed;
				if (bodyCycleSpeed < reverseSpeedMax)	bodyCycleSpeed = reverseSpeedMax;
			}
		}
		else
		{
			bodyCycleSpeed += forwardCycleRate * time_slice;
			if (bodyCycleSpeed > bodyTargetSpeed)		bodyCycleSpeed = bodyTargetSpeed;
			if (bodyCycleSpeed > reverseSpeedMax2)		bodyCycleSpeed = reverseSpeedMax2;
		}
		distance = bodyAnimation.Advance(
			time_slice * (bodyCycleSpeed / reverseStrideLength) * globalTimeScale, loop);
		break;

	case 0x12: case 0x13:								// WalkToGimp
		if (bodyTargetSpeed <= bodyCycleSpeed)
		{
			if (bodyTargetSpeed < bodyCycleSpeed)
			{
				bodyCycleSpeed -= gimpCycleRate * time_slice;
				if (bodyCycleSpeed < bodyTargetSpeed)	bodyCycleSpeed = bodyTargetSpeed;
				if (bodyCycleSpeed < gimpStrideLength)	bodyCycleSpeed = gimpStrideLength;
			}
		}
		else
		{
			bodyCycleSpeed += gimpCycleRate * time_slice;
			if (bodyCycleSpeed > bodyTargetSpeed)		bodyCycleSpeed = bodyTargetSpeed;
			if (bodyCycleSpeed > gimpSpeedMax)			bodyCycleSpeed = gimpSpeedMax;
		}
		{
			Scalar ratio = bodyCycleSpeed / gimpStrideLength;
			if (ratio <= ZeroSpeed)	ratio = -ratio;
			distance = bodyAnimation.Advance(
				ratio * time_slice * globalTimeScale, loop);
		}
		break;

	case 0x16: case 0x17: case 0x18: case 0x19: case 0x1a: case 0x1b:
		Assign(this->motionEventName, "");				// FUN_00408440(this+0x598, "")
		motionEventArmed		= 0;					// this+0x5a4
		bodyResetLatch			= 0;					// this+0x658
		deathAnimationLatched	= 0;					// this+0x650
		bodyStateAlarm.SetLevel(0);						// this+0x714
		bodyAnimation.Reset(loop);						// FUN_004283b8(this+0x6bc, loop)
		break;

	default:
		DebugStream << (AnimationNames + bodyAnimationState * 0x3c);
		DebugStream.Emit();
		Verify(False, "Unsupported mech animation!",
			"d:\\tesla\\bt\\bt\\MECH2.CPP", 0x206);
	}

	return distance;
}


//###########################################################################
//###########################################################################
//					AdvanceBodyAnimationAirborne  (channel B, jump-capable)
//
// @004a5bf8  (MECH2.CPP:0x2E8)
//
// Airborne variant of AdvanceBodyAnimation selected by Mech::IntegrateMotion
// when jump jets are active.  Adds a pre-clamp of bodyTargetSpeed to the jump
// speed limits and handles the FallForward / FallBackward jump cycles
// (states 0x18/0x19); gimp-to-stand (0x16/0x17) and the lateral falls
// (0x1a/0x1b) join the normal advance group here rather than resetting.
//###########################################################################
//###########################################################################
Scalar
	Mech::AdvanceBodyAnimationAirborne(Scalar time_slice, int loop)
{
	Scalar distance = 0.0f;

	//
	// While in any forward/reverse/gimp *moving* cycle, clamp the target to
	// the jump speed limit for the current gait, then floor at zero.
	//
	{
		int state = bodyAnimationState;					// this+0x728
		if ((unsigned)(state - 6) < 2 || (unsigned)(state - 0x0c) < 2
			|| (unsigned)(state - 0x12) < 2)
		{
			if (movementMode == 3)						// run jump
			{
				if (bodyTargetSpeed > jumpRunSpeedMax)	bodyTargetSpeed = jumpRunSpeedMax;	// 0x53c
			}
			else										// walk jump
			{
				if (bodyTargetSpeed > jumpWalkSpeedMax)	bodyTargetSpeed = jumpWalkSpeedMax;	// 0x540
			}
			if (bodyTargetSpeed < ZeroSpeed)	bodyTargetSpeed = ZeroSpeed;
		}
	}

	switch (bodyAnimationState)
	{
	case StandingAnimation:								// 0
		if (bodyTargetSpeed <= standSpeed)				// 0x6b4 <= 0x530
		{
			distance = 0.0f;
			break;
		}
		SetBodyAnimation(5);
		// FALLTHROUGH

	case 2: case 3: case 4: case 5: case 8: case 9: case 10: case 0x0b:
	case 0x0e: case 0x0f: case 0x10: case 0x11: case 0x14: case 0x15:
	case 0x16: case 0x17: case 0x1a: case 0x1b: case 0x20:
		distance = bodyAnimation.Advance(
			time_slice * globalTimeScale * idleStrideScale, loop);
		bodyCycleSpeed = distance / time_slice;
		break;

	case 1:
		distance = 0.0f;
		break;

	case 6: case 7:										// WalkToRun
		if (bodyTargetSpeed <= bodyCycleSpeed)
		{
			if (bodyTargetSpeed < bodyCycleSpeed)
			{
				bodyCycleSpeed -= forwardCycleRate * time_slice;
				if (bodyCycleSpeed < bodyTargetSpeed)	bodyCycleSpeed = bodyTargetSpeed;
				if (bodyCycleSpeed < standSpeed)		bodyCycleSpeed = standSpeed;
			}
		}
		else
		{
			bodyCycleSpeed += forwardCycleRate * time_slice;
			if (bodyCycleSpeed > bodyTargetSpeed)		bodyCycleSpeed = bodyTargetSpeed;
			if (bodyCycleSpeed > walkStrideLength)		bodyCycleSpeed = walkStrideLength;
		}
		distance = bodyAnimation.Advance(
			time_slice * (bodyCycleSpeed / walkStrideLength) * globalTimeScale, loop);
		break;

	case 0x0c: case 0x0d:								// StandToReverse
		if (bodyTargetSpeed <= bodyCycleSpeed)
		{
			if (bodyTargetSpeed < bodyCycleSpeed)
			{
				bodyCycleSpeed -= forwardCycleRate * time_slice;
				if (bodyCycleSpeed < bodyTargetSpeed)	bodyCycleSpeed = bodyTargetSpeed;
				if (bodyCycleSpeed < reverseSpeedMax)	bodyCycleSpeed = reverseSpeedMax;
			}
		}
		else
		{
			bodyCycleSpeed += forwardCycleRate * time_slice;
			if (bodyCycleSpeed > bodyTargetSpeed)		bodyCycleSpeed = bodyTargetSpeed;
			if (bodyCycleSpeed > reverseSpeedMax2)		bodyCycleSpeed = reverseSpeedMax2;
		}
		distance = bodyAnimation.Advance(
			time_slice * (bodyCycleSpeed / reverseStrideLength) * globalTimeScale, loop);
		break;

	case 0x12: case 0x13:								// WalkToGimp
		if (bodyTargetSpeed <= bodyCycleSpeed)
		{
			if (bodyTargetSpeed < bodyCycleSpeed)
			{
				bodyCycleSpeed -= gimpCycleRate * time_slice;
				if (bodyCycleSpeed < bodyTargetSpeed)	bodyCycleSpeed = bodyTargetSpeed;
				if (bodyCycleSpeed < gimpStrideLength)	bodyCycleSpeed = gimpStrideLength;
			}
		}
		else
		{
			bodyCycleSpeed += gimpCycleRate * time_slice;
			if (bodyCycleSpeed > bodyTargetSpeed)		bodyCycleSpeed = bodyTargetSpeed;
			if (bodyCycleSpeed > gimpSpeedMax)			bodyCycleSpeed = gimpSpeedMax;
		}
		{
			Scalar ratio = bodyCycleSpeed / gimpStrideLength;
			if (ratio <= ZeroSpeed)	ratio = -ratio;
			distance = bodyAnimation.Advance(
				ratio * time_slice * globalTimeScale, loop);
		}
		break;

	case 0x18: case 0x19:								// FallForward / FallBackward (jump)
		{
			Scalar ratio;
			if (movementMode == 3)						// run jump: caps 0x53c / 0x544
			{
				if (bodyTargetSpeed <= bodyCycleSpeed)
				{
					if (bodyTargetSpeed < bodyCycleSpeed)
					{
						bodyCycleSpeed -= forwardCycleRate * time_slice;
						if (bodyCycleSpeed < bodyTargetSpeed)	bodyCycleSpeed = bodyTargetSpeed;
						if (bodyCycleSpeed < jumpRunSpeedMax)	bodyCycleSpeed = jumpRunSpeedMax;	// 0x53c
					}
				}
				else
				{
					bodyCycleSpeed += forwardCycleRate * time_slice;
					if (bodyCycleSpeed > bodyTargetSpeed)		bodyCycleSpeed = bodyTargetSpeed;
					if (bodyCycleSpeed > jumpRunStrideLength)	bodyCycleSpeed = jumpRunStrideLength;	// 0x544
				}
				ratio = bodyCycleSpeed / jumpRunStrideLength;	// 0x544
			}
			else										// walk jump: caps 0x540 / 0x548
			{
				if (bodyTargetSpeed <= bodyCycleSpeed)
				{
					if (bodyTargetSpeed < bodyCycleSpeed)
					{
						bodyCycleSpeed -= forwardCycleRate * time_slice;
						if (bodyCycleSpeed < bodyTargetSpeed)	bodyCycleSpeed = bodyTargetSpeed;
						if (bodyCycleSpeed < jumpWalkSpeedMax)	bodyCycleSpeed = jumpWalkSpeedMax;	// 0x540
					}
				}
				else
				{
					bodyCycleSpeed += forwardCycleRate * time_slice;
					if (bodyCycleSpeed > bodyTargetSpeed)		bodyCycleSpeed = bodyTargetSpeed;
					if (bodyCycleSpeed > jumpWalkStrideLength)	bodyCycleSpeed = jumpWalkStrideLength;	// 0x548
				}
				ratio = bodyCycleSpeed / jumpWalkStrideLength;	// 0x548
			}
			distance = bodyAnimation.Advance(
				time_slice * ratio * globalTimeScale, loop);
		}
		break;

	default:
		DebugStream << (AnimationNames + bodyAnimationState * 0x3c);
		DebugStream.Emit();
		Verify(False, "Unsupported mech animation!",
			"d:\\tesla\\bt\\bt\\MECH2.CPP", 0x2E8);
	}

	return distance;
}


//###########################################################################
//###########################################################################
//					AdvanceLegAnimationAirborne  (channel A, jump-capable)
//
// @004a71f4  (MECH2.CPP:0x672)
//
// Airborne variant of AdvanceLegAnimation.  Like the ground version it reads
// the live commanded speed from the controls subsystem, but here it also
// CLAMPS that source value to the jump speed limit (writing it back), and
// handles the FallForward / FallBackward jump cycles (0x18/0x19).  No death
// latch / footstep block; gimp-to-stand and lateral falls join the normal
// advance group.
//###########################################################################
//###########################################################################
Scalar
	Mech::AdvanceLegAnimationAirborne(Scalar time_slice)
{
	ReconMotionSource	*motionSource	= *(ReconMotionSource **)(this->controlSource);	// **(this+0x128)
	Scalar		 distance		= 0.0f;
	int			 mode			= movementMode;							// this+0x40
	int			 state			= legAnimationState;					// this+0x3b0

	//
	// Clamp the source commandedSpeed (motionSource->commandedSpeed, +0x128)
	// to the jump speed cap while in a moving cycle, then floor at zero.
	//
	if ((unsigned)(state - 6) < 2 || (unsigned)(state - 0x0c) < 2
		|| (unsigned)(state - 0x12) < 2)
	{
		if (mode == 3)
		{
			if (motionSource->commandedSpeed > jumpRunSpeedMax)
				motionSource->commandedSpeed = jumpRunSpeedMax;			// 0x53c
		}
		else
		{
			if (motionSource->commandedSpeed > jumpWalkSpeedMax)
				motionSource->commandedSpeed = jumpWalkSpeedMax;		// 0x540
		}
		if (motionSource->commandedSpeed < ZeroSpeed)
			motionSource->commandedSpeed = ZeroSpeed;
	}

	switch (legAnimationState)
	{
	case StandingAnimation:								// 0
		if (motionSource->commandedSpeed <= standSpeed)	// +0x128 <= 0x530
		{
			distance = 0.0f;
			break;
		}
		SetLegAnimation(5);
		// FALLTHROUGH

	case 2: case 3: case 5: case 8: case 9: case 10: case 0x0b:
	case 0x0e: case 0x0f: case 0x10: case 0x11: case 0x14: case 0x15:
	case 0x16: case 0x17: case 0x1a: case 0x1b: case 0x20:
	advance_normally:
		distance = legAnimation.Advance(
			time_slice * globalTimeScale * idleStrideScale, 1);
		legCycleSpeed = distance / time_slice;
		break;

	case 1:
		distance = 0.0f;
		break;

	case 4:												// WalkToStand
		if (standSpeed < motionSource->commandedSpeed)
		{
			legStateAlarm.SetLevel(0);
			RequestActionFlags(8);
			break;
		}
		goto advance_normally;

	case 6: case 7:										// WalkToRun
		if (motionSource->commandedSpeed <= legCycleSpeed)
		{
			if (motionSource->commandedSpeed < legCycleSpeed)
			{
				legCycleSpeed -= forwardCycleRate * time_slice;
				if (legCycleSpeed < motionSource->commandedSpeed)	legCycleSpeed = motionSource->commandedSpeed;
				if (legCycleSpeed < standSpeed)						legCycleSpeed = standSpeed;
			}
		}
		else
		{
			legCycleSpeed += forwardCycleRate * time_slice;
			if (legCycleSpeed > motionSource->commandedSpeed)		legCycleSpeed = motionSource->commandedSpeed;
			if (legCycleSpeed > walkStrideLength)					legCycleSpeed = walkStrideLength;
		}
		distance = legAnimation.Advance(
			time_slice * (legCycleSpeed / walkStrideLength) * globalTimeScale, 1);
		break;

	case 0x0c: case 0x0d:								// StandToReverse
		if (motionSource->commandedSpeed <= legCycleSpeed)
		{
			if (motionSource->commandedSpeed < legCycleSpeed)
			{
				legCycleSpeed -= forwardCycleRate * time_slice;
				if (legCycleSpeed < motionSource->commandedSpeed)	legCycleSpeed = motionSource->commandedSpeed;
				if (legCycleSpeed < reverseSpeedMax)				legCycleSpeed = reverseSpeedMax;
			}
		}
		else
		{
			legCycleSpeed += forwardCycleRate * time_slice;
			if (legCycleSpeed > motionSource->commandedSpeed)		legCycleSpeed = motionSource->commandedSpeed;
			if (legCycleSpeed > reverseSpeedMax2)					legCycleSpeed = reverseSpeedMax2;
		}
		distance = legAnimation.Advance(
			time_slice * (legCycleSpeed / reverseStrideLength) * globalTimeScale, 1);
		break;

	case 0x12: case 0x13:								// WalkToGimp
		if (motionSource->commandedSpeed <= legCycleSpeed)
		{
			if (motionSource->commandedSpeed < legCycleSpeed)
			{
				legCycleSpeed -= gimpCycleRate * time_slice;
				if (legCycleSpeed < motionSource->commandedSpeed)	legCycleSpeed = motionSource->commandedSpeed;
				if (legCycleSpeed < gimpStrideLength)				legCycleSpeed = gimpStrideLength;
			}
		}
		else
		{
			legCycleSpeed += gimpCycleRate * time_slice;
			if (legCycleSpeed > motionSource->commandedSpeed)		legCycleSpeed = motionSource->commandedSpeed;
			if (legCycleSpeed > gimpSpeedMax)						legCycleSpeed = gimpSpeedMax;
		}
		{
			Scalar ratio = legCycleSpeed / gimpStrideLength;
			if (ratio <= ZeroSpeed)	ratio = -ratio;
			distance = legAnimation.Advance(ratio * time_slice * globalTimeScale, 1);
		}
		break;

	case 0x18: case 0x19:								// FallForward / FallBackward (jump)
		{
			Scalar ratio;
			if (mode == 3)								// run jump
			{
				if (motionSource->commandedSpeed <= legCycleSpeed)
				{
					if (motionSource->commandedSpeed < legCycleSpeed)
					{
						legCycleSpeed -= forwardCycleRate * time_slice;
						if (legCycleSpeed < motionSource->commandedSpeed)	legCycleSpeed = motionSource->commandedSpeed;
						if (legCycleSpeed < jumpRunSpeedMax)				legCycleSpeed = jumpRunSpeedMax;
					}
				}
				else
				{
					legCycleSpeed += forwardCycleRate * time_slice;
					if (legCycleSpeed > motionSource->commandedSpeed)		legCycleSpeed = motionSource->commandedSpeed;
					if (legCycleSpeed > jumpRunStrideLength)				legCycleSpeed = jumpRunStrideLength;
				}
				ratio = legCycleSpeed / jumpRunStrideLength;	// 0x544
			}
			else										// walk jump
			{
				if (motionSource->commandedSpeed <= legCycleSpeed)
				{
					if (motionSource->commandedSpeed < legCycleSpeed)
					{
						legCycleSpeed -= forwardCycleRate * time_slice;
						if (legCycleSpeed < motionSource->commandedSpeed)	legCycleSpeed = motionSource->commandedSpeed;
						if (legCycleSpeed < jumpWalkSpeedMax)				legCycleSpeed = jumpWalkSpeedMax;
					}
				}
				else
				{
					legCycleSpeed += forwardCycleRate * time_slice;
					if (legCycleSpeed > motionSource->commandedSpeed)		legCycleSpeed = motionSource->commandedSpeed;
					if (legCycleSpeed > jumpWalkStrideLength)				legCycleSpeed = jumpWalkStrideLength;
				}
				ratio = legCycleSpeed / jumpWalkStrideLength;	// 0x548
			}
			distance = legAnimation.Advance(time_slice * ratio * globalTimeScale, 1);
		}
		break;

	default:
		DebugStream << (AnimationNames + legAnimationState * 0x3c);
		DebugStream.Emit();
		Verify(False, "Unsupported mech animation!",
			"d:\\tesla\\bt\\bt\\MECH2.CPP", 0x672);
	}

	return distance;
}

//===========================================================================//
// End of recovered mech2.cpp slice.
//===========================================================================//
