diff --git a/src/sim/units.test.ts b/src/sim/units.test.ts index 4a034c4..40aef58 100644 --- a/src/sim/units.test.ts +++ b/src/sim/units.test.ts @@ -20,6 +20,7 @@ import { type UnitState, } from './units'; import { createCombat, segmentHits, stepCombat } from './combat'; +import { arrowFor } from '../ui/hud'; const world = generateWorld(1337); const graph = buildGraph(world.roads); @@ -735,11 +736,18 @@ describe('nobody fires at what they cannot see', () => { describe('traffic that drives like traffic', () => { /** Where each car sits relative to the centre of the road it is on. */ const sideOfRoad = (unit: { x: number; z: number; heading: number }, seg: (typeof world.roads.segments)[number]) => { - // Signed offset from the segment's centreline, positive to the car's right. + // Signed offset from the centreline, positive to the *car's* right. + // + // Right-handed world, Y up: something facing +Z has its right toward -X. + // So for a forward of (sin a, cos a) the right vector is (-cos a, sin a). + // Deriving this independently of the sim is the whole value of the test — + // the first version copied the sim's vector, inherited its inverted sign, + // and cheerfully certified that everything drove on the correct side while + // the entire map drove on the left. const along = Math.atan2(seg.bx - seg.ax, seg.bz - seg.az); const px = unit.x - seg.ax; const pz = unit.z - seg.az; - const lateral = px * Math.cos(along) - pz * Math.sin(along); + const lateral = -px * Math.cos(along) + pz * Math.sin(along); // Flip for cars travelling the other way down the same segment. return Math.cos(unit.heading - along) >= 0 ? lateral : -lateral; }; @@ -893,3 +901,47 @@ describe('your own side, with vehicles', () => { } }); }); + +describe('which way is right', () => { + /** + * The lane offset is the one piece of geometry in this file where getting the + * sign backwards is both easy and completely invisible: everything still + * drives in neat parallel lines, just on the wrong side of the road. The + * first version of it did exactly that. + * + * So it is checked against the HUD's compass rather than against another copy + * of the same reasoning. Two modules that must agree, written independently, + * is the only version of this test that can actually fail. + */ + const laneRightOf = (headingRad: number) => { + const dirX = Math.sin(headingRad); + const dirZ = Math.cos(headingRad); + // Must match sim/units.ts `advance`. + return { x: -dirZ, z: dirX }; + }; + + const bearingTo = (headingRad: number, to: { x: number; z: number }) => + Math.atan2(to.x, to.z) - headingRad; + + it('offsets cars toward the side the HUD calls right', () => { + for (const heading of [0, Math.PI / 2, Math.PI, -Math.PI / 2, 0.7, -2.4]) { + const right = laneRightOf(heading); + expect(arrowFor(bearingTo(heading, right))).toBe('→'); + } + }); + + it('is not merely self-consistent — the other side reads as left', () => { + for (const heading of [0, 1.2, -0.9]) { + const right = laneRightOf(heading); + expect(arrowFor(bearingTo(heading, { x: -right.x, z: -right.z }))).toBe('←'); + } + }); + + it('agrees with the car: steering right from heading 0 goes toward -X', () => { + // Measured on the running game with Rapier: hold W and D from heading 0 and + // the car ends up at negative x. The sim layer cannot import the physics, + // so the number is recorded here instead of re-derived. + expect(laneRightOf(0).x).toBeLessThan(0); + expect(laneRightOf(0).z).toBeCloseTo(0, 9); + }); +}); diff --git a/src/sim/units.ts b/src/sim/units.ts index 86916cb..c2a1f81 100644 --- a/src/sim/units.ts +++ b/src/sim/units.ts @@ -423,9 +423,20 @@ function advance(unit: Unit, roads: RoadNetwork, state: UnitState, dt: number): dirX /= legLength; dirZ /= legLength; } - // Right of the direction of travel. - const rightX = dirZ; - const rightZ = -dirX; + /* + * Right of the direction of travel. + * + * Worth deriving rather than guessing, because the obvious form is the wrong + * one and it is invisible in code: this world is right-handed with Y up, so a + * car facing +Z has its right-hand side toward **-X**, not +X. Confirmed by + * the HUD's own compass — which reads +X at heading 0 as a left turn — and by + * driving the thing: hold W and D from heading 0 and the car goes to -X. + * + * The first version of this had the sign the other way round and every + * vehicle in the game quietly drove on the left. + */ + const rightX = -dirZ; + const rightZ = dirX; const halfWidth = (segmentBetween(roads, unit.lastNode ?? next, next)?.width ?? 9) / 2; const offset = halfWidth * LANE_SHARE; diff --git a/src/ui/hud.ts b/src/ui/hud.ts index 86311dd..9177670 100644 --- a/src/ui/hud.ts +++ b/src/ui/hud.ts @@ -65,7 +65,14 @@ export interface HudModel { /** Eight-point compass, starting at "straight ahead" and turning left. */ const ARROWS = ['↑', '↖', '←', '↙', '↓', '↘', '→', '↗']; -const arrowFor = (bearing: number): string => { +/** + * Exported so the world and the HUD can be held to the same idea of "right". + * This world is right-handed with Y up, so a car facing +Z has its right-hand + * side toward -X — which is easy to get backwards in code and invisible when + * you do, so sim/units.ts is tested against this function rather than against + * its own copy of the convention. + */ +export const arrowFor = (bearing: number): string => { const sector = Math.round(bearing / (Math.PI / 4)); return ARROWS[((sector % 8) + 8) % 8]!; };