The "gimp" family had two conflicting donor readings -- limp vs jump-jet --
flagged in 5.3.94 rather than guessed at. The binary settles it: LIMP. The
mode test in every gimp function is mech+0x40 in {3,4}, the same values the
damage model documents as "limp gait graphic (left 3 / right 4)"; there is no
jump-jet control anywhere in the pod cockpit; and the donor's port routing
("graphicAlarm 3/4") was the same field under a different name. Its
"run-jump clip" mech3 annotations were the misreading that started the murk.
RECONSTRUCTED, completing all 12 of mech2's census functions:
GimpLegClipFinished @004a7970 GimpBodyClipFinished @004a6344
AdvanceLegAnimationGimp @004a71f4 AdvanceBodyAnimationGimp @004a5bf8
+ the limp branch atop both normal *ClipFinished
+ the limp pick in Simulate (replaces the normal advancers while limping)
HOW A LIMP WORKS, now from the bytes rather than description:
It replaces ONE stride. Limping left, the right stride (6) hands off to
the left limp figure (0x16 -> the self-cycling 0x18) while the other leg
keeps its normal clips. The asymmetry IS the limp.
Both machines CLAMP THEIR DEMAND while in a cycle -- the leg machine
writes the mapper's own speedDemand cell down to the damaged side's cap
(new MechControlsMapper::SetSpeedDemand, matching the binary's direct
mapper+0x128 write), the body machine clamps bodyTargetSpeed, both floor
at zero. A limping mech cannot command more than its figure carries, nor
reverse out of a forward cycle.
The limp advancers keep states 0x16-0x1b LIVE -- the normal advancers
treat those as the reset group, which is exactly why the limp flavours
must be selected while limping or the figure is neutralized mid-cycle.
No death latch, no wind-down: the movement modes are exclusive.
ALSO EXPLAINED IN PASSING: Ghidra's 3760-byte FUN_004a6344 -- the census's
largest function -- is really THREE functions. The two normal ClipFinished
callbacks (@004a6928/@004a6d8c) are reached only via data pointers, so the
decompiler folded them into the gimp-body machine's extent.
All movement-mode reads route through Mech::MovementMode() (mech+0x40 == the
simulation state), which honours a BT_FORCE_LIMP=3|4 dev hook so the gait
could be verified before the damage model's limp hook exists.
VERIFIED, two runs on the rig:
NO-REGRESSION: the normal mission's speed sequence is BIT-IDENTICAL to
5.3.95 (7.31972, 26.6726, 22.1601, ...). The branch costs nothing.
FORCED LEFT LIMP (new pod_render_limp.conf): the mapper still demands
26.9; the hull lurches at 10-23. And on the wire the healthy walk's tight
pose-count pairs (650/649 ... 434/421) BREAK to a 3x asymmetry -- the
drag-leg joint at 18 poses against its partner's 55. The limp is visible
in the data exactly the way it will be visible on screen.
STILL OPEN in this family: the damage hook (leg zone >= 0.5 -> mode 3/4, a
MECHDMG increment -- nothing sets the mode in real play yet), and
IntegrateMotion's remaining pieces.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2051 lines
53 KiB
C++
2051 lines
53 KiB
C++
//===========================================================================//
|
|
// File: mech2.cpp //
|
|
// Project: BattleTech Brick: Entity Manager //
|
|
// Contents: Mech gait animation -- the transition machine //
|
|
//---------------------------------------------------------------------------//
|
|
// Date Who Modification //
|
|
// -------- --- ---------------------------------------------------------- //
|
|
// //
|
|
//---------------------------------------------------------------------------//
|
|
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
|
|
// All Rights reserved worldwide //
|
|
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
|
|
//===========================================================================//
|
|
|
|
#include <bt.hpp>
|
|
#pragma hdrstop
|
|
|
|
#if !defined(MECH_HPP)
|
|
# include <mech.hpp>
|
|
#endif
|
|
|
|
#if !defined(MECHMPPR_HPP)
|
|
# include <mechmppr.hpp>
|
|
#endif
|
|
|
|
#if !defined(APP_HPP)
|
|
# include <app.hpp>
|
|
#endif
|
|
|
|
//
|
|
//#############################################################################
|
|
// A mech walks on two parallel clip channels.
|
|
//
|
|
// The LEG channel is the locally-simulated gait. Its transitions read the
|
|
// LIVE commanded speed out of the controls mapper, so it responds to the
|
|
// stick the instant it moves.
|
|
//
|
|
// The BODY channel is the displayed motion, and the distance IT advances is
|
|
// what carries the mech forward. Its transitions read bodyTargetSpeed --
|
|
// a snapshot -- which is what lets a dead-reckoned or networked mech walk
|
|
// properly with no controls mapper of its own.
|
|
//
|
|
// Both channels run the same state machine over the same clips; only the
|
|
// speed they consult differs. That is the whole reason the two ClipFinished
|
|
// functions below are near-twins rather than one shared routine, and the
|
|
// symmetry is load-bearing: where the binary's two jump tables agree, a
|
|
// disagreement in this file is a bug.
|
|
//
|
|
// Each clip is ONE STRIDE, which is why every state is handed. A walk is
|
|
// Right, Left, Right, ... and each entry to and exit from a cycle has its own
|
|
// handed pair so the mech always leaves on the correct foot.
|
|
//
|
|
// The machine only ever runs at END OF CLIP. SequenceController::Advance
|
|
// calls the channel's finished callback, which picks the next state, re-arms
|
|
// the channel, and spends the leftover time in the new clip -- returning the
|
|
// distance that leftover covered so Advance can fold it in. Getting that
|
|
// contract wrong double-counts the mech's forward motion.
|
|
//#############################################################################
|
|
//
|
|
|
|
//
|
|
//#############################################################################
|
|
// @004a7fc4 -- bind the leg channel to a state's clip and record the state.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::SetLegAnimation(int state)
|
|
{
|
|
Check(this);
|
|
//
|
|
// Bounded by the SLOT count, not the name count: slot 0x20 is the
|
|
// bump/crash clip and is legitimately bound on a wall impact.
|
|
//
|
|
Verify(state >= 0 && state < AnimationSlotCount);
|
|
|
|
legAnimation.SelectSequence(
|
|
animationClips[state],
|
|
(void *)Mech::LegClipFinished,
|
|
0,
|
|
0);
|
|
legStateAlarm.SetLevel((unsigned)state);
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// @004a800c -- the body channel's equivalent.
|
|
//
|
|
// This one also drives the animation StateIndicators, which is how the audio
|
|
// subsystem's watchers learn a gait changed. Guarded to the constructed
|
|
// range so an out-of-range clip cannot trip StateIndicator's own Verify.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::SetBodyAnimation(int state)
|
|
{
|
|
Check(this);
|
|
Verify(state >= 0 && state < AnimationSlotCount);
|
|
|
|
bodyAnimation.SelectSequence(
|
|
animationClips[state],
|
|
(void *)Mech::BodyClipFinished,
|
|
0,
|
|
0);
|
|
bodyStateAlarm.SetLevel((unsigned)state);
|
|
|
|
animationState.SetState(state);
|
|
replicantAnimationState.SetState(state);
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// The shared tails (@0x4a6a06 leg / @0x4a6e66 body) every handler ends in:
|
|
// bind the next state, then spend the carryover inside it.
|
|
//#############################################################################
|
|
//
|
|
Scalar
|
|
Mech::LegTransition(int next_state, Scalar advance_time, int move_joints)
|
|
{
|
|
Check(this);
|
|
|
|
SetLegAnimation(next_state);
|
|
return legAnimation.Advance(advance_time, move_joints);
|
|
}
|
|
|
|
Scalar
|
|
Mech::BodyTransition(int next_state, Scalar advance_time, int move_joints)
|
|
{
|
|
Check(this);
|
|
|
|
SetBodyAnimation(next_state);
|
|
return bodyAnimation.Advance(advance_time, move_joints);
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// @004a6928 -- the LEG channel's end-of-clip machine (jump table @0x4a69aa).
|
|
//
|
|
// Reads the live commanded speed from the controls mapper. A mech with no
|
|
// mapper reads zero and simply idles, which is the correct behaviour for a
|
|
// replicant.
|
|
//#############################################################################
|
|
//
|
|
Scalar
|
|
Mech::LegClipFinished(
|
|
Mech *mech,
|
|
unsigned /* callback_arg */,
|
|
Scalar carryover,
|
|
int move_joints
|
|
)
|
|
{
|
|
Check(mech);
|
|
|
|
//
|
|
// A LIMPING mech's transitions run the limp machine instead (movement
|
|
// mode 3 = left-leg limp, 4 = right; the damage model's own "limp gait
|
|
// graphic (left 3 / right 4)"). Guarded on the clip set existing --
|
|
// LoadLocomotionClips leaves hasGimpClips 0 for a model without it.
|
|
//
|
|
{
|
|
int
|
|
mode = mech->MovementMode();
|
|
if ((mode == 3 || mode == 4) && mech->hasGimpClips)
|
|
{
|
|
return mech->GimpLegClipFinished(carryover);
|
|
}
|
|
}
|
|
|
|
//
|
|
// The binary reads subsystemArray[0] -- the roster's controls-mapper slot.
|
|
// A mech without one (a replicant) reads zero and idles, which is right.
|
|
//
|
|
Scalar
|
|
demand = 0.0f;
|
|
if (mech->subsystemArray != NULL && mech->subsystemArray[0] != NULL)
|
|
{
|
|
demand =
|
|
((MechControlsMapper *)mech->subsystemArray[0])->GetSpeedDemand();
|
|
}
|
|
|
|
Scalar
|
|
cycle_rate = mech->forwardCycleRate,
|
|
time_scale = mech->globalTimeScale,
|
|
cycle = mech->legCycleSpeed,
|
|
tail_time = carryover * time_scale;
|
|
|
|
switch (mech->legStateAlarm.GetLevel())
|
|
{
|
|
//
|
|
// Standing and the idle group -- nothing to transition to.
|
|
//
|
|
case 0: case 1: case 22: case 23: case 24: case 25: case 26: case 27:
|
|
return 0.0f;
|
|
|
|
case 2:
|
|
mech->legStateAlarm.SetLevel(1);
|
|
return 0.0f;
|
|
|
|
//
|
|
// The transition-END clips: having arrived, 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:
|
|
mech->legStateAlarm.SetLevel(0);
|
|
return 0.0f;
|
|
|
|
//
|
|
// Walking, right foot down (@0x4a6aad). Three ways out: stop if the
|
|
// demand has fallen below the "moving at all" threshold, step up toward
|
|
// the run cycle if it is over the walk cap, otherwise take the next
|
|
// stride on the other foot.
|
|
//
|
|
// The stop and step-up tests each check the DEMAND and the CURRENT CYCLE
|
|
// SPEED slewed by one carryover -- so a momentary flick of the stick
|
|
// cannot yank the mech out of a stride it has already committed to.
|
|
//
|
|
case 5: case 6: case 14:
|
|
if (
|
|
demand < mech->standSpeed &&
|
|
(cycle - cycle_rate * carryover) < mech->standSpeed
|
|
)
|
|
{
|
|
return mech->LegTransition(9, tail_time, move_joints);
|
|
}
|
|
if (
|
|
demand > mech->walkStrideLength &&
|
|
(cycle + cycle_rate * carryover) > mech->walkStrideLength
|
|
)
|
|
{
|
|
return mech->LegTransition(0xb, tail_time, move_joints);
|
|
}
|
|
return mech->LegTransition(
|
|
7,
|
|
carryover * cycle * time_scale / mech->walkStrideLength,
|
|
move_joints);
|
|
|
|
//
|
|
// Walking, left foot down (@0x4a69d6) -- the mirror.
|
|
//
|
|
case 7: case 15:
|
|
if (
|
|
demand < mech->standSpeed &&
|
|
(cycle - cycle_rate * carryover) < mech->standSpeed
|
|
)
|
|
{
|
|
return mech->LegTransition(8, tail_time, move_joints);
|
|
}
|
|
if (
|
|
demand > mech->walkStrideLength &&
|
|
(cycle + cycle_rate * carryover) > mech->walkStrideLength
|
|
)
|
|
{
|
|
return mech->LegTransition(0xa, tail_time, move_joints);
|
|
}
|
|
return mech->LegTransition(
|
|
6,
|
|
carryover * cycle * time_scale / mech->walkStrideLength,
|
|
move_joints);
|
|
|
|
//
|
|
// Running / reversing (@0x4a6bdb and @0x4a6b63): drop back to the walk
|
|
// cycle when the demand decays, else alternate feet.
|
|
//
|
|
case 10: case 12:
|
|
if (
|
|
demand < mech->reverseSpeedMax &&
|
|
(cycle - cycle_rate * carryover) < mech->reverseSpeedMax
|
|
)
|
|
{
|
|
return mech->LegTransition(0xf, tail_time, move_joints);
|
|
}
|
|
return mech->LegTransition(
|
|
0xd,
|
|
carryover * cycle * time_scale / mech->reverseStrideLength,
|
|
move_joints);
|
|
|
|
case 11: case 13:
|
|
if (
|
|
demand < mech->reverseSpeedMax &&
|
|
(cycle - cycle_rate * carryover) < mech->reverseSpeedMax
|
|
)
|
|
{
|
|
return mech->LegTransition(0xe, tail_time, move_joints);
|
|
}
|
|
return mech->LegTransition(
|
|
0xc,
|
|
carryover * cycle * time_scale / mech->reverseStrideLength,
|
|
move_joints);
|
|
|
|
//
|
|
// Limping (@0x4a6c17 and @0x4a6cc4). gimpStrideLength is authored
|
|
// NEGATIVE, so the cycle time comes out negative and has to be folded
|
|
// positive before it can be spent -- the binary does exactly this at
|
|
// @0x4a6c6e / @0x4a6d3d.
|
|
//
|
|
case 16: case 18:
|
|
if (
|
|
demand > mech->gimpSpeedMax &&
|
|
(mech->gimpCycleRate * carryover + cycle) > mech->gimpSpeedMax
|
|
)
|
|
{
|
|
return mech->LegTransition(0x15, tail_time, move_joints);
|
|
}
|
|
{
|
|
Scalar
|
|
cycle_time = carryover * cycle * time_scale / mech->gimpStrideLength;
|
|
if (cycle_time <= 0.0f)
|
|
{
|
|
cycle_time = -cycle_time;
|
|
}
|
|
return mech->LegTransition(0x13, cycle_time, move_joints);
|
|
}
|
|
|
|
case 17: case 19:
|
|
if (
|
|
demand > mech->gimpSpeedMax &&
|
|
(mech->gimpCycleRate * carryover + cycle) > mech->gimpSpeedMax
|
|
)
|
|
{
|
|
return mech->LegTransition(0x14, tail_time, move_joints);
|
|
}
|
|
{
|
|
Scalar
|
|
cycle_time = carryover * cycle * time_scale / mech->gimpStrideLength;
|
|
if (cycle_time <= 0.0f)
|
|
{
|
|
cycle_time = -cycle_time;
|
|
}
|
|
return mech->LegTransition(0x12, cycle_time, move_joints);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Falls, crashes and the death clips play out and stop here.
|
|
//
|
|
return 0.0f;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// @004a6d8c -- the BODY channel's end-of-clip machine (jump table @0x4a6e0a).
|
|
//
|
|
// Structurally identical to the leg machine above, reading bodyTargetSpeed
|
|
// instead of the live mapper demand. Kept as its own routine because that is
|
|
// how the binary has it, and because the two tables are each other's check.
|
|
//#############################################################################
|
|
//
|
|
Scalar
|
|
Mech::BodyClipFinished(
|
|
Mech *mech,
|
|
unsigned /* callback_arg */,
|
|
Scalar carryover,
|
|
int move_joints
|
|
)
|
|
{
|
|
Check(mech);
|
|
|
|
{
|
|
int
|
|
mode = mech->MovementMode();
|
|
if ((mode == 3 || mode == 4) && mech->hasGimpClips)
|
|
{
|
|
return mech->GimpBodyClipFinished(carryover, move_joints);
|
|
}
|
|
}
|
|
|
|
Scalar
|
|
cycle_rate = mech->forwardCycleRate,
|
|
time_scale = mech->globalTimeScale,
|
|
cycle = mech->bodyCycleSpeed,
|
|
demand = mech->bodyTargetSpeed,
|
|
tail_time = carryover * time_scale;
|
|
|
|
switch (mech->bodyStateAlarm.GetLevel())
|
|
{
|
|
case 0: case 1: case 22: case 23: case 24: case 25: case 26: case 27:
|
|
return 0.0f;
|
|
|
|
case 2:
|
|
mech->bodyStateAlarm.SetLevel(1);
|
|
return 0.0f;
|
|
|
|
case 3: case 4: case 8: case 9: case 20: case 21:
|
|
case 28: case 29: case 30: case 31: case 32:
|
|
mech->bodyStateAlarm.SetLevel(0);
|
|
return 0.0f;
|
|
|
|
//
|
|
// Walking, right foot down (@0x4a6f11).
|
|
//
|
|
case 5: case 6: case 14:
|
|
if (
|
|
demand < mech->standSpeed &&
|
|
(cycle - cycle_rate * carryover) < mech->standSpeed
|
|
)
|
|
{
|
|
return mech->BodyTransition(9, tail_time, move_joints);
|
|
}
|
|
if (
|
|
demand > mech->walkStrideLength &&
|
|
(cycle + cycle_rate * carryover) > mech->walkStrideLength
|
|
)
|
|
{
|
|
return mech->BodyTransition(0xb, tail_time, move_joints);
|
|
}
|
|
return mech->BodyTransition(
|
|
7,
|
|
carryover * cycle * time_scale / mech->walkStrideLength,
|
|
move_joints);
|
|
|
|
//
|
|
// Walking, left foot down (@0x4a6e36).
|
|
//
|
|
case 7: case 15:
|
|
if (
|
|
demand < mech->standSpeed &&
|
|
(cycle - cycle_rate * carryover) < mech->standSpeed
|
|
)
|
|
{
|
|
return mech->BodyTransition(8, tail_time, move_joints);
|
|
}
|
|
if (
|
|
demand > mech->walkStrideLength &&
|
|
(cycle + cycle_rate * carryover) > mech->walkStrideLength
|
|
)
|
|
{
|
|
return mech->BodyTransition(0xa, tail_time, move_joints);
|
|
}
|
|
return mech->BodyTransition(
|
|
6,
|
|
carryover * cycle * time_scale / mech->walkStrideLength,
|
|
move_joints);
|
|
|
|
//
|
|
// Running / reversing (@0x4a7041 and @0x4a6fc7).
|
|
//
|
|
case 10: case 12:
|
|
if (
|
|
demand < mech->reverseSpeedMax &&
|
|
(cycle - cycle_rate * carryover) < mech->reverseSpeedMax
|
|
)
|
|
{
|
|
return mech->BodyTransition(0xf, tail_time, move_joints);
|
|
}
|
|
return mech->BodyTransition(
|
|
0xd,
|
|
carryover * cycle * time_scale / mech->reverseStrideLength,
|
|
move_joints);
|
|
|
|
case 11: case 13:
|
|
if (
|
|
demand < mech->reverseSpeedMax &&
|
|
(cycle - cycle_rate * carryover) < mech->reverseSpeedMax
|
|
)
|
|
{
|
|
return mech->BodyTransition(0xe, tail_time, move_joints);
|
|
}
|
|
return mech->BodyTransition(
|
|
0xc,
|
|
carryover * cycle * time_scale / mech->reverseStrideLength,
|
|
move_joints);
|
|
|
|
//
|
|
// The reverse cycle (@0x4a707d and @0x4a712c). Note these are the BACK
|
|
// gait, not a limp, despite sharing the gimp caps: while the demand stays
|
|
// below gimpSpeedMax the cycle alternates 0x12 <-> 0x13, and a forward
|
|
// demand leaves through the back-to-stand pair. Reading them as "gimp,
|
|
// fall back to standing" makes the body loop stand -> reverse-entry
|
|
// forever, which is a slow reverse with a wrong-footed exit.
|
|
//
|
|
case 16: case 18:
|
|
if (
|
|
demand > mech->gimpSpeedMax &&
|
|
(mech->gimpCycleRate * carryover + cycle) > mech->gimpSpeedMax
|
|
)
|
|
{
|
|
return mech->BodyTransition(0x15, tail_time, move_joints);
|
|
}
|
|
{
|
|
Scalar
|
|
cycle_time = carryover * cycle * time_scale / mech->gimpStrideLength;
|
|
if (cycle_time <= 0.0f)
|
|
{
|
|
cycle_time = -cycle_time;
|
|
}
|
|
return mech->BodyTransition(0x13, cycle_time, move_joints);
|
|
}
|
|
|
|
case 17: case 19:
|
|
if (
|
|
demand > mech->gimpSpeedMax &&
|
|
(mech->gimpCycleRate * carryover + cycle) > mech->gimpSpeedMax
|
|
)
|
|
{
|
|
return mech->BodyTransition(0x14, tail_time, move_joints);
|
|
}
|
|
{
|
|
Scalar
|
|
cycle_time = carryover * cycle * time_scale / mech->gimpStrideLength;
|
|
if (cycle_time <= 0.0f)
|
|
{
|
|
cycle_time = -cycle_time;
|
|
}
|
|
return mech->BodyTransition(0x12, cycle_time, move_joints);
|
|
}
|
|
}
|
|
|
|
return 0.0f;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// @004a5028 -- the LEG channel's per-frame update (ground flavour).
|
|
//
|
|
// Reads the LIVE demand from the controls mapper, arms the death clips off
|
|
// the movement mode, slews legCycleSpeed toward the demand inside each
|
|
// cycle's caps, and advances the clip -- which is what writes the leg
|
|
// joints. Returns the cycle distance covered this frame.
|
|
//
|
|
// Reconstructed from the RAW decomp rather than the BT411 donor: the donor
|
|
// carries port-era replicant accommodations and a relocated turn-in-place
|
|
// dispatcher that belong to ITS network model, not to the binary. In the
|
|
// binary the trn dispatcher lives in the master performance (mech4), and a
|
|
// replicant's mapper cell replicates -- so this function reads the mapper
|
|
// unconditionally, exactly as decompiled.
|
|
//#############################################################################
|
|
//
|
|
Scalar
|
|
Mech::AdvanceLegAnimation(Scalar time_slice)
|
|
{
|
|
Check(this);
|
|
|
|
//
|
|
// The binary reads the roster's slot 0 with no null check -- a mech
|
|
// always has its controls mapper by the time it ticks.
|
|
//
|
|
MechControlsMapper
|
|
*mapper = (MechControlsMapper *)subsystemArray[0];
|
|
Check_Pointer(mapper);
|
|
Scalar
|
|
demand = mapper->GetSpeedDemand(),
|
|
distance = 0.0f;
|
|
|
|
//
|
|
// One-shot: movement modes 5..8 are the falls/deaths; latch the matching
|
|
// crash clip exactly once.
|
|
//
|
|
if (!deathAnimationLatched)
|
|
{
|
|
switch (MovementMode())
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Wind-down: once the cycle speed has decayed to nothing during a
|
|
// walk-transition state, drop straight to standing.
|
|
//
|
|
{
|
|
int
|
|
state = (int)legStateAlarm.GetLevel();
|
|
if (
|
|
legCycleSpeed <= 0.0f &&
|
|
(state == 6 || state == 7 || state == 8 || state == 9)
|
|
)
|
|
{
|
|
legStateAlarm.SetLevel(0);
|
|
legResetLatch = 1;
|
|
}
|
|
}
|
|
|
|
switch (legStateAlarm.GetLevel())
|
|
{
|
|
case 0:
|
|
//
|
|
// Standing. A demand above standSpeed begins the walk; a NEGATIVE
|
|
// demand backs up; anything in between stays put. Arming a state
|
|
// FALLS THROUGH so the new clip advances this same frame.
|
|
//
|
|
if (standSpeed < demand)
|
|
{
|
|
SetLegAnimation(5);
|
|
}
|
|
else
|
|
{
|
|
distance = 0.0f;
|
|
if (demand >= 0.0f)
|
|
{
|
|
break;
|
|
}
|
|
SetLegAnimation(0x10);
|
|
}
|
|
// fall through
|
|
|
|
case 2: case 3: case 5: case 8: case 9: case 10: case 0xb:
|
|
case 0xe: case 0xf: case 0x10: case 0x11: case 0x14: case 0x15:
|
|
case 0x1c: case 0x1d: case 0x1e: case 0x1f: case 0x20:
|
|
advance_clip:
|
|
//
|
|
// The plain-advance group: transitions, falls and deaths play at the
|
|
// global rate scaled by the idle/transition stride scale. The
|
|
// Standing guard is the binary's own (MECH2.CPP:0xd3) -- unreachable
|
|
// through the fall-through above (arming rewrote the level), it
|
|
// catches a DIRECT entry with the alarm still at 0.
|
|
//
|
|
if (legStateAlarm.GetLevel() == 0)
|
|
{
|
|
Fail("Standing Not Supported");
|
|
}
|
|
distance = legAnimation.Advance(
|
|
time_slice * globalTimeScale * idleStrideScale, 1);
|
|
legCycleSpeed = distance / time_slice;
|
|
break;
|
|
|
|
case 1:
|
|
distance = 0.0f;
|
|
break;
|
|
|
|
case 4:
|
|
//
|
|
// Turn-in-place. A demand outside [0, standSpeed] abandons the turn
|
|
// -- drop to standing and request the leg-state update record --
|
|
// otherwise the turn clip advances like any transition. (What ARMS
|
|
// state 4 is the master performance's dispatcher, mech4 -- not here.)
|
|
//
|
|
if (standSpeed < demand)
|
|
{
|
|
legStateAlarm.SetLevel(0);
|
|
ForceUpdate(8);
|
|
break;
|
|
}
|
|
distance = 0.0f;
|
|
if (demand < 0.0f)
|
|
{
|
|
legStateAlarm.SetLevel(0);
|
|
ForceUpdate(8);
|
|
break;
|
|
}
|
|
goto advance_clip;
|
|
|
|
case 6: case 7:
|
|
//
|
|
// The walk cycle. Slew the cycle speed toward the demand at
|
|
// forwardCycleRate: upward capped by the demand then the walk
|
|
// stride, downward floored by the demand then standSpeed. The clip
|
|
// advances at (cycle / walkStride) of its authored rate -- a slow
|
|
// walk IS the walk clip played slow.
|
|
//
|
|
if (demand > legCycleSpeed)
|
|
{
|
|
legCycleSpeed += forwardCycleRate * time_slice;
|
|
if (legCycleSpeed > demand)
|
|
{
|
|
legCycleSpeed = demand;
|
|
}
|
|
if (legCycleSpeed > walkStrideLength)
|
|
{
|
|
legCycleSpeed = walkStrideLength;
|
|
}
|
|
}
|
|
else if (demand < legCycleSpeed)
|
|
{
|
|
legCycleSpeed -= forwardCycleRate * time_slice;
|
|
if (legCycleSpeed < demand)
|
|
{
|
|
legCycleSpeed = demand;
|
|
}
|
|
if (legCycleSpeed < standSpeed)
|
|
{
|
|
legCycleSpeed = standSpeed;
|
|
}
|
|
}
|
|
distance = legAnimation.Advance(
|
|
time_slice * (legCycleSpeed / walkStrideLength) * globalTimeScale,
|
|
1);
|
|
break;
|
|
|
|
case 0xc: case 0xd:
|
|
//
|
|
// The run cycle -- same slew, its own caps: up to runSpeedMax, down
|
|
// no further than reverseSpeedMax (the drop-out threshold the
|
|
// ClipFinished handler tests).
|
|
//
|
|
if (demand > legCycleSpeed)
|
|
{
|
|
legCycleSpeed += forwardCycleRate * time_slice;
|
|
if (legCycleSpeed > demand)
|
|
{
|
|
legCycleSpeed = demand;
|
|
}
|
|
if (legCycleSpeed > runSpeedMax)
|
|
{
|
|
legCycleSpeed = runSpeedMax;
|
|
}
|
|
}
|
|
else if (demand < legCycleSpeed)
|
|
{
|
|
legCycleSpeed -= forwardCycleRate * time_slice;
|
|
if (legCycleSpeed < demand)
|
|
{
|
|
legCycleSpeed = demand;
|
|
}
|
|
if (legCycleSpeed < reverseSpeedMax)
|
|
{
|
|
legCycleSpeed = reverseSpeedMax;
|
|
}
|
|
}
|
|
distance = legAnimation.Advance(
|
|
time_slice * (legCycleSpeed / reverseStrideLength) * globalTimeScale,
|
|
1);
|
|
break;
|
|
|
|
case 0x12: case 0x13:
|
|
//
|
|
// The reverse cycle. Everything is NEGATIVE here -- the demand, the
|
|
// cycle speed, and both caps (gimpSpeedMax ~ -4, gimpStrideLength
|
|
// ~ -20 on the Mad Cat), so "up" slews toward zero and "down" toward
|
|
// full reverse, at the reverse's own gimpCycleRate. The advance
|
|
// ratio is folded positive: a reverse clip is authored backward, it
|
|
// is not played backward.
|
|
//
|
|
if (demand > legCycleSpeed)
|
|
{
|
|
legCycleSpeed += gimpCycleRate * time_slice;
|
|
if (legCycleSpeed > demand)
|
|
{
|
|
legCycleSpeed = demand;
|
|
}
|
|
if (legCycleSpeed > gimpSpeedMax)
|
|
{
|
|
legCycleSpeed = gimpSpeedMax;
|
|
}
|
|
}
|
|
else if (demand < legCycleSpeed)
|
|
{
|
|
legCycleSpeed -= gimpCycleRate * time_slice;
|
|
if (legCycleSpeed < demand)
|
|
{
|
|
legCycleSpeed = demand;
|
|
}
|
|
if (legCycleSpeed < gimpStrideLength)
|
|
{
|
|
legCycleSpeed = gimpStrideLength;
|
|
}
|
|
}
|
|
{
|
|
Scalar
|
|
ratio = legCycleSpeed / gimpStrideLength;
|
|
if (ratio <= 0.0f)
|
|
{
|
|
ratio = -ratio;
|
|
}
|
|
distance = legAnimation.Advance(
|
|
ratio * time_slice * globalTimeScale, 1);
|
|
}
|
|
break;
|
|
|
|
case 0x16: case 0x17: case 0x18: case 0x19: case 0x1a: case 0x1b:
|
|
//
|
|
// The reset group -- limp exits and the four falls. Clear the
|
|
// motion-event state and every one-shot, drop to standing, and put
|
|
// the skeleton back to its neutral pose.
|
|
//
|
|
motionEventName = "";
|
|
motionEventArmed = 0;
|
|
legResetLatch = 0;
|
|
deathAnimationLatched = 0;
|
|
legStateAlarm.SetLevel(0);
|
|
legAnimation.Reset(1);
|
|
break;
|
|
|
|
default:
|
|
Fail("Unsupported mech animation");
|
|
}
|
|
|
|
return distance;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// @004a5678 -- the BODY channel's per-frame update (ground flavour).
|
|
//
|
|
// The displayed-motion twin. Differences from the leg version, all
|
|
// binary-verified: the demand is bodyTargetSpeed (the snapshot, no mapper
|
|
// access); there is NO wind-down block and NO turn-in-place case (state 4
|
|
// sits in the plain group); move_joints arrives as a parameter and reaches
|
|
// every Advance AND the reset's Reset call -- so the caller decides whether
|
|
// this channel poses the skeleton or only measures the stride.
|
|
//#############################################################################
|
|
//
|
|
Scalar
|
|
Mech::AdvanceBodyAnimation(Scalar time_slice, int move_joints)
|
|
{
|
|
Check(this);
|
|
|
|
Scalar
|
|
demand = bodyTargetSpeed,
|
|
distance = 0.0f;
|
|
|
|
if (!deathAnimationLatched)
|
|
{
|
|
switch (MovementMode())
|
|
{
|
|
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 (bodyStateAlarm.GetLevel())
|
|
{
|
|
case 0:
|
|
distance = 0.0f;
|
|
if (standSpeed < demand)
|
|
{
|
|
SetBodyAnimation(5);
|
|
}
|
|
else
|
|
{
|
|
if (demand >= 0.0f)
|
|
{
|
|
break;
|
|
}
|
|
SetBodyAnimation(0x10);
|
|
}
|
|
// fall through
|
|
|
|
case 2: case 3: case 4: case 5: case 8: case 9: case 10: case 0xb:
|
|
case 0xe: case 0xf: case 0x10: case 0x11: case 0x14: case 0x15:
|
|
case 0x1c: case 0x1d: case 0x1e: case 0x1f: case 0x20:
|
|
distance = bodyAnimation.Advance(
|
|
time_slice * globalTimeScale * idleStrideScale, move_joints);
|
|
bodyCycleSpeed = distance / time_slice;
|
|
break;
|
|
|
|
case 1:
|
|
distance = 0.0f;
|
|
break;
|
|
|
|
case 6: case 7:
|
|
if (demand > bodyCycleSpeed)
|
|
{
|
|
bodyCycleSpeed += forwardCycleRate * time_slice;
|
|
if (bodyCycleSpeed > demand)
|
|
{
|
|
bodyCycleSpeed = demand;
|
|
}
|
|
if (bodyCycleSpeed > walkStrideLength)
|
|
{
|
|
bodyCycleSpeed = walkStrideLength;
|
|
}
|
|
}
|
|
else if (demand < bodyCycleSpeed)
|
|
{
|
|
bodyCycleSpeed -= forwardCycleRate * time_slice;
|
|
if (bodyCycleSpeed < demand)
|
|
{
|
|
bodyCycleSpeed = demand;
|
|
}
|
|
if (bodyCycleSpeed < standSpeed)
|
|
{
|
|
bodyCycleSpeed = standSpeed;
|
|
}
|
|
}
|
|
distance = bodyAnimation.Advance(
|
|
time_slice * (bodyCycleSpeed / walkStrideLength) * globalTimeScale,
|
|
move_joints);
|
|
break;
|
|
|
|
case 0xc: case 0xd:
|
|
if (demand > bodyCycleSpeed)
|
|
{
|
|
bodyCycleSpeed += forwardCycleRate * time_slice;
|
|
if (bodyCycleSpeed > demand)
|
|
{
|
|
bodyCycleSpeed = demand;
|
|
}
|
|
if (bodyCycleSpeed > runSpeedMax)
|
|
{
|
|
bodyCycleSpeed = runSpeedMax;
|
|
}
|
|
}
|
|
else if (demand < bodyCycleSpeed)
|
|
{
|
|
bodyCycleSpeed -= forwardCycleRate * time_slice;
|
|
if (bodyCycleSpeed < demand)
|
|
{
|
|
bodyCycleSpeed = demand;
|
|
}
|
|
if (bodyCycleSpeed < reverseSpeedMax)
|
|
{
|
|
bodyCycleSpeed = reverseSpeedMax;
|
|
}
|
|
}
|
|
distance = bodyAnimation.Advance(
|
|
time_slice * (bodyCycleSpeed / reverseStrideLength) * globalTimeScale,
|
|
move_joints);
|
|
break;
|
|
|
|
case 0x12: case 0x13:
|
|
if (demand > bodyCycleSpeed)
|
|
{
|
|
bodyCycleSpeed += gimpCycleRate * time_slice;
|
|
if (bodyCycleSpeed > demand)
|
|
{
|
|
bodyCycleSpeed = demand;
|
|
}
|
|
if (bodyCycleSpeed > gimpSpeedMax)
|
|
{
|
|
bodyCycleSpeed = gimpSpeedMax;
|
|
}
|
|
}
|
|
else if (demand < bodyCycleSpeed)
|
|
{
|
|
bodyCycleSpeed -= gimpCycleRate * time_slice;
|
|
if (bodyCycleSpeed < demand)
|
|
{
|
|
bodyCycleSpeed = demand;
|
|
}
|
|
if (bodyCycleSpeed < gimpStrideLength)
|
|
{
|
|
bodyCycleSpeed = gimpStrideLength;
|
|
}
|
|
}
|
|
{
|
|
Scalar
|
|
ratio = bodyCycleSpeed / gimpStrideLength;
|
|
if (ratio <= 0.0f)
|
|
{
|
|
ratio = -ratio;
|
|
}
|
|
distance = bodyAnimation.Advance(
|
|
ratio * time_slice * globalTimeScale, move_joints);
|
|
}
|
|
break;
|
|
|
|
case 0x16: case 0x17: case 0x18: case 0x19: case 0x1a: case 0x1b:
|
|
motionEventName = "";
|
|
motionEventArmed = 0;
|
|
bodyResetLatch = 0;
|
|
deathAnimationLatched = 0;
|
|
bodyStateAlarm.SetLevel(0);
|
|
bodyAnimation.Reset(move_joints);
|
|
break;
|
|
|
|
default:
|
|
Fail("Unsupported mech animation");
|
|
}
|
|
|
|
return distance;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// THE LIMP MACHINES (@004a7970 leg / @004a6344 body) -- the transition
|
|
// tables that run while movement mode is 3 (left-leg limp) or 4 (right).
|
|
//
|
|
// The limp replaces ONE stride: limping left, the right stride (6) hands off
|
|
// to the left limp figure (0x16 -> the 0x18 cycle); limping right, the left
|
|
// stride (7) hands off to the right figure (0x17 -> 0x19). The other leg's
|
|
// clips keep their normal alternation, which is what makes it read as a limp
|
|
// rather than a different gait.
|
|
//
|
|
// Both machines CLAMP THEIR DEMAND while in any cycle -- the leg machine
|
|
// writes the mapper's own speedDemand cell down to the damaged side's cap,
|
|
// the body machine clamps bodyTargetSpeed -- so a limping mech cannot
|
|
// command more speed than its limp figure carries, and cannot command a
|
|
// reverse out of a forward cycle (the clamp floors at zero).
|
|
//#############################################################################
|
|
//
|
|
Scalar
|
|
Mech::GimpLegClipFinished(Scalar carryover)
|
|
{
|
|
Check(this);
|
|
|
|
MechControlsMapper
|
|
*mapper = (MechControlsMapper *)subsystemArray[0];
|
|
Check_Pointer(mapper);
|
|
|
|
int
|
|
mode = MovementMode(),
|
|
state = (int)legStateAlarm.GetLevel();
|
|
|
|
//
|
|
// The demand clamp (see the block comment above).
|
|
//
|
|
if (
|
|
state == 6 || state == 7 || state == 0xc || state == 0xd ||
|
|
state == 0x12 || state == 0x13
|
|
)
|
|
{
|
|
Scalar
|
|
cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax,
|
|
demand = mapper->GetSpeedDemand();
|
|
if (demand > cap)
|
|
{
|
|
demand = cap;
|
|
}
|
|
if (demand < 0.0f)
|
|
{
|
|
demand = 0.0f;
|
|
}
|
|
mapper->SetSpeedDemand(demand);
|
|
}
|
|
|
|
Scalar
|
|
demand = mapper->GetSpeedDemand(),
|
|
cycle_rate = forwardCycleRate,
|
|
time_scale = globalTimeScale,
|
|
cycle = legCycleSpeed;
|
|
int
|
|
plain_next;
|
|
|
|
switch (state)
|
|
{
|
|
case 2:
|
|
legStateAlarm.SetLevel(1);
|
|
return 0.0f;
|
|
|
|
case 3: case 8: case 9: case 0x14: case 0x15: case 0x1a: case 0x1b:
|
|
case 4:
|
|
case 0x20:
|
|
legStateAlarm.SetLevel(0);
|
|
return 0.0f;
|
|
|
|
//
|
|
// The right-stride walk family. Continuing hands off to the LEFT
|
|
// stride when the RIGHT leg is the good one (mode 4), and to the left
|
|
// limp figure when the left leg is the bad one (mode 3).
|
|
//
|
|
case 5: case 6: case 0xe:
|
|
if (
|
|
demand < standSpeed &&
|
|
(cycle - cycle_rate * carryover) < standSpeed
|
|
)
|
|
{
|
|
plain_next = 9;
|
|
break;
|
|
}
|
|
if (
|
|
demand > walkStrideLength &&
|
|
(cycle + cycle_rate * carryover) > walkStrideLength
|
|
)
|
|
{
|
|
plain_next = 0xb;
|
|
break;
|
|
}
|
|
SetLegAnimation((mode == 4) ? 7 : 0x16);
|
|
return legAnimation.Advance(
|
|
carryover * cycle * time_scale / walkStrideLength, 1);
|
|
|
|
case 7: case 0xf:
|
|
if (
|
|
demand < standSpeed &&
|
|
(cycle - cycle_rate * carryover) < standSpeed
|
|
)
|
|
{
|
|
plain_next = 8;
|
|
break;
|
|
}
|
|
if (
|
|
demand > walkStrideLength &&
|
|
(cycle + cycle_rate * carryover) > walkStrideLength
|
|
)
|
|
{
|
|
plain_next = 10;
|
|
break;
|
|
}
|
|
SetLegAnimation((mode == 3) ? 6 : 0x17);
|
|
return legAnimation.Advance(
|
|
carryover * cycle * time_scale / walkStrideLength, 1);
|
|
|
|
case 10: case 0xc:
|
|
if (
|
|
demand < reverseSpeedMax &&
|
|
(cycle - cycle_rate * carryover) < reverseSpeedMax
|
|
)
|
|
{
|
|
plain_next = 0xf;
|
|
break;
|
|
}
|
|
SetLegAnimation(0xd);
|
|
return legAnimation.Advance(
|
|
carryover * cycle * time_scale / reverseStrideLength, 1);
|
|
|
|
case 0xb: case 0xd:
|
|
if (
|
|
demand < reverseSpeedMax &&
|
|
(cycle - cycle_rate * carryover) < reverseSpeedMax
|
|
)
|
|
{
|
|
plain_next = 0xe;
|
|
break;
|
|
}
|
|
SetLegAnimation(0xc);
|
|
return legAnimation.Advance(
|
|
carryover * cycle * time_scale / reverseStrideLength, 1);
|
|
|
|
case 0x10: case 0x12:
|
|
if (
|
|
demand > gimpSpeedMax &&
|
|
(gimpCycleRate * carryover + cycle) > gimpSpeedMax
|
|
)
|
|
{
|
|
plain_next = 0x15;
|
|
break;
|
|
}
|
|
SetLegAnimation(0x13);
|
|
{
|
|
Scalar
|
|
cycle_time = carryover * cycle * time_scale / gimpStrideLength;
|
|
if (cycle_time <= 0.0f)
|
|
{
|
|
cycle_time = -cycle_time;
|
|
}
|
|
return legAnimation.Advance(cycle_time, 1);
|
|
}
|
|
|
|
case 0x11: case 0x13:
|
|
if (
|
|
demand > gimpSpeedMax &&
|
|
(gimpCycleRate * carryover + cycle) > gimpSpeedMax
|
|
)
|
|
{
|
|
plain_next = 0x14;
|
|
break;
|
|
}
|
|
SetLegAnimation(0x12);
|
|
{
|
|
Scalar
|
|
cycle_time = carryover * cycle * time_scale / gimpStrideLength;
|
|
if (cycle_time <= 0.0f)
|
|
{
|
|
cycle_time = -cycle_time;
|
|
}
|
|
return legAnimation.Advance(cycle_time, 1);
|
|
}
|
|
|
|
//
|
|
// The limp figures themselves: keep cycling while the demand holds,
|
|
// exit toward standing when it drops.
|
|
//
|
|
case 0x16: case 0x18:
|
|
if (
|
|
demand < gimpLeftSpeedMax &&
|
|
(cycle - cycle_rate * carryover) < gimpLeftSpeedMax
|
|
)
|
|
{
|
|
plain_next = 0x1a;
|
|
break;
|
|
}
|
|
SetLegAnimation(0x18);
|
|
return legAnimation.Advance(
|
|
carryover * cycle * time_scale / gimpLeftStrideLength, 1);
|
|
|
|
case 0x17: case 0x19:
|
|
if (
|
|
demand < gimpRightSpeedMax &&
|
|
(cycle - cycle_rate * carryover) < gimpRightSpeedMax
|
|
)
|
|
{
|
|
plain_next = 0x1b;
|
|
break;
|
|
}
|
|
SetLegAnimation(0x19);
|
|
return legAnimation.Advance(
|
|
carryover * cycle * time_scale / gimpRightStrideLength, 1);
|
|
|
|
default:
|
|
return 0.0f;
|
|
}
|
|
|
|
//
|
|
// The plain tail every non-cycle exit lands in.
|
|
//
|
|
SetLegAnimation(plain_next);
|
|
return legAnimation.Advance(carryover * time_scale, 1);
|
|
}
|
|
|
|
Scalar
|
|
Mech::GimpBodyClipFinished(Scalar carryover, int move_joints)
|
|
{
|
|
Check(this);
|
|
|
|
int
|
|
mode = MovementMode(),
|
|
state = (int)bodyStateAlarm.GetLevel();
|
|
|
|
if (
|
|
state == 6 || state == 7 || state == 0xc || state == 0xd ||
|
|
state == 0x12 || state == 0x13
|
|
)
|
|
{
|
|
Scalar
|
|
cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax;
|
|
if (bodyTargetSpeed > cap)
|
|
{
|
|
bodyTargetSpeed = cap;
|
|
}
|
|
if (bodyTargetSpeed < 0.0f)
|
|
{
|
|
bodyTargetSpeed = 0.0f;
|
|
}
|
|
}
|
|
|
|
Scalar
|
|
demand = bodyTargetSpeed,
|
|
cycle_rate = forwardCycleRate,
|
|
time_scale = globalTimeScale,
|
|
cycle = bodyCycleSpeed;
|
|
int
|
|
plain_next;
|
|
|
|
switch (state)
|
|
{
|
|
case 2:
|
|
bodyStateAlarm.SetLevel(1);
|
|
return 0.0f;
|
|
|
|
case 3: case 8: case 9: case 0x14: case 0x15: case 0x1a: case 0x1b:
|
|
case 4:
|
|
case 0x20:
|
|
bodyStateAlarm.SetLevel(0);
|
|
return 0.0f;
|
|
|
|
case 5: case 6: case 0xe:
|
|
if (
|
|
demand < standSpeed &&
|
|
(cycle - cycle_rate * carryover) < standSpeed
|
|
)
|
|
{
|
|
plain_next = 9;
|
|
break;
|
|
}
|
|
if (
|
|
demand > walkStrideLength &&
|
|
(cycle + cycle_rate * carryover) > walkStrideLength
|
|
)
|
|
{
|
|
plain_next = 0xb;
|
|
break;
|
|
}
|
|
SetBodyAnimation((mode == 4) ? 7 : 0x16);
|
|
return bodyAnimation.Advance(
|
|
carryover * cycle * time_scale / walkStrideLength, move_joints);
|
|
|
|
case 7: case 0xf:
|
|
if (
|
|
demand < standSpeed &&
|
|
(cycle - cycle_rate * carryover) < standSpeed
|
|
)
|
|
{
|
|
plain_next = 8;
|
|
break;
|
|
}
|
|
if (
|
|
demand > walkStrideLength &&
|
|
(cycle + cycle_rate * carryover) > walkStrideLength
|
|
)
|
|
{
|
|
plain_next = 10;
|
|
break;
|
|
}
|
|
SetBodyAnimation((mode == 3) ? 6 : 0x17);
|
|
return bodyAnimation.Advance(
|
|
carryover * cycle * time_scale / walkStrideLength, move_joints);
|
|
|
|
case 10: case 0xc:
|
|
if (
|
|
demand < reverseSpeedMax &&
|
|
(cycle - cycle_rate * carryover) < reverseSpeedMax
|
|
)
|
|
{
|
|
plain_next = 0xf;
|
|
break;
|
|
}
|
|
SetBodyAnimation(0xd);
|
|
return bodyAnimation.Advance(
|
|
carryover * cycle * time_scale / reverseStrideLength, move_joints);
|
|
|
|
case 0xb: case 0xd:
|
|
if (
|
|
demand < reverseSpeedMax &&
|
|
(cycle - cycle_rate * carryover) < reverseSpeedMax
|
|
)
|
|
{
|
|
plain_next = 0xe;
|
|
break;
|
|
}
|
|
SetBodyAnimation(0xc);
|
|
return bodyAnimation.Advance(
|
|
carryover * cycle * time_scale / reverseStrideLength, move_joints);
|
|
|
|
case 0x10: case 0x12:
|
|
if (
|
|
demand > gimpSpeedMax &&
|
|
(gimpCycleRate * carryover + cycle) > gimpSpeedMax
|
|
)
|
|
{
|
|
plain_next = 0x15;
|
|
break;
|
|
}
|
|
SetBodyAnimation(0x13);
|
|
{
|
|
Scalar
|
|
cycle_time = carryover * cycle * time_scale / gimpStrideLength;
|
|
if (cycle_time <= 0.0f)
|
|
{
|
|
cycle_time = -cycle_time;
|
|
}
|
|
return bodyAnimation.Advance(cycle_time, move_joints);
|
|
}
|
|
|
|
case 0x11: case 0x13:
|
|
if (
|
|
demand > gimpSpeedMax &&
|
|
(gimpCycleRate * carryover + cycle) > gimpSpeedMax
|
|
)
|
|
{
|
|
plain_next = 0x14;
|
|
break;
|
|
}
|
|
SetBodyAnimation(0x12);
|
|
{
|
|
Scalar
|
|
cycle_time = carryover * cycle * time_scale / gimpStrideLength;
|
|
if (cycle_time <= 0.0f)
|
|
{
|
|
cycle_time = -cycle_time;
|
|
}
|
|
return bodyAnimation.Advance(cycle_time, move_joints);
|
|
}
|
|
|
|
case 0x16: case 0x18:
|
|
if (
|
|
demand < gimpLeftSpeedMax &&
|
|
(cycle - cycle_rate * carryover) < gimpLeftSpeedMax
|
|
)
|
|
{
|
|
plain_next = 0x1a;
|
|
break;
|
|
}
|
|
SetBodyAnimation(0x18);
|
|
return bodyAnimation.Advance(
|
|
carryover * cycle * time_scale / gimpLeftStrideLength, move_joints);
|
|
|
|
case 0x17: case 0x19:
|
|
if (
|
|
demand < gimpRightSpeedMax &&
|
|
(cycle - cycle_rate * carryover) < gimpRightSpeedMax
|
|
)
|
|
{
|
|
plain_next = 0x1b;
|
|
break;
|
|
}
|
|
SetBodyAnimation(0x19);
|
|
return bodyAnimation.Advance(
|
|
carryover * cycle * time_scale / gimpRightStrideLength, move_joints);
|
|
|
|
default:
|
|
return 0.0f;
|
|
}
|
|
|
|
SetBodyAnimation(plain_next);
|
|
return bodyAnimation.Advance(carryover * time_scale, move_joints);
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// THE LIMP ADVANCERS (@004a71f4 leg / @004a5bf8 body). Selected instead of
|
|
// the normal pair while limping, and structurally different in exactly the
|
|
// ways a limp needs:
|
|
//
|
|
// * States 0x16-0x1b are LIVE here -- the limp entries advance in the
|
|
// plain group and the 0x18/0x19 figures get their own slewed cycles with
|
|
// the damaged side's caps. (The NORMAL advancers treat those states as
|
|
// the reset group, which is why these flavours must be selected while
|
|
// limping -- the normal one would neutralize the figure mid-cycle.)
|
|
//
|
|
// * No death latch and no wind-down: the movement modes are exclusive, so
|
|
// a limping mech is by definition not falling.
|
|
//
|
|
// * The same demand clamp as the transition machines, applied per frame.
|
|
//#############################################################################
|
|
//
|
|
Scalar
|
|
Mech::AdvanceLegAnimationGimp(Scalar time_slice)
|
|
{
|
|
Check(this);
|
|
|
|
MechControlsMapper
|
|
*mapper = (MechControlsMapper *)subsystemArray[0];
|
|
Check_Pointer(mapper);
|
|
|
|
int
|
|
mode = MovementMode(),
|
|
state = (int)legStateAlarm.GetLevel();
|
|
|
|
if (
|
|
state == 6 || state == 7 || state == 0xc || state == 0xd ||
|
|
state == 0x12 || state == 0x13
|
|
)
|
|
{
|
|
Scalar
|
|
cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax,
|
|
clamped = mapper->GetSpeedDemand();
|
|
if (clamped > cap)
|
|
{
|
|
clamped = cap;
|
|
}
|
|
if (clamped < 0.0f)
|
|
{
|
|
clamped = 0.0f;
|
|
}
|
|
mapper->SetSpeedDemand(clamped);
|
|
}
|
|
|
|
Scalar
|
|
demand = mapper->GetSpeedDemand(),
|
|
distance = 0.0f;
|
|
|
|
switch (legStateAlarm.GetLevel())
|
|
{
|
|
case 0:
|
|
if (demand <= standSpeed)
|
|
{
|
|
break;
|
|
}
|
|
SetLegAnimation(5);
|
|
// fall through
|
|
|
|
case 2: case 3: case 5: case 8: case 9: case 10: case 0xb:
|
|
case 0xe: case 0xf: case 0x10: case 0x11: case 0x14: case 0x15:
|
|
case 0x16: case 0x17: case 0x1a: case 0x1b: case 0x20:
|
|
advance_clip:
|
|
distance = legAnimation.Advance(
|
|
time_slice * globalTimeScale * idleStrideScale, 1);
|
|
legCycleSpeed = distance / time_slice;
|
|
break;
|
|
|
|
case 1:
|
|
break;
|
|
|
|
case 4:
|
|
if (standSpeed < demand)
|
|
{
|
|
legStateAlarm.SetLevel(0);
|
|
ForceUpdate(8);
|
|
break;
|
|
}
|
|
goto advance_clip;
|
|
|
|
case 6: case 7:
|
|
if (demand > legCycleSpeed)
|
|
{
|
|
legCycleSpeed += forwardCycleRate * time_slice;
|
|
if (legCycleSpeed > demand)
|
|
{
|
|
legCycleSpeed = demand;
|
|
}
|
|
if (legCycleSpeed > walkStrideLength)
|
|
{
|
|
legCycleSpeed = walkStrideLength;
|
|
}
|
|
}
|
|
else if (demand < legCycleSpeed)
|
|
{
|
|
legCycleSpeed -= forwardCycleRate * time_slice;
|
|
if (legCycleSpeed < demand)
|
|
{
|
|
legCycleSpeed = demand;
|
|
}
|
|
if (legCycleSpeed < standSpeed)
|
|
{
|
|
legCycleSpeed = standSpeed;
|
|
}
|
|
}
|
|
distance = legAnimation.Advance(
|
|
time_slice * (legCycleSpeed / walkStrideLength) * globalTimeScale,
|
|
1);
|
|
break;
|
|
|
|
case 0xc: case 0xd:
|
|
if (demand > legCycleSpeed)
|
|
{
|
|
legCycleSpeed += forwardCycleRate * time_slice;
|
|
if (legCycleSpeed > demand)
|
|
{
|
|
legCycleSpeed = demand;
|
|
}
|
|
if (legCycleSpeed > runSpeedMax)
|
|
{
|
|
legCycleSpeed = runSpeedMax;
|
|
}
|
|
}
|
|
else if (demand < legCycleSpeed)
|
|
{
|
|
legCycleSpeed -= forwardCycleRate * time_slice;
|
|
if (legCycleSpeed < demand)
|
|
{
|
|
legCycleSpeed = demand;
|
|
}
|
|
if (legCycleSpeed < reverseSpeedMax)
|
|
{
|
|
legCycleSpeed = reverseSpeedMax;
|
|
}
|
|
}
|
|
distance = legAnimation.Advance(
|
|
time_slice * (legCycleSpeed / reverseStrideLength) * globalTimeScale,
|
|
1);
|
|
break;
|
|
|
|
case 0x12: case 0x13:
|
|
if (demand > legCycleSpeed)
|
|
{
|
|
legCycleSpeed += gimpCycleRate * time_slice;
|
|
if (legCycleSpeed > demand)
|
|
{
|
|
legCycleSpeed = demand;
|
|
}
|
|
if (legCycleSpeed > gimpSpeedMax)
|
|
{
|
|
legCycleSpeed = gimpSpeedMax;
|
|
}
|
|
}
|
|
else if (demand < legCycleSpeed)
|
|
{
|
|
legCycleSpeed -= gimpCycleRate * time_slice;
|
|
if (legCycleSpeed < demand)
|
|
{
|
|
legCycleSpeed = demand;
|
|
}
|
|
if (legCycleSpeed < gimpStrideLength)
|
|
{
|
|
legCycleSpeed = gimpStrideLength;
|
|
}
|
|
}
|
|
{
|
|
Scalar
|
|
ratio = legCycleSpeed / gimpStrideLength;
|
|
if (ratio <= 0.0f)
|
|
{
|
|
ratio = -ratio;
|
|
}
|
|
distance = legAnimation.Advance(
|
|
ratio * time_slice * globalTimeScale, 1);
|
|
}
|
|
break;
|
|
|
|
//
|
|
// The limp figures: slewed like a walk, inside the damaged side's caps.
|
|
//
|
|
case 0x18: case 0x19:
|
|
{
|
|
Scalar
|
|
speed_cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax,
|
|
stride = (mode == 3) ? gimpLeftStrideLength : gimpRightStrideLength;
|
|
|
|
if (demand > legCycleSpeed)
|
|
{
|
|
legCycleSpeed += forwardCycleRate * time_slice;
|
|
if (legCycleSpeed > demand)
|
|
{
|
|
legCycleSpeed = demand;
|
|
}
|
|
if (legCycleSpeed > stride)
|
|
{
|
|
legCycleSpeed = stride;
|
|
}
|
|
}
|
|
else if (demand < legCycleSpeed)
|
|
{
|
|
legCycleSpeed -= forwardCycleRate * time_slice;
|
|
if (legCycleSpeed < demand)
|
|
{
|
|
legCycleSpeed = demand;
|
|
}
|
|
if (legCycleSpeed < speed_cap)
|
|
{
|
|
legCycleSpeed = speed_cap;
|
|
}
|
|
}
|
|
distance = legAnimation.Advance(
|
|
time_slice * (legCycleSpeed / stride) * globalTimeScale, 1);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
Fail("Unsupported mech animation");
|
|
}
|
|
|
|
return distance;
|
|
}
|
|
|
|
Scalar
|
|
Mech::AdvanceBodyAnimationGimp(Scalar time_slice, int move_joints)
|
|
{
|
|
Check(this);
|
|
|
|
int
|
|
mode = MovementMode(),
|
|
state = (int)bodyStateAlarm.GetLevel();
|
|
|
|
if (
|
|
state == 6 || state == 7 || state == 0xc || state == 0xd ||
|
|
state == 0x12 || state == 0x13
|
|
)
|
|
{
|
|
Scalar
|
|
cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax;
|
|
if (bodyTargetSpeed > cap)
|
|
{
|
|
bodyTargetSpeed = cap;
|
|
}
|
|
if (bodyTargetSpeed < 0.0f)
|
|
{
|
|
bodyTargetSpeed = 0.0f;
|
|
}
|
|
}
|
|
|
|
Scalar
|
|
demand = bodyTargetSpeed,
|
|
distance = 0.0f;
|
|
|
|
switch (bodyStateAlarm.GetLevel())
|
|
{
|
|
case 0:
|
|
if (demand <= standSpeed)
|
|
{
|
|
break;
|
|
}
|
|
SetBodyAnimation(5);
|
|
// fall through
|
|
|
|
case 2: case 3: case 4: case 5: case 8: case 9: case 10: case 0xb:
|
|
case 0xe: case 0xf: 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, move_joints);
|
|
bodyCycleSpeed = distance / time_slice;
|
|
break;
|
|
|
|
case 1:
|
|
break;
|
|
|
|
case 6: case 7:
|
|
if (demand > bodyCycleSpeed)
|
|
{
|
|
bodyCycleSpeed += forwardCycleRate * time_slice;
|
|
if (bodyCycleSpeed > demand)
|
|
{
|
|
bodyCycleSpeed = demand;
|
|
}
|
|
if (bodyCycleSpeed > walkStrideLength)
|
|
{
|
|
bodyCycleSpeed = walkStrideLength;
|
|
}
|
|
}
|
|
else if (demand < bodyCycleSpeed)
|
|
{
|
|
bodyCycleSpeed -= forwardCycleRate * time_slice;
|
|
if (bodyCycleSpeed < demand)
|
|
{
|
|
bodyCycleSpeed = demand;
|
|
}
|
|
if (bodyCycleSpeed < standSpeed)
|
|
{
|
|
bodyCycleSpeed = standSpeed;
|
|
}
|
|
}
|
|
distance = bodyAnimation.Advance(
|
|
time_slice * (bodyCycleSpeed / walkStrideLength) * globalTimeScale,
|
|
move_joints);
|
|
break;
|
|
|
|
case 0xc: case 0xd:
|
|
if (demand > bodyCycleSpeed)
|
|
{
|
|
bodyCycleSpeed += forwardCycleRate * time_slice;
|
|
if (bodyCycleSpeed > demand)
|
|
{
|
|
bodyCycleSpeed = demand;
|
|
}
|
|
if (bodyCycleSpeed > runSpeedMax)
|
|
{
|
|
bodyCycleSpeed = runSpeedMax;
|
|
}
|
|
}
|
|
else if (demand < bodyCycleSpeed)
|
|
{
|
|
bodyCycleSpeed -= forwardCycleRate * time_slice;
|
|
if (bodyCycleSpeed < demand)
|
|
{
|
|
bodyCycleSpeed = demand;
|
|
}
|
|
if (bodyCycleSpeed < reverseSpeedMax)
|
|
{
|
|
bodyCycleSpeed = reverseSpeedMax;
|
|
}
|
|
}
|
|
distance = bodyAnimation.Advance(
|
|
time_slice * (bodyCycleSpeed / reverseStrideLength) * globalTimeScale,
|
|
move_joints);
|
|
break;
|
|
|
|
case 0x12: case 0x13:
|
|
if (demand > bodyCycleSpeed)
|
|
{
|
|
bodyCycleSpeed += gimpCycleRate * time_slice;
|
|
if (bodyCycleSpeed > demand)
|
|
{
|
|
bodyCycleSpeed = demand;
|
|
}
|
|
if (bodyCycleSpeed > gimpSpeedMax)
|
|
{
|
|
bodyCycleSpeed = gimpSpeedMax;
|
|
}
|
|
}
|
|
else if (demand < bodyCycleSpeed)
|
|
{
|
|
bodyCycleSpeed -= gimpCycleRate * time_slice;
|
|
if (bodyCycleSpeed < demand)
|
|
{
|
|
bodyCycleSpeed = demand;
|
|
}
|
|
if (bodyCycleSpeed < gimpStrideLength)
|
|
{
|
|
bodyCycleSpeed = gimpStrideLength;
|
|
}
|
|
}
|
|
{
|
|
Scalar
|
|
ratio = bodyCycleSpeed / gimpStrideLength;
|
|
if (ratio <= 0.0f)
|
|
{
|
|
ratio = -ratio;
|
|
}
|
|
distance = bodyAnimation.Advance(
|
|
ratio * time_slice * globalTimeScale, move_joints);
|
|
}
|
|
break;
|
|
|
|
case 0x18: case 0x19:
|
|
{
|
|
Scalar
|
|
speed_cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax,
|
|
stride = (mode == 3) ? gimpLeftStrideLength : gimpRightStrideLength;
|
|
|
|
if (demand > bodyCycleSpeed)
|
|
{
|
|
bodyCycleSpeed += forwardCycleRate * time_slice;
|
|
if (bodyCycleSpeed > demand)
|
|
{
|
|
bodyCycleSpeed = demand;
|
|
}
|
|
if (bodyCycleSpeed > stride)
|
|
{
|
|
bodyCycleSpeed = stride;
|
|
}
|
|
}
|
|
else if (demand < bodyCycleSpeed)
|
|
{
|
|
bodyCycleSpeed -= forwardCycleRate * time_slice;
|
|
if (bodyCycleSpeed < demand)
|
|
{
|
|
bodyCycleSpeed = demand;
|
|
}
|
|
if (bodyCycleSpeed < speed_cap)
|
|
{
|
|
bodyCycleSpeed = speed_cap;
|
|
}
|
|
}
|
|
distance = bodyAnimation.Advance(
|
|
time_slice * (bodyCycleSpeed / stride) * globalTimeScale,
|
|
move_joints);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
Fail("Unsupported mech animation");
|
|
}
|
|
|
|
return distance;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// @004a7f50 -- prefix + suffix -> the clip's resource ID.
|
|
//
|
|
// The clip names are the model's 3-char animation prefix with a 3-char gait
|
|
// suffix appended ("mad" + "wwr" = madwwr), resolved by name over the
|
|
// animation resources. Returns a pointer to the found description's
|
|
// resourceID; NULL when the model has no such clip -- which is a REAL case
|
|
// (the limp set is optional), so callers must tolerate it.
|
|
//#############################################################################
|
|
//
|
|
ResourceDescription::ResourceID *
|
|
Mech::ResolveAnimationClip(const char *prefix, const char *suffix)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(prefix);
|
|
Check_Pointer(suffix);
|
|
|
|
char
|
|
clip_name[12];
|
|
|
|
strcpy(clip_name, prefix);
|
|
strcat(clip_name, suffix);
|
|
|
|
ResourceDescription
|
|
*description = application->GetResourceFile()->FindResourceDescription(
|
|
clip_name,
|
|
ResourceDescription::AnimationResourceType,
|
|
ResourceDescription::NullResourceID);
|
|
|
|
return (description != NULL) ? &description->resourceID : NULL;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// @004a8054 -- bind the clip at animationClips[slot] into the leg channel and
|
|
// integrate its keyframe strides. Returns (via the out parameters) the total
|
|
// cycle distance and the final keyframe time; the loader divides total by
|
|
// time to recover a cycle speed.
|
|
//
|
|
// The callback is NULL on purpose: measurement only ever PARSES the clip
|
|
// (SelectSequence), it never plays it, so the finished callback can never
|
|
// fire. The binary passes a live pointer here; NULL is behaviourally
|
|
// identical and avoids arming a transition machine mid-load. [T3]
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech::MeasureClipStride(int slot, Scalar *total, Scalar *last_key)
|
|
{
|
|
Check(this);
|
|
Verify(slot >= 0 && slot < AnimationSlotCount);
|
|
|
|
legAnimation.SelectSequence(animationClips[slot], NULL, 0, 0);
|
|
|
|
*total = 0.0f;
|
|
*last_key = 0.0f;
|
|
|
|
int
|
|
frame;
|
|
for (frame = 0; frame < legAnimation.keyframeCount; ++frame)
|
|
{
|
|
Scalar
|
|
frame_time = legAnimation.keyframeTimes[frame];
|
|
|
|
*total += (frame_time - *last_key) *
|
|
legAnimation.keyframeData[frame].stride;
|
|
*last_key = frame_time;
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// @004a80d4 -- resolve and cache every gait clip, measuring the gait
|
|
// constants from the clips themselves as it goes. This is where standSpeed,
|
|
// walkStrideLength, reverseSpeedMax, reverseStrideLength, gimpSpeedMax and
|
|
// gimpStrideLength actually COME FROM -- they are properties of the authored
|
|
// animations, not authored numbers.
|
|
//
|
|
// Two binary behaviours reproduced deliberately; neither is a transcription
|
|
// slip. See the sidecar before "fixing" either:
|
|
//
|
|
// * The speed caps read keyframeData[keyframeCount] -- one entry PAST the
|
|
// last frame (the binary reads 0x690 + 8 + [0x670]*0xc).
|
|
//
|
|
// * The reverse-cycle stride divides the bbl measurement by STALE data:
|
|
// both bbr and bbl are measured into the same pair, so the divide takes
|
|
// its second terms from whatever the run cycle left behind. A 1995
|
|
// copy-paste bug, shipped, and therefore reproduced -- the walk and run
|
|
// cycles above it show what was obviously intended.
|
|
//
|
|
// DIVERGENCE FROM THE BINARY, on purpose: the binary dereferences every
|
|
// ResolveAnimationClip result unguarded -- a mech whose model lacks a
|
|
// MANDATORY clip crashes on load. Here a miss stores NullResourceID (which
|
|
// SelectSequence resolves to an empty, inert controller) and the dependent
|
|
// measurement is skipped, leaving the bring-up default in place. [T3: keeps
|
|
// the current boot alive on models whose clip sets have not been verified;
|
|
// revisit once every fleet mech is known-good.]
|
|
//#############################################################################
|
|
//
|
|
|
|
//
|
|
// Resolve one slot: store the clip ID or NullResourceID. Returns whether the
|
|
// clip exists, so dependent measurements can be skipped on a miss.
|
|
//
|
|
int
|
|
Mech::LoadClipSlot(int slot, const char *prefix, const char *suffix)
|
|
{
|
|
ResourceDescription::ResourceID
|
|
*clip_ID = ResolveAnimationClip(prefix, suffix);
|
|
|
|
animationClips[slot] =
|
|
(clip_ID != NULL) ? *clip_ID : ResourceDescription::NullResourceID;
|
|
|
|
return clip_ID != NULL;
|
|
}
|
|
|
|
void
|
|
Mech::LoadLocomotionClips(ModelResource *model)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(model);
|
|
|
|
const char
|
|
*prefix = model->animationPrefix;
|
|
//
|
|
// Zero-initialized because the guarded skips below can reach the reverse
|
|
// divide with the run pair unmeasured -- a path the (unguarded) binary
|
|
// does not have, so the stale-pair reproduction must not become an
|
|
// uninitialized read on top of it.
|
|
//
|
|
Scalar
|
|
total_a = 0.0f, last_a = 0.0f,
|
|
total_b = 0.0f, last_b = 0.0f;
|
|
|
|
gyroRumbleTimer = 0.0f;
|
|
|
|
//
|
|
// Stand -> walk. standSpeed is the clip's final-entry stride.
|
|
//
|
|
if (LoadClipSlot(5, prefix, "swr"))
|
|
{
|
|
legAnimation.SelectSequence(animationClips[5], NULL, 0, 0);
|
|
standSpeed =
|
|
legAnimation.keyframeData[legAnimation.keyframeCount].stride;
|
|
}
|
|
|
|
//
|
|
// The forward walk cycle: stride = (s6 + s7) / (d6 + d7).
|
|
//
|
|
if (
|
|
LoadClipSlot(6, prefix, "wwr") &&
|
|
LoadClipSlot(7, prefix, "wwl")
|
|
)
|
|
{
|
|
MeasureClipStride(6, &total_a, &last_a);
|
|
MeasureClipStride(7, &total_b, &last_b);
|
|
walkStrideLength = (total_a + total_b) / (last_a + last_b);
|
|
}
|
|
|
|
LoadClipSlot(8, prefix, "wsr");
|
|
LoadClipSlot(9, prefix, "wsl");
|
|
|
|
//
|
|
// Walk -> run. reverseSpeedMax is measured from wrr the same way
|
|
// standSpeed is from swr.
|
|
//
|
|
if (LoadClipSlot(10, prefix, "wrr"))
|
|
{
|
|
legAnimation.SelectSequence(animationClips[10], NULL, 0, 0);
|
|
reverseSpeedMax =
|
|
legAnimation.keyframeData[legAnimation.keyframeCount].stride;
|
|
}
|
|
LoadClipSlot(11, prefix, "wrl");
|
|
|
|
//
|
|
// The run cycle.
|
|
//
|
|
if (
|
|
LoadClipSlot(12, prefix, "rrr") &&
|
|
LoadClipSlot(13, prefix, "rrl")
|
|
)
|
|
{
|
|
MeasureClipStride(12, &total_a, &last_a);
|
|
MeasureClipStride(13, &total_b, &last_b);
|
|
reverseStrideLength = (total_a + total_b) / (last_a + last_b);
|
|
}
|
|
|
|
LoadClipSlot(14, prefix, "rwr");
|
|
LoadClipSlot(15, prefix, "rwl");
|
|
|
|
//
|
|
// The bump/crash stagger clip, slot 0x20 -- the reason the clip array is
|
|
// bigger than the state-name table.
|
|
//
|
|
LoadClipSlot(0x20, prefix, "bmp");
|
|
|
|
//
|
|
// The reverse set. gimpSpeedMax is measured from the entry clip; the
|
|
// cycle stride divide below reproduces the binary's stale-pair bug (see
|
|
// the header comment) and is negated exactly where the binary negates.
|
|
//
|
|
if (LoadClipSlot(16, prefix, "sbr"))
|
|
{
|
|
legAnimation.SelectSequence(animationClips[16], NULL, 0, 0);
|
|
gimpSpeedMax =
|
|
legAnimation.keyframeData[legAnimation.keyframeCount].stride;
|
|
}
|
|
LoadClipSlot(17, prefix, "sbl");
|
|
LoadClipSlot(20, prefix, "bsr");
|
|
LoadClipSlot(21, prefix, "bsl");
|
|
|
|
if (
|
|
LoadClipSlot(18, prefix, "bbr") &&
|
|
LoadClipSlot(19, prefix, "bbl")
|
|
)
|
|
{
|
|
MeasureClipStride(18, &total_a, &last_a);
|
|
MeasureClipStride(19, &total_a, &last_a); // the binary's stale pair:
|
|
// total_b/last_b still hold
|
|
// the run-cycle figures
|
|
gimpStrideLength = (total_a + total_b) / (last_a + last_b);
|
|
gimpStrideLength = -gimpStrideLength;
|
|
}
|
|
|
|
//
|
|
// The OPTIONAL limp set. Probe for wgl; a model without it has no limp
|
|
// clips at all, and the limp machine must never be entered for it.
|
|
//
|
|
hasGimpClips = 0;
|
|
if (ResolveAnimationClip(prefix, "wgl") != NULL)
|
|
{
|
|
hasGimpClips = 1;
|
|
|
|
if (LoadClipSlot(22, prefix, "wgl"))
|
|
{
|
|
legAnimation.SelectSequence(animationClips[22], NULL, 0, 0);
|
|
gimpLeftSpeedMax =
|
|
legAnimation.keyframeData[legAnimation.keyframeCount].stride;
|
|
}
|
|
if (LoadClipSlot(23, prefix, "wgr"))
|
|
{
|
|
legAnimation.SelectSequence(animationClips[23], NULL, 0, 0);
|
|
gimpRightSpeedMax =
|
|
legAnimation.keyframeData[legAnimation.keyframeCount].stride;
|
|
}
|
|
if (LoadClipSlot(24, prefix, "ggr"))
|
|
{
|
|
MeasureClipStride(24, &total_a, &last_a);
|
|
gimpLeftStrideLength = total_a / last_a;
|
|
}
|
|
if (LoadClipSlot(25, prefix, "ggl"))
|
|
{
|
|
MeasureClipStride(25, &total_a, &last_a);
|
|
gimpRightStrideLength = total_a / last_a;
|
|
}
|
|
LoadClipSlot(26, prefix, "gsl");
|
|
LoadClipSlot(27, prefix, "gsr");
|
|
}
|
|
|
|
Check_Fpu();
|
|
}
|