feat(tabletop): let candidates inherit and override route stacking

Candidates now carry their own stacking strategy, inheriting the route's
when absent, so the default tilt applies consistently. Drop the now
redundant explicit tilt from the poker deck.
This commit is contained in:
2026-08-10 00:27:01 +08:00
parent 43c6334413
commit 8d0e393100
5 changed files with 22 additions and 3 deletions
+1 -1
View File
@@ -341,7 +341,7 @@ string,number,number,number
The router should select only the first candidate with all params matched against its props — the fields in the candidate's CSV row (e.g. `:seat` matches the candidate's `seat` value).
When no candidates match, the whole route fails to match.
A candidate inherits the route's `x`, `y`, `rotation`, and `stacking`, and may override any of them with its own values. When no candidates match, the whole route fails to match.
### Stacking
-1
View File
@@ -106,7 +106,6 @@ layout:
align: center
zStart: 0
#zEnd: 100
tilt: 1
curve: M -50 -200 C 50 -150 450 -150 550 -200
- route: /community/:slot
candidates:
+2
View File
@@ -84,6 +84,8 @@ export interface Candidate {
x?: number;
y?: number;
rotation?: number;
/** Stacking strategy; overrides the route's when set, else inherits it. */
stacking?: Stacking;
}
export interface Route {
+3 -1
View File
@@ -17,7 +17,9 @@ export function PartPlacement({ pkg, placement }: { pkg: Package; placement: Pla
const part = pkg.parts.get(piece.split(':').slice(1).join(':'));
if (!part) return null;
const { x, y, rotation, z, tilt } = useStacking(route.stacking, index, stackSize);
// A candidate inherits the route's stacking unless it overrides it.
const stacking = candidate?.stacking ?? route.stacking;
const { x, y, rotation, z, tilt } = useStacking(stacking, index, stackSize);
// Route anchors and stacking offsets are in mm; convert to world units so
// parts land on the (world-scaled) surface. `z` raises the part along the
+16
View File
@@ -98,6 +98,22 @@ describe('computeSurfacePlacements', () => {
expect(placements[0]!.candidate).toEqual({ seat: '0', x: 40, y: 5, rotation: 1 });
});
it('keeps the candidate stacking alongside its anchor', () => {
const surface = makeSurface({
layout: [
{
route: '/dock/:seat',
x: 0,
y: 0,
rotation: 0,
candidates: [{ seat: '0', x: 40, y: 5, stacking: { tilt: 2 } }],
},
],
});
const placements = computeSurfacePlacements(surface, { '/dock/0': ['harbor:boat#fleet'] });
expect(placements[0]!.candidate).toEqual({ seat: '0', x: 40, y: 5, stacking: { tilt: 2 } });
});
it('keeps the same piece on two paths as distinct placements', () => {
const surface = makeSurface({
layout: [