diff --git a/docs/TOTALA-EXE.md b/docs/TOTALA-EXE.md index a6411dbc..ff9d30d7 100644 --- a/docs/TOTALA-EXE.md +++ b/docs/TOTALA-EXE.md @@ -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. diff --git a/src/rwe/sim/UnitBehaviorService_air.test.cpp b/src/rwe/sim/UnitBehaviorService_air.test.cpp index ad71713c..e6751015 100644 --- a/src/rwe/sim/UnitBehaviorService_air.test.cpp +++ b/src/rwe/sim/UnitBehaviorService_air.test.cpp @@ -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(); @@ -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. @@ -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); diff --git a/src/rwe/sim/UnitBehaviorService_util.cpp b/src/rwe/sim/UnitBehaviorService_util.cpp index 6dca258c..8c813a26 100644 --- a/src/rwe/sim/UnitBehaviorService_util.cpp +++ b/src/rwe/sim/UnitBehaviorService_util.cpp @@ -920,9 +920,18 @@ 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 @@ -930,7 +939,7 @@ namespace rwe // 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; diff --git a/src/rwe/sim/brakerate.test.cpp b/src/rwe/sim/brakerate.test.cpp index 90b89a68..e75ce954 100644 --- a/src/rwe/sim/brakerate.test.cpp +++ b/src/rwe/sim/brakerate.test.cpp @@ -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 pieces; + UnitState unit(pieces, std::unique_ptr{}); + 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) { @@ -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); + } + } }