Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/TOTALA-EXE.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,12 @@ the job rather than on the flight path.
moved the construction-aircraft bank peak by 0.00° (a construction aircraft
never reaches its `BrakeRate` of 1.5) and nothing faster had been measured.
Measured now with the shipped fighters' numbers: the worst crab angle through
a ninety-degree turn drops to a few degrees (`brakerate.test.cpp`). Still not
applied in the attack-run, gunship and dogfight states, which steer their own
velocities; those want their own pass.
a ninety-degree turn drops to a few degrees (`brakerate.test.cpp`). Applied
in every air state since B4 #41: `Mover::Update` (`0x43DD20`, §87) hands any
`canfly` unit to `0x43D290` whatever its mission, so the gunship ring and the
dogfight, which already flew through the flying state's function, had it
from the first port, and the attack run, which steers its own heading, now
runs the brake step before that swing.
- **Pitch.** The original also pitches aircraft from the longitudinal component
of the same accumulator, via `PitchScale` (`def+0x1A6`) into `unit+0x68`. RWE
has no pitch for units at all and the renderer applies only yaw and roll.
Expand Down
16 changes: 15 additions & 1 deletion src/rwe/sim/UnitBehaviorService_air.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ namespace rwe
auto state = makeAttackRunState(SimVector(0_ss, 50_ss, 0_ss));
state.phase = AirMovementStateAttackRun::Phase::Approaching;
state.currentVelocity = SimVector(4_ss, 0_ss, 0_ss);
// Pointing the way it flies, as it would be in the game.
unit.rotation = UnitState::toRotation(state.currentVelocity);

// Fly the reversal out and check it never slows to a hover.
auto slowest = state.currentVelocity.length();
Expand Down Expand Up @@ -327,11 +329,20 @@ namespace rwe
auto state = makeAttackRunState(SimVector(0_ss, 50_ss, 0_ss));
state.phase = AirMovementStateAttackRun::Phase::Approaching;
state.currentVelocity = SimVector(4_ss, 0_ss, 0_ss);
// Pointing the way it flies: with the nose left on its default
// heading the brake step would veer the aircraft off along it on
// the first tick and the reversal would be over in four.
unit.rotation = UnitState::toRotation(state.currentVelocity);

for (int tick = 0; tick < 200 && state.currentVelocity.x >= 0_ss; ++tick)
{
state.currentVelocity = computeNewAttackRunVelocity(unit, def, state);
unit.position = unit.position + state.currentVelocity;
// The nose follows the flight path, as updateUnitRotation
// turns it; the brake step redirects speed above BrakeRate
// along it, so a nose left on its default heading would
// hold the aircraft on that heading for the whole loop.
unit.rotation = UnitState::toRotation(state.currentVelocity);
}

// Half a circle of radius r displaces the aircraft about 2r sideways.
Expand Down Expand Up @@ -420,8 +431,11 @@ namespace rwe
auto state = makeAttackRunState(SimVector(100_ss, 50_ss, 0_ss));
state.phase = AirMovementStateAttackRun::Phase::Engaging;
state.runOutDirection = SimVector(1_ss, 0_ss, 0_ss);
// Already at max velocity
// Already at max velocity, and pointing the way it flies: the
// nose follows the run's flight path in updateUnitRotation, and
// the brake step redirects speed above BrakeRate along it.
state.currentVelocity = SimVector(4_ss, 0_ss, 0_ss);
unit.rotation = UnitState::toRotation(SimVector(1_ss, 0_ss, 0_ss));
auto v = computeNewAttackRunVelocity(unit, def, state);
// velocity magnitude should remain at maxVelocity (4)
REQUIRE(v.length() > 3.99_ssf);
Expand Down
13 changes: 11 additions & 2 deletions src/rwe/sim/UnitBehaviorService_util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -920,17 +920,26 @@ namespace rwe
{
auto targetPoint = computeAttackRunTargetPoint(unit, unitDefinition, physics);

// The brake step first, as in the original, where every air mission
// goes through the one per-tick mover (Mover::Update at 0x43DD20
// picks 0x43D290 for any `canfly` unit, §87) and so through the
// step at 0x43D38E before the heading is touched. A Thunder's
// BrakeRate of 0.4 against its MaxVelocity of 9 means nearly all of
// its speed is pulled round to its nose every tick: a bomber flies
// where it points.
auto currentVelocity = applyBrakeRateNoseReaim(physics.currentVelocity, UnitState::toDirection(unit.rotation), unitDefinition.brakeRate);

// Speed: always winding up towards the aircraft's best. A run never
// brakes — flying slower does not help it hit anything.
auto speed = physics.currentVelocity.length();
auto speed = currentVelocity.length();
speed = rweMin(unitDefinition.maxVelocity, speed + unitDefinition.acceleration);

// Heading: an aircraft cannot slide sideways, it banks. Swing the
// current heading towards the aim point by at most one tick's worth
// of turn, so coming back for another pass is a arc of radius
// speed / turnRate flown at full speed, rather than a stop and a
// pivot on the spot.
SimVector flatVelocity(physics.currentVelocity.x, 0_ss, physics.currentVelocity.z);
SimVector flatVelocity(currentVelocity.x, 0_ss, currentVelocity.z);
auto currentHeading = flatVelocity.lengthSquared() > 0_ss
? UnitState::toRotation(flatVelocity)
: unit.rotation;
Expand Down
87 changes: 87 additions & 0 deletions src/rwe/sim/brakerate.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ namespace rwe
UnitDefinition corvamp() { return fighterFromFbi(12_ss, 7_ss, SimScalar(0.35f), 620_ss); }
/** ARMCA, the construction aircraft: MaxVelocity=6.9 BrakeRate=1.5 Acceleration=0.06 TurnRate=90. */
UnitDefinition armca() { return fighterFromFbi(SimScalar(6.9f), SimScalar(1.5f), SimScalar(0.06f), 90_ss); }
/** ARMTHUND, the Thunder bomber: MaxVelocity=9 BrakeRate=0.4 Acceleration=0.08 TurnRate=356. */
UnitDefinition armthund() { return fighterFromFbi(9_ss, SimScalar(0.4f), SimScalar(0.08f), 356_ss); }
/** ARMBRAWL, the Brawler gunship: MaxVelocity=6.6 BrakeRate=4 Acceleration=0.16 TurnRate=800, HoverAttack=1. */
UnitDefinition armbrawl() { return fighterFromFbi(SimScalar(6.6f), 4_ss, SimScalar(0.16f), 800_ss); }

/** A unit at the origin with its nose on +z, flying flat out along +x: the worst crab there is. */
UnitState crabbingUnit()
{
std::vector<UnitMesh> pieces;
UnitState unit(pieces, std::unique_ptr<CobEnvironment>{});
unit.position = SimVector(0_ss, 100_ss, 0_ss);
unit.previousPosition = unit.position;
unit.rotation = UnitState::toRotation(SimVector(0_ss, 0_ss, 1_ss));
return unit;
}

float horizontalSpeed(const SimVector& v)
{
Expand Down Expand Up @@ -177,4 +192,76 @@ namespace rwe
REQUIRE(caWith <= worstCrabThroughTurn(caWithout) + 1.0f);
}
}

