Everyone was driving on the left
This world is right-handed with Y up, so a car facing +Z has its right-hand side toward -X. The lane offset used (dirZ, -dirX), which is +X, which is left. Every vehicle in the game drove on the wrong side of the road. It was invisible for the usual reason: traffic still held neat parallel lines and still passed oncoming cars without touching, just on the wrong side. And the test agreed, because the test computed its idea of "right" by copying the sim's vector, so it inherited the same inverted sign and cheerfully certified the whole map. Confirmed three ways before changing anything: the geometry, the HUD's own compass - which reads +X at heading 0 as a left turn - and driving the car, where holding W and D from heading 0 ends up at negative x. The test now derives nothing from sim/units.ts. It asks the HUD's compass which way the lane offset points and requires the answer to be "right". Two modules that must agree, written independently, is the only version of this test that can fail; inverting the vector under it now fails all three cases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b4f8fd6c99
commit
627eca2f3d
@ -20,6 +20,7 @@ import {
|
|||||||
type UnitState,
|
type UnitState,
|
||||||
} from './units';
|
} from './units';
|
||||||
import { createCombat, segmentHits, stepCombat } from './combat';
|
import { createCombat, segmentHits, stepCombat } from './combat';
|
||||||
|
import { arrowFor } from '../ui/hud';
|
||||||
|
|
||||||
const world = generateWorld(1337);
|
const world = generateWorld(1337);
|
||||||
const graph = buildGraph(world.roads);
|
const graph = buildGraph(world.roads);
|
||||||
@ -735,11 +736,18 @@ describe('nobody fires at what they cannot see', () => {
|
|||||||
describe('traffic that drives like traffic', () => {
|
describe('traffic that drives like traffic', () => {
|
||||||
/** Where each car sits relative to the centre of the road it is on. */
|
/** 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]) => {
|
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 along = Math.atan2(seg.bx - seg.ax, seg.bz - seg.az);
|
||||||
const px = unit.x - seg.ax;
|
const px = unit.x - seg.ax;
|
||||||
const pz = unit.z - seg.az;
|
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.
|
// Flip for cars travelling the other way down the same segment.
|
||||||
return Math.cos(unit.heading - along) >= 0 ? lateral : -lateral;
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -423,9 +423,20 @@ function advance(unit: Unit, roads: RoadNetwork, state: UnitState, dt: number):
|
|||||||
dirX /= legLength;
|
dirX /= legLength;
|
||||||
dirZ /= legLength;
|
dirZ /= legLength;
|
||||||
}
|
}
|
||||||
// Right of the direction of travel.
|
/*
|
||||||
const rightX = dirZ;
|
* Right of the direction of travel.
|
||||||
const rightZ = -dirX;
|
*
|
||||||
|
* 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 halfWidth = (segmentBetween(roads, unit.lastNode ?? next, next)?.width ?? 9) / 2;
|
||||||
const offset = halfWidth * LANE_SHARE;
|
const offset = halfWidth * LANE_SHARE;
|
||||||
|
|||||||
@ -65,7 +65,14 @@ export interface HudModel {
|
|||||||
/** Eight-point compass, starting at "straight ahead" and turning left. */
|
/** Eight-point compass, starting at "straight ahead" and turning left. */
|
||||||
const ARROWS = ['↑', '↖', '←', '↙', '↓', '↘', '→', '↗'];
|
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));
|
const sector = Math.round(bearing / (Math.PI / 4));
|
||||||
return ARROWS[((sector % 8) + 8) % 8]!;
|
return ARROWS[((sector % 8) + 8) % 8]!;
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user