From b25cf5201ffedffcc12c4b31ee2b7748fbcab6b0 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Sun, 27 Sep 2026 00:12:46 +0000 Subject: [PATCH] fix: keep coupled cars on their own rail height Since #36, coupledPose placed each car so its front coupler met the car ahead in all three dimensions. Where the grade changes between two cars, their couplers sit at different heights, so a car behind a locomotive on a slope was pulled up to a block below its rail, burying its wheels in the ballast. Meet the coupler ahead seen from above only, and keep the car's rail height. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../vehicles/handlers/train/Connector.java | 7 ++++++- .../vehicles/handlers/train/ConnectorTest.java | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/train/Connector.java b/src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/train/Connector.java index d770d1f..e93e49e 100644 --- a/src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/train/Connector.java +++ b/src/main/java/net/tfminecraft/vehicleframework/vehicles/handlers/train/Connector.java @@ -51,6 +51,11 @@ public Vector positionAt(TrackPose pose) { return offsetAt(pose).add(new Vector(pose.x, pose.y, pose.z)); } + /** + * Places a car so this coupler meets {@code target} seen from above. The car keeps the + * height of its own rail: where the grade changes between two cars, their couplers + * sit at different heights, and meeting exactly would lift or sink the car off its track. + */ public TrackPose coupledPose(TrackPose nominal, Vector target) { double dx = target.getX() - nominal.x; double dz = target.getZ() - nominal.z; @@ -58,7 +63,7 @@ public TrackPose coupledPose(TrackPose nominal, Vector target) { ? (float) Math.toDegrees(Math.atan2(-dx, dz)) : nominal.yaw; TrackPose facing = new TrackPose(nominal.x, nominal.y, nominal.z, yaw, nominal.pitch); Vector offset = offsetAt(facing); - return new TrackPose(target.getX() - offset.getX(), target.getY() - offset.getY(), + return new TrackPose(target.getX() - offset.getX(), nominal.y, target.getZ() - offset.getZ(), yaw, nominal.pitch); } diff --git a/src/test/java/net/tfminecraft/vehicleframework/vehicles/handlers/train/ConnectorTest.java b/src/test/java/net/tfminecraft/vehicleframework/vehicles/handlers/train/ConnectorTest.java index 87e66f9..aee1478 100644 --- a/src/test/java/net/tfminecraft/vehicleframework/vehicles/handlers/train/ConnectorTest.java +++ b/src/test/java/net/tfminecraft/vehicleframework/vehicles/handlers/train/ConnectorTest.java @@ -58,11 +58,27 @@ void passengerFrontMeetsLocomotiveBackWithRealPivotsAndBaseRotation( TrackPose nominal = new TrackPose(96, 64, 194, yaw - 25, pitch); TrackPose coupled = front.coupledPose(nominal, back.positionAt(parent)); Vector renderedFront = renderedAnchor(coupled, PASSENGER_FRONT, new Vector3f(), -entityYaw, size); - assertEquals(0, expectedBack.distance(renderedFront), 1e-5); + // Couplers meet seen from above; the car stays at its own rail height. + assertEquals(0, Math.hypot(expectedBack.getX() - renderedFront.getX(), + expectedBack.getZ() - renderedFront.getZ()), 1e-5); + assertEquals(nominal.y, coupled.y, 1e-9); assertEquals(pitch, coupled.pitch); assertEquals(new Vector3f(0, 15f / 16, 78f / 16), PASSENGER_FRONT); } + @Test + void carOnLevelTrackBehindLocomotiveOnGradeStaysOnItsRail() { + // Lab case: locomotive 11 degrees nose down, passenger car still on the level. + Connector back = connector(LOCO_BACK, LOCO_PIVOT, 88, new Vector3f(1)); + Connector front = connector(PASSENGER_FRONT, new Vector3f(), 88, new Vector3f(1)); + TrackPose loco = new TrackPose(5449.57, 430.384, 3462.5, 88.4f, 11.1f); + TrackPose nominal = new TrackPose(5457.82, 432.0, 3462.49, 88.4f, 0f); + TrackPose coupled = front.coupledPose(nominal, back.positionAt(loco)); + assertEquals(432.0, coupled.y, 1e-9); + assertEquals(nominal.x, coupled.x, 0.5); + assertEquals(nominal.z, coupled.z, 0.5); + } + private static Vector renderedAnchor(TrackPose pose, Vector3f point, Vector3f pivot, float entityYaw, Vector3f scale) { Vector3d result = new Matrix4d().translation(pose.x, pose.y, pose.z)