A Heavy Grade shows which way it climbs
Jesse: "heavy grade mainline card tooltip states climbs east, but card doesn't show it." The Frame has carried gradeUp all along and the tip has said it; the card drew nothing, so the one Mainline card whose orientation is set per game was the one you had to hover to read. A brown wedge in the lower right rising toward the climb, with a bone arrow lying along its slope, centred on the triangle's centroid. East is right on this map (Gitea#18), which is what lets a wedge be read without a compass. Four passes. Up the hypotenuse the arrow began at the wedge's thin corner, where there is no height, so its head read as clipped. Level, it was contained but did not read as climbing. At 30° — steeper than the wedge's own 22.5° — it had to be tucked into the fat half. Parallel to the slope is the shape that fits: the gap to the hypotenuse is then constant, so the arrow can sit centred. The wedge grew to 58x24 to pay for it, since a centred arrow has less room than an off-centre one. Sizes are a search result, clearing every edge by 2.88px. The first containment test bounded the arrow against the CARD, which it never left, while the wedge clipped it — a green check on a visibly broken glyph. It checks the TRIANGLE now with a 2px floor, plus parallelism derived from the wedge and centroid placement. Orientation is always set, measured not assumed: 400 seeds x 4 player counts, 535 grades placed, 0 without one, 276 east / 259 west. Stays in the unshipped v0.7.9. 874 tests pass, one new. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
45cf521a40
commit
cf018b4a5f
@@ -146,6 +146,12 @@ export function divisionSvg(nodes: DivisionView[], roster?: DivisionRoster | nul
|
||||
below?: Cell['trains'];
|
||||
/** Mainline cards only: §2.1 divides one into two regions. 0 elsewhere — no bars are drawn. */
|
||||
regions: number;
|
||||
/**
|
||||
* Which way a Heavy Grade climbs, or null on every other card. Drawn as a wedge, because the
|
||||
* tooltip said "climbs east" and the card itself showed nothing — so the one card whose
|
||||
* orientation the PLAYER chooses was the one card you had to hover to read (Jesse, 2026-08-30).
|
||||
*/
|
||||
gradeUp?: string | null;
|
||||
w: number;
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -259,6 +265,7 @@ export function divisionSvg(nodes: DivisionView[], roster?: DivisionRoster | nul
|
||||
seat: null,
|
||||
// A Division Point is one region — the queue trains enter and leave the Division through.
|
||||
regions: dp ? 1 : (n.regions ?? 0),
|
||||
gradeUp: dp ? null : (n.gradeUp ?? null),
|
||||
w: dp ? CW.dp : CW.ml,
|
||||
});
|
||||
}
|
||||
@@ -357,6 +364,73 @@ export function divisionSvg(nodes: DivisionView[], roster?: DivisionRoster | nul
|
||||
out += `<line class="bs-region" x1="${c.x + 6 + RW * r}" y1="${c.y + RAIL_Y - 14}" x2="${c.x + 6 + RW * r}" y2="${c.y + RAIL_Y + 10}"/>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* WHICH WAY A HEAVY GRADE CLIMBS, drawn rather than only said.
|
||||
*
|
||||
* The tooltip has said "climbs east" since the Frame carried `gradeUp`, and the card showed
|
||||
* nothing — so the one Mainline card whose orientation the PLAYER sets, and the one where a
|
||||
* Helpers or Brakeman modifier means opposite things at opposite ends, was the one you had to
|
||||
* hover to read (Jesse, 2026-08-30).
|
||||
*
|
||||
* A wedge rising toward the climb, with an arrow up its slope. Two cues rather than one: the
|
||||
* wedge alone asks the reader to judge which end is taller, which at eleven pixels of rise is a
|
||||
* comparison rather than a glance. Bottom-right, clear of the left-aligned capacity line and
|
||||
* below the region bars, so it never lands under a train chip.
|
||||
*
|
||||
* `east` is RIGHT on this map and always has been (Gitea#18) — that is what makes a wedge
|
||||
* readable without a compass, and it is why the row layout is worth its width.
|
||||
*/
|
||||
if (c.gradeUp === 'east' || c.gradeUp === 'west') {
|
||||
const s = c.gradeUp === 'east' ? 1 : -1;
|
||||
const GW = 58;
|
||||
const RISE = 24;
|
||||
const gx1 = c.x + c.w - 9 - GW;
|
||||
const gx2 = c.x + c.w - 9;
|
||||
const yb = c.y + CH - 6;
|
||||
const peakX = s === 1 ? gx2 : gx1;
|
||||
out += `<polygon class="bs-grade" points="${gx1},${yb} ${gx2},${yb} ${peakX},${yb - RISE}"/>`;
|
||||
|
||||
/**
|
||||
* THE ARROW LIES ALONG THE WEDGE'S OWN SLOPE, CENTRED IN IT (Jesse, 2026-08-30).
|
||||
*
|
||||
* Parallel to the hypotenuse is the shape that fits: the perpendicular gap to the slope is
|
||||
* then CONSTANT along the whole arrow, instead of closing at one end the way a steeper line
|
||||
* does. The earlier 30° pass had to be tucked into the fat half to survive, because 30° is
|
||||
* steeper than this wedge climbs — at 58×24 the slope is 22.5°, and the arrow simply lies on
|
||||
* it.
|
||||
*
|
||||
* Centred on the TRIANGLE'S CENTROID (2/3 along the base, 1/3 up), which is the balance point
|
||||
* of the form rather than of its bounding box — centring on the box would push the arrow into
|
||||
* the thin corner where there is no height for it.
|
||||
*
|
||||
* The wedge grew 50×22 → 58×24 to pay for that: the centroid sits only ~7px from the
|
||||
* hypotenuse, so a centred arrow has less room than an off-centre one and needs a bigger form
|
||||
* to keep it. Sizes are the best fit found by search, clearing every edge by 2.88px;
|
||||
* `web.test.ts` re-derives it and fails under 2px.
|
||||
*
|
||||
* Worked in the wedge's own frame — `u` along the base from the thin corner, `h` up from it —
|
||||
* so a westward climb is one sign on `u` rather than a second set of coordinates.
|
||||
*/
|
||||
const L = 20;
|
||||
const HL = 6;
|
||||
const HW = 3.5;
|
||||
const T = 1.4;
|
||||
const A = Math.atan2(RISE, GW);
|
||||
const cos = Math.cos(A);
|
||||
const sin = Math.sin(A);
|
||||
const cu = (2 * GW) / 3;
|
||||
const ch = RISE / 3;
|
||||
const pt = (lx: number, ly: number): string => {
|
||||
const u = cu + lx * cos - ly * sin;
|
||||
const h = ch + lx * sin + ly * cos;
|
||||
return `${s === 1 ? gx1 + u : gx2 - u},${yb - h}`;
|
||||
};
|
||||
const H = L / 2;
|
||||
out +=
|
||||
`<polygon class="bs-gradeup" points="${pt(-H, -T)} ${pt(H - HL, -T)} ${pt(H - HL, -HW)} ` +
|
||||
`${pt(H, 0)} ${pt(H - HL, HW)} ${pt(H - HL, T)} ${pt(-H, T)}"/>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* ON THE DIVISION MAP, A TRAIN IS A CHIP — name, which way it points, how many cars.
|
||||
*
|
||||
@@ -1123,6 +1197,14 @@ export const BOARD_CSS = `
|
||||
/* The vertical bars a Mainline card is divided into (§2.1). Drawn faint: they are the ruler the
|
||||
train is measured against, not something to look at instead of the train. */
|
||||
.bs-region{stroke:#4a5361;stroke-width:1.2;stroke-dasharray:3 3}
|
||||
/* THE HEAVY GRADE WEDGE. Terrain, so it is coloured as terrain rather than as a warning.
|
||||
SOLID BROWN, fill and border the same (Jesse, 2026-08-30) — the first pass paired a desaturated
|
||||
fill with an amber arrow and the pair read reddish, which on a map that spends amber on "it is
|
||||
happening here" made a fixed piece of landscape look like a live alert. One flat brown recedes
|
||||
into scenery; the arrow is bone so the DIRECTION, which is the fact being reported, is the part
|
||||
that carries. */
|
||||
.bs-grade{fill:#6b5334;stroke:#6b5334;stroke-width:1}
|
||||
.bs-gradeup{fill:#f2e8d5}
|
||||
.bs-slot{fill:none;stroke:#5f6b7a;stroke-width:1.1;stroke-dasharray:3 2}
|
||||
.bs-slot.bs-occ{stroke-dasharray:none;stroke-width:1.6}
|
||||
/* CAR TYPE BY COLOUR, LOAD STATE BY FILL — the SAME distinction the train tray draws, because they
|
||||
|
||||
Reference in New Issue
Block a user