TEST_CASE("brake step: every air state goes through it, as every air mission does in the original", "[brakerate]")
{
// Mover::Update (0x43DD20) picks the air follower 0x43D290 for any
// canfly unit whatever its mission (TOTALA-EXE.md S:87), and the
// brake step is inside it. Each state here starts a unit flying flat
// out along +x with its nose on +z and a goal far off along +x, and
// reads what one tick does to the sideways speed. With the rule the
// crosswise component drops to BrakeRate (give or take a tick's
// acceleration); with BrakeRate out of reach it stays where it was.
SECTION("the bomber's attack run, ARMTHUND")
{
auto def = armthund();
auto unit = crabbingUnit();
AirMovementStateAttackRun run;
run.phase = AirMovementStateAttackRun::Phase::Approaching;
run.lastKnownTargetPos = SimVector(100000_ss, 100_ss, 0_ss);
run.runOutDirection = SimVector(1_ss, 0_ss, 0_ss);
run.currentVelocity = SimVector(9_ss, 0_ss, 0_ss);

auto with = computeNewAttackRunVelocity(unit, def, run);
// 0.4 stays across, 8.6 goes along the nose, then the run swings
// the heading a tick's turn back towards the target.
REQUIRE(simScalarToFloat(with.z) > simScalarToFloat(with.x));
REQUIRE(simScalarToFloat(with.z) > 7.0f);

auto without = def;
without.brakeRate = 1000_ss;
auto flat = computeNewAttackRunVelocity(unit, without, run);
REQUIRE(simScalarToFloat(flat.x) > 8.0f);
}

SECTION("the gunship ring, ARMBRAWL")
{
auto def = armbrawl();
auto unit = crabbingUnit();
AirMovementStateHoverAttack ring;
ring.station = SimVector(100000_ss, 100_ss, 0_ss);
ring.targetPosition = ring.station;
ring.currentVelocity = SimVector(SimScalar(6.6f), 0_ss, 0_ss);

auto with = computeNewHoverAttackVelocity(unit, def, ring);
REQUIRE(simScalarToFloat(with.x) == Approx(4.0f).margin(0.2f));
REQUIRE(simScalarToFloat(with.z) > 2.0f);

auto without = def;
without.brakeRate = 1000_ss;
auto flat = computeNewHoverAttackVelocity(unit, without, ring);
REQUIRE(simScalarToFloat(flat.x) > 6.0f);
REQUIRE(simScalarToFloat(flat.z) < 0.2f);
}

SECTION("the dogfight, ARMFIG")
{
auto def = armfig();
auto unit = crabbingUnit();
AirMovementStateDogfight fight;
fight.phase = AirMovementStateDogfight::Phase::Pursuing;
fight.goalPosition = SimVector(100000_ss, 100_ss, 0_ss);
fight.currentVelocity = SimVector(10_ss, 0_ss, 0_ss);

auto with = computeNewDogfightVelocity(unit, def, fight);
REQUIRE(simScalarToFloat(with.x) == Approx(6.0f).margin(0.4f));
REQUIRE(simScalarToFloat(with.z) > 3.0f);

auto without = def;
without.brakeRate = 1000_ss;
auto flat = computeNewDogfightVelocity(unit, without, fight);
REQUIRE(simScalarToFloat(flat.x) > 9.0f);
REQUIRE(simScalarToFloat(flat.z) < 0.4f);
}
}
}