feat: add facing orientation to parts
Replace the face/back boolean with a three-state facing (face, back, standing) seeded from setup placements. Parts now orient about the anchor at the resting face/edge, so back-down parts sit on the table instead of below it and standing parts rest on their bottom edge.
This commit is contained in:
@@ -272,6 +272,10 @@ setup:
|
|||||||
parts: harbor:boat#fleet
|
parts: harbor:boat#fleet
|
||||||
- path: /deck
|
- path: /deck
|
||||||
parts: harbor:card
|
parts: harbor:card
|
||||||
|
facing: back
|
||||||
|
- path: /table
|
||||||
|
parts: harbor:token#wood
|
||||||
|
facing: standing
|
||||||
```
|
```
|
||||||
|
|
||||||
`surfaces` lists the surfaces enabled at the start. A surface not listed is
|
`surfaces` lists the surfaces enabled at the start. A surface not listed is
|
||||||
@@ -287,6 +291,16 @@ the deck, then move these cards to the flop".
|
|||||||
either. A bare type expands to all parts of that type during game state
|
either. A bare type expands to all parts of that type during game state
|
||||||
initialization.
|
initialization.
|
||||||
|
|
||||||
|
`facing` sets how the placed parts are oriented on the board, defaulting to
|
||||||
|
`face`:
|
||||||
|
|
||||||
|
- `face` — lay flat, front up, resting on the bottom face.
|
||||||
|
- `back` — lay flat, front down (flipped over), resting on the top face.
|
||||||
|
- `standing` — stand upright on the bottom edge, front texture still showing.
|
||||||
|
|
||||||
|
A part's `facing` is seeded into the game state and can change at runtime; it
|
||||||
|
only affects orientation, never the part's texture.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Concepts
|
## 4. Concepts
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ interface GameState {
|
|||||||
interface PartState {
|
interface PartState {
|
||||||
path: string; // the path key this part is on
|
path: string; // the path key this part is on
|
||||||
index: number; // the part's position in its path's stack
|
index: number; // the part's position in its path's stack
|
||||||
face: boolean; // whether the part's face is up
|
facing: 'face' | 'back' | 'standing'; // how the part is oriented on the board
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -21,19 +21,19 @@ source-of-truth game state:
|
|||||||
interface PartState {
|
interface PartState {
|
||||||
path: string, // the path key this part is on
|
path: string, // the path key this part is on
|
||||||
index: number, // the part's position in its path's stack
|
index: number, // the part's position in its path's stack
|
||||||
face: boolean, // whether the part's face is up
|
facing: 'face' | 'back' | 'standing', // how the part is oriented on the board
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**assumption:** each piece on the board has a unique id, even tokens of the same type. so a part id appears at most once, and a path's ordered children (for stacking) are derived from the map by sorting on `index`. this makes the render list keyed by piece id stable and unambiguous.
|
**assumption:** each piece on the board has a unique id, even tokens of the same type. so a part id appears at most once, and a path's ordered children (for stacking) are derived from the map by sorting on `index`. this makes the render list keyed by piece id stable and unambiguous.
|
||||||
|
|
||||||
derived surface render state: game state + surface routes => map of piece id to `{ surface, route, candidate, index, stackSize, face }` for rendering on a surface. keys of this map makes a stable render list.
|
derived surface render state: game state + surface routes => map of piece id to `{ surface, route, candidate, index, stackSize, facing }` for rendering on a surface. keys of this map makes a stable render list.
|
||||||
|
|
||||||
- `route` - the matched route.
|
- `route` - the matched route.
|
||||||
- `candidate` - the matched candidate for a `:param` route, carrying its anchor `x`/`y`/`rotation`. absent for routes without candidates.
|
- `candidate` - the matched candidate for a `:param` route, carrying its anchor `x`/`y`/`rotation`. absent for routes without candidates.
|
||||||
- `index` - the piece's position in its path's stack.
|
- `index` - the piece's position in its path's stack.
|
||||||
- `stackSize` - the number of pieces on the path.
|
- `stackSize` - the number of pieces on the path.
|
||||||
- `face` - whether the piece's face is up.
|
- `facing` - how the piece is oriented on the board (`face` / `back` / `standing`).
|
||||||
|
|
||||||
the render map is per enabled surface: a piece may appear on more than one enabled surface (e.g. an expansion path and the main board), and each is rendered independently.
|
the render map is per enabled surface: a piece may appear on more than one enabled surface (e.g. an expansion path and the main board), and each is rendered independently.
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ const setupValue = z.union([z.string(), z.array(z.string())]);
|
|||||||
const setupPlacement = z.object({
|
const setupPlacement = z.object({
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
parts: setupValue,
|
parts: setupValue,
|
||||||
|
facing: z.enum(['face', 'back', 'standing']).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const setupSchema = z.object({
|
const setupSchema = z.object({
|
||||||
|
|||||||
@@ -157,6 +157,12 @@ export interface Surface {
|
|||||||
|
|
||||||
export type SetupValue = string | string[];
|
export type SetupValue = string | string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How a part is oriented on the board. `face` lays it flat front-up, `back`
|
||||||
|
* flips it over front-down, and `standing` stands it on its bottom edge.
|
||||||
|
*/
|
||||||
|
export type Facing = 'face' | 'back' | 'standing';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One setup placement: move `parts` to `path`. Entries are applied in order,
|
* One setup placement: move `parts` to `path`. Entries are applied in order,
|
||||||
* so a part listed in a later entry ends up on that entry's path.
|
* so a part listed in a later entry ends up on that entry's path.
|
||||||
@@ -166,6 +172,8 @@ export interface SetupPlacement {
|
|||||||
path: string;
|
path: string;
|
||||||
/** Parts to place: a part id, a bare type (expands to all of that type), or a list of either. */
|
/** Parts to place: a part id, a bare type (expands to all of that type), or a list of either. */
|
||||||
parts: SetupValue;
|
parts: SetupValue;
|
||||||
|
/** Initial facing for the placed parts; defaults to `face`. */
|
||||||
|
facing?: Facing;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Seeds the state store: the enabled surfaces and an ordered list of placements. */
|
/** Seeds the state store: the enabled surfaces and an ordered list of placements. */
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export {
|
|||||||
fallbackShape,
|
fallbackShape,
|
||||||
traceToShape,
|
traceToShape,
|
||||||
traceToUvBounds,
|
traceToUvBounds,
|
||||||
|
facingTransform,
|
||||||
MM_TO_WORLD,
|
MM_TO_WORLD,
|
||||||
} from './part.js';
|
} from './part.js';
|
||||||
export { resolveAssetUrl, assetUrl, traceImage } from './http.js';
|
export { resolveAssetUrl, assetUrl, traceImage } from './http.js';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
fallbackShape,
|
fallbackShape,
|
||||||
traceToShape,
|
traceToShape,
|
||||||
traceToUvBounds,
|
traceToUvBounds,
|
||||||
|
facingTransform,
|
||||||
} from './part.js';
|
} from './part.js';
|
||||||
|
|
||||||
describe('partDimensions', () => {
|
describe('partDimensions', () => {
|
||||||
@@ -20,6 +21,25 @@ describe('partDimensions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('facingTransform', () => {
|
||||||
|
const dims = { height: 2, depth: 0.1 };
|
||||||
|
|
||||||
|
it('lays face-up flat on the minZ face', () => {
|
||||||
|
expect(facingTransform('face', dims)).toEqual({ pivot: [0, 0, 0], xRotation: -Math.PI / 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lays back-down flat on the maxZ face', () => {
|
||||||
|
expect(facingTransform('back', dims)).toEqual({ pivot: [0, 0, 0.1], xRotation: Math.PI / 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stands on the bottom (minY) edge', () => {
|
||||||
|
expect(facingTransform('standing', dims)).toEqual({
|
||||||
|
pivot: [0, -1, 0.05],
|
||||||
|
xRotation: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('spriteUvFromCrop', () => {
|
describe('spriteUvFromCrop', () => {
|
||||||
it('returns full-image UVs without a crop', () => {
|
it('returns full-image UVs without a crop', () => {
|
||||||
expect(spriteUvFromCrop(undefined)).toEqual({ repeatX: 1, repeatY: 1, offsetX: 0, offsetY: 0 });
|
expect(spriteUvFromCrop(undefined)).toEqual({ repeatX: 1, repeatY: 1, offsetX: 0, offsetY: 0 });
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* so they can be unit-tested in a plain node environment (mirroring the web
|
* so they can be unit-tested in a plain node environment (mirroring the web
|
||||||
* app's `cardResolution.ts`).
|
* app's `cardResolution.ts`).
|
||||||
*/
|
*/
|
||||||
import type { Part, Crop } from '@tts/bgm';
|
import type { Part, Crop, Facing } from '@tts/bgm';
|
||||||
import {
|
import {
|
||||||
rectShape,
|
rectShape,
|
||||||
roundedRectShape,
|
roundedRectShape,
|
||||||
@@ -31,6 +31,31 @@ export function partDimensions(part: Part): { width: number; height: number; dep
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The transform that orients a part for a `facing`, in the part's mesh-local
|
||||||
|
* frame (shape in XY centered at origin, extruded along +Z from `0` to
|
||||||
|
* `depth`). `pivot` is the center of the face/edge that rests on the table and
|
||||||
|
* should land at the anchor; `xRotation` (radians, about the local X axis)
|
||||||
|
* orients the part. Tilt is applied separately about the local Y (long) axis,
|
||||||
|
* so both the facing rotation and tilt spin about the anchor.
|
||||||
|
*/
|
||||||
|
export function facingTransform(
|
||||||
|
facing: Facing,
|
||||||
|
dims: { height: number; depth: number },
|
||||||
|
): { pivot: [number, number, number]; xRotation: number } {
|
||||||
|
switch (facing) {
|
||||||
|
case 'face':
|
||||||
|
// Lay flat, front up, resting on the minZ face.
|
||||||
|
return { pivot: [0, 0, 0], xRotation: -Math.PI / 2 };
|
||||||
|
case 'back':
|
||||||
|
// Lay flat, front down, resting on the maxZ face.
|
||||||
|
return { pivot: [0, 0, dims.depth], xRotation: Math.PI / 2 };
|
||||||
|
case 'standing':
|
||||||
|
// Stand upright on the bottom (minY) edge.
|
||||||
|
return { pivot: [0, -dims.height / 2, dims.depth / 2], xRotation: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UV repeat/offset that selects a single sprite from a sheet, given a crop
|
* UV repeat/offset that selects a single sprite from a sheet, given a crop
|
||||||
* `[col, row, cols, rows]` that divides the image into a `cols` x `rows` grid
|
* `[col, row, cols, rows]` that divides the image into a `cols` x `rows` grid
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import type { Package } from '@tts/bgm';
|
|||||||
import { useStacking } from './stacking.js';
|
import { useStacking } from './stacking.js';
|
||||||
import type { Placement } from './state.js';
|
import type { Placement } from './state.js';
|
||||||
import { PartView } from './partView.js';
|
import { PartView } from './partView.js';
|
||||||
import { MM_TO_WORLD, DEG_TO_RAD } from './part.js';
|
import { MM_TO_WORLD, DEG_TO_RAD, facingTransform, partDimensions } from './part.js';
|
||||||
|
|
||||||
export function PartPlacement({ pkg, placement }: { pkg: Package; placement: Placement }) {
|
export function PartPlacement({ pkg, placement }: { pkg: Package; placement: Placement }) {
|
||||||
const { route, candidate, piece, index, stackSize, face } = placement;
|
const { route, candidate, piece, index, stackSize, facing } = placement;
|
||||||
// `piece` is `package:type#id`; the parts map is keyed by `type#id`.
|
// `piece` is `package:type#id`; the parts map is keyed by `type#id`.
|
||||||
const part = pkg.parts.get(piece.split(':').slice(1).join(':'));
|
const part = pkg.parts.get(piece.split(':').slice(1).join(':'));
|
||||||
if (!part) return null;
|
if (!part) return null;
|
||||||
@@ -34,12 +34,16 @@ export function PartPlacement({ pkg, placement }: { pkg: Package; placement: Pla
|
|||||||
const anchorRotation =
|
const anchorRotation =
|
||||||
((candidate?.rotation ?? route.rotation ?? 0) - rotation) * DEG_TO_RAD;
|
((candidate?.rotation ?? route.rotation ?? 0) - rotation) * DEG_TO_RAD;
|
||||||
|
|
||||||
|
// The facing pivot is the center of the face/edge that rests on the table
|
||||||
|
// and should land at the anchor. Translate the mesh by the pivot, then apply
|
||||||
|
// the facing rotation and tilt about it, so the part sits on the table.
|
||||||
|
const { width, height, depth } = partDimensions(part);
|
||||||
|
const { pivot, xRotation } = facingTransform(facing, { height, depth });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group position={[anchorX, anchorZ, anchorY]} rotation={[0, anchorRotation, 0]}>
|
<group position={[anchorX, anchorZ, anchorY]} rotation={[0, anchorRotation, 0]}>
|
||||||
{/* The part mesh extrudes along +Z; lay it flat so its face points up.
|
<group position={[-pivot[0], -pivot[1], -pivot[2]]}>
|
||||||
A face-down part is flipped over about its local X axis. */}
|
<group rotation={[xRotation, tilt * DEG_TO_RAD, 0]}>
|
||||||
<group rotation={[-Math.PI / 2, tilt * DEG_TO_RAD, 0]}>
|
|
||||||
<group rotation={!face ? [Math.PI, 0, 0] : [0, 0, 0]}>
|
|
||||||
<PartView part={part} baseUrl={part.baseUrl} />
|
<PartView part={part} baseUrl={part.baseUrl} />
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ describe('seedFromSetup', () => {
|
|||||||
};
|
};
|
||||||
const state = seedFromSetup(pkg, setup);
|
const state = seedFromSetup(pkg, setup);
|
||||||
expect(state.surfaces).toEqual({ 'board#harbor': true });
|
expect(state.surfaces).toEqual({ 'board#harbor': true });
|
||||||
expect(state.parts).toEqual({ 'harbor:card#fleet': { path: '/deck', index: 0, face: true } });
|
expect(state.parts).toEqual({ 'harbor:card#fleet': { path: '/deck', index: 0, facing: 'face' } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('enables all surfaces when omitted', () => {
|
it('enables all surfaces when omitted', () => {
|
||||||
@@ -65,7 +65,24 @@ describe('seedFromSetup', () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
const state = seedFromSetup(pkg, setup);
|
const state = seedFromSetup(pkg, setup);
|
||||||
expect(state.parts['harbor:card#fleet']).toEqual({ path: '/hand', index: 0, face: true });
|
expect(state.parts['harbor:card#fleet']).toEqual({ path: '/hand', index: 0, facing: 'face' });
|
||||||
expect(state.parts['harbor:token#wood']).toEqual({ path: '/deck', index: 0, face: true });
|
expect(state.parts['harbor:token#wood']).toEqual({ path: '/deck', index: 0, facing: 'face' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('seeds the facing from the placement, defaulting to face', () => {
|
||||||
|
const setup = {
|
||||||
|
type: 'game',
|
||||||
|
id: 'main',
|
||||||
|
setup: [
|
||||||
|
{ path: '/deck', parts: 'harbor:card#fleet', facing: 'back' },
|
||||||
|
{ path: '/table', parts: 'harbor:token#wood', facing: 'standing' },
|
||||||
|
{ path: '/hand', parts: 'harbor:token#grain' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const state = seedFromSetup(pkg, setup);
|
||||||
|
expect(state.parts['harbor:card#fleet']).toEqual({ path: '/deck', index: 0, facing: 'back' });
|
||||||
|
expect(state.parts['harbor:token#wood']).toEqual({ path: '/table', index: 0, facing: 'standing' });
|
||||||
|
// No `facing` on the placement defaults to `face`.
|
||||||
|
expect(state.parts['harbor:token#grain']).toEqual({ path: '/hand', index: 0, facing: 'face' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -55,7 +55,7 @@ export function seedFromSetup(pkg: Package, setup: Setup): {
|
|||||||
const parts: Record<string, PartState> = {};
|
const parts: Record<string, PartState> = {};
|
||||||
for (const placement of setup.setup) {
|
for (const placement of setup.setup) {
|
||||||
for (const id of expandSetupValue(pkg, placement.parts)) {
|
for (const id of expandSetupValue(pkg, placement.parts)) {
|
||||||
parts[id] = { path: placement.path, index: 0, face: true };
|
parts[id] = { path: placement.path, index: 0, facing: placement.facing ?? 'face' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const byPath: Record<string, string[]> = {};
|
const byPath: Record<string, string[]> = {};
|
||||||
|
|||||||
@@ -68,9 +68,9 @@ describe('matchRoute', () => {
|
|||||||
describe('childrenByPath', () => {
|
describe('childrenByPath', () => {
|
||||||
it('groups parts by path, ordered by index', () => {
|
it('groups parts by path, ordered by index', () => {
|
||||||
const parts = {
|
const parts = {
|
||||||
'harbor:card#a': { path: '/deck', index: 1, face: true },
|
'harbor:card#a': { path: '/deck', index: 1, facing: 'face' },
|
||||||
'harbor:card#b': { path: '/deck', index: 0, face: false },
|
'harbor:card#b': { path: '/deck', index: 0, facing: 'back' },
|
||||||
'harbor:card#c': { path: '/community/0', index: 0, face: true },
|
'harbor:card#c': { path: '/community/0', index: 0, facing: 'standing' },
|
||||||
};
|
};
|
||||||
expect(childrenByPath(parts)).toEqual({
|
expect(childrenByPath(parts)).toEqual({
|
||||||
'/deck': ['harbor:card#b', 'harbor:card#a'],
|
'/deck': ['harbor:card#b', 'harbor:card#a'],
|
||||||
@@ -80,24 +80,24 @@ describe('childrenByPath', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('computeSurfacePlacements', () => {
|
describe('computeSurfacePlacements', () => {
|
||||||
it('places parts on a matching route with index, stackSize, and face', () => {
|
it('places parts on a matching route with index, stackSize, and facing', () => {
|
||||||
const surface = makeSurface({
|
const surface = makeSurface({
|
||||||
layout: [{ route: '/deck', x: -100, y: 0, rotation: 0 }],
|
layout: [{ route: '/deck', x: -100, y: 0, rotation: 0 }],
|
||||||
});
|
});
|
||||||
const placements = computeSurfacePlacements(surface, {
|
const placements = computeSurfacePlacements(surface, {
|
||||||
'harbor:card#a': { path: '/deck', index: 0, face: true },
|
'harbor:card#a': { path: '/deck', index: 0, facing: 'face' },
|
||||||
'harbor:card#b': { path: '/deck', index: 1, face: false },
|
'harbor:card#b': { path: '/deck', index: 1, facing: 'back' },
|
||||||
});
|
});
|
||||||
expect(placements).toHaveLength(2);
|
expect(placements).toHaveLength(2);
|
||||||
expect(placements[0]).toMatchObject({ piece: 'harbor:card#a', index: 0, stackSize: 2, face: true });
|
expect(placements[0]).toMatchObject({ piece: 'harbor:card#a', index: 0, stackSize: 2, facing: 'face' });
|
||||||
expect(placements[1]).toMatchObject({ piece: 'harbor:card#b', index: 1, stackSize: 2, face: false });
|
expect(placements[1]).toMatchObject({ piece: 'harbor:card#b', index: 1, stackSize: 2, facing: 'back' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('drops parts with no matching route', () => {
|
it('drops parts with no matching route', () => {
|
||||||
const surface = makeSurface({ layout: [{ route: '/deck', x: 0, y: 0, rotation: 0 }] });
|
const surface = makeSurface({ layout: [{ route: '/deck', x: 0, y: 0, rotation: 0 }] });
|
||||||
const placements = computeSurfacePlacements(surface, {
|
const placements = computeSurfacePlacements(surface, {
|
||||||
'harbor:card#a': { path: '/deck', index: 0, face: true },
|
'harbor:card#a': { path: '/deck', index: 0, facing: 'face' },
|
||||||
'harbor:card#b': { path: '/elsewhere', index: 0, face: true },
|
'harbor:card#b': { path: '/elsewhere', index: 0, facing: 'face' },
|
||||||
});
|
});
|
||||||
expect(placements).toHaveLength(1);
|
expect(placements).toHaveLength(1);
|
||||||
expect(placements[0]!.piece).toBe('harbor:card#a');
|
expect(placements[0]!.piece).toBe('harbor:card#a');
|
||||||
@@ -116,7 +116,7 @@ describe('computeSurfacePlacements', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
const placements = computeSurfacePlacements(surface, {
|
const placements = computeSurfacePlacements(surface, {
|
||||||
'harbor:boat#fleet': { path: '/dock/0', index: 0, face: true },
|
'harbor:boat#fleet': { path: '/dock/0', index: 0, facing: 'face' },
|
||||||
});
|
});
|
||||||
expect(placements[0]!.candidate).toEqual({ seat: '0', x: 40, y: 5, rotation: 1 });
|
expect(placements[0]!.candidate).toEqual({ seat: '0', x: 40, y: 5, rotation: 1 });
|
||||||
});
|
});
|
||||||
@@ -134,7 +134,7 @@ describe('computeSurfacePlacements', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
const placements = computeSurfacePlacements(surface, {
|
const placements = computeSurfacePlacements(surface, {
|
||||||
'harbor:boat#fleet': { path: '/dock/0', index: 0, face: true },
|
'harbor:boat#fleet': { path: '/dock/0', index: 0, facing: 'face' },
|
||||||
});
|
});
|
||||||
expect(placements[0]!.candidate).toEqual({ seat: '0', x: 40, y: 5, stacking: { tilt: 2 } });
|
expect(placements[0]!.candidate).toEqual({ seat: '0', x: 40, y: 5, stacking: { tilt: 2 } });
|
||||||
});
|
});
|
||||||
@@ -144,8 +144,8 @@ describe('computeSurfacePlacements', () => {
|
|||||||
layout: [{ route: '/deck', x: 0, y: 0, rotation: 0 }],
|
layout: [{ route: '/deck', x: 0, y: 0, rotation: 0 }],
|
||||||
});
|
});
|
||||||
const placements = computeSurfacePlacements(surface, {
|
const placements = computeSurfacePlacements(surface, {
|
||||||
'harbor:card#b': { path: '/deck', index: 1, face: true },
|
'harbor:card#b': { path: '/deck', index: 1, facing: 'face' },
|
||||||
'harbor:card#a': { path: '/deck', index: 0, face: true },
|
'harbor:card#a': { path: '/deck', index: 0, facing: 'face' },
|
||||||
});
|
});
|
||||||
expect(placements.map((p) => p.piece)).toEqual(['harbor:card#a', 'harbor:card#b']);
|
expect(placements.map((p) => p.piece)).toEqual(['harbor:card#a', 'harbor:card#b']);
|
||||||
});
|
});
|
||||||
@@ -155,7 +155,7 @@ describe('computeRenderState', () => {
|
|||||||
it('only includes enabled surfaces', () => {
|
it('only includes enabled surfaces', () => {
|
||||||
const state = {
|
const state = {
|
||||||
surfaces: { 'board#harbor': true, 'hud#hand': false },
|
surfaces: { 'board#harbor': true, 'hud#hand': false },
|
||||||
parts: { 'harbor:card#a': { path: '/deck', index: 0, face: true } },
|
parts: { 'harbor:card#a': { path: '/deck', index: 0, facing: 'face' } },
|
||||||
};
|
};
|
||||||
pkg.surfaces.set(
|
pkg.surfaces.set(
|
||||||
'board#harbor',
|
'board#harbor',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import type { Candidate, Package, Route, Surface } from '@tts/bgm';
|
import type { Candidate, Facing, Package, Route, Surface } from '@tts/bgm';
|
||||||
|
|
||||||
/** The placement state of a single part on the board. */
|
/** The placement state of a single part on the board. */
|
||||||
export interface PartState {
|
export interface PartState {
|
||||||
@@ -19,8 +19,8 @@ export interface PartState {
|
|||||||
path: string;
|
path: string;
|
||||||
/** The part's position in its path's stack. */
|
/** The part's position in its path's stack. */
|
||||||
index: number;
|
index: number;
|
||||||
/** Whether the part's face is up (`true`) or down (`false`). */
|
/** How the part is oriented on the board. */
|
||||||
face: boolean;
|
facing: Facing;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Source-of-truth game state. */
|
/** Source-of-truth game state. */
|
||||||
@@ -47,8 +47,8 @@ export interface Placement {
|
|||||||
index: number;
|
index: number;
|
||||||
/** The number of pieces on the path. */
|
/** The number of pieces on the path. */
|
||||||
stackSize: number;
|
stackSize: number;
|
||||||
/** Whether the piece's face is up. */
|
/** How the piece is oriented on the board. */
|
||||||
face: boolean;
|
facing: Facing;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TabletopState extends GameState {
|
interface TabletopState extends GameState {
|
||||||
@@ -169,7 +169,7 @@ export function computeSurfacePlacements(surface: Surface, parts: Record<string,
|
|||||||
piece,
|
piece,
|
||||||
index: ps.index,
|
index: ps.index,
|
||||||
stackSize,
|
stackSize,
|
||||||
face: ps.face,
|
facing: ps.facing,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user