Add tests for the tabletop and interaction stores (seed, surface toggles,
setPart, movePart reindexing/clamping), the interaction store's hold/drop
and dialog stack, and the SVG path sampler (Q/T/S/A commands, unsupported
commands, pointAt edge cases, and zero-length curves). stacking.ts goes
from 61% to 99.5% line coverage.
Add the dialog role and the interaction affordances the free-interaction
layer depends on: Setup.interactions, Package.dialogs, and the Interaction,
DialogAction, and Dialog types. Wire the dialog role through the zod
schemas, package collection (with duplicate checking), and vite
serialization, and update the tabletop serializedToPackage and test
fixtures to carry dialogs.
Add the interaction half of the sandbox: a held part and dialog stack as
UI state outside the game store, with pure helpers (interactionsFor,
dropPaths, pickPath, partFacings, nextFacing). PartPlacement is now
interactive (pick up, drag to move, click to cycle facing), and a drop on
a path with a stack-dialog interaction opens the deck dialog instead of
moving directly. DialogLayer renders the stack-inspector dialog whose
insert button issues a move. Add the dialog role, Setup.interactions, and
Package.dialogs to @tts/bgm, and declare the deck insert dialog in
games/poker.
Group the bgm spec cluster under docs/bgm and move dev logs and plans under docs/status, add an overview index, and update cross-references in the docs, README, and source comments.
TextureLoader leaves colorSpace as NoColorSpace, so card, tile, token, model, and part art is uploaded as linear and then double-decoded in the shader, washing out contrast. Mark each texture sRGB so the GPU decodes it once, correctly.
Include the Workshop title and preview image in the items response so the mod header survives a page refresh, not just navigation from search. Resolve both from the same Steam API call.
Add a /pdf proxy route that forces Content-Disposition inline so PDFs render instead of downloading, and a web viewer that embeds the document in an iframe with a standalone open-in-new-tab link.
Replace the CommandHost with a handler registry keyed by message type.
Message is a permissive base type the host extends; CommandResult is a
discriminated union on the type suffix. Commands and orchestrators are
the same async shape taking a RunContext with an AbortSignal; runCommand
emits :done/:cancel/:error.
New @tts/engine package: the pure message layer that drives bgm games.
MessageQueue snapshots and drains on tick; TriggerRegistry matches by
type/id with runtime enable/disable; runOrchestrator suspends on wait
until a matching message is processed. Engine ties them together behind
the CommandHost contract that @tts/tabletop will implement.
A definition is identified by role.type and named role.type.lang
(package.lang for packages). Code blocks declare it via role= on the
info string; real files by their filename. Blocks and files without a
matching role.type are ignored. file= still overrides the auto-name.
Relative asset and $variants paths now resolve against the markdown
file's directory (baseDir) rather than the virtual name. Same role.type
with different ids groups under one name; duplicate type#id still
errors. Migrate fixtures and example games to role=.
Parse role.type#id from the fence info string and merge it into each
parsed object, erroring on conflicts with the content. id on the info
string can't combine with $variants. Auto-named blocks now hash their
content instead of the info string, so identical blocks dedupe.
Expand $variants to accept an array of csv paths, concatenating their
rows. Detect a path by a .csv suffix on the first line instead of a
newline, so inline csv and paths are self-documenting and the rule
applies uniformly to single values and array elements.
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.
Replace the path->parts setup map with an ordered list of placements, each moving its parts to a path. Entries apply in order, so a part listed in a later placement ends up on that placement's path.
Replace the path->part-list store with a per-part map keyed by part id, so each placed part carries its path, stack index, and face. Derive each path's ordered children for stacking, and flip face-down parts in PartPlacement.
The default include is now resolved relative to the package
declaration's directory, so the explicit root-relative patterns are
redundant. Drop them in favor of the default.
The default include matched every yaml across the games root, so a
package could absorb a sibling game's defs (e.g. two game#main setups).
Resolve patterns against the package declaration's directory instead;
a leading slash marks a pattern as root-relative.
Replace the dark surface plane with a grey background and a spotlight,
size the contact shadow to the table, and scale its blur and fade with
the shadow plane. Disable depth test on the stacking curve overlay.
Fix the tangent angle at the curve's start and rotate cards to follow
the curve direction. Add a stacking curve visualization shown with
surface debug.
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.
Apply the same tilt to every stacked part and rotate it about its local
Y axis instead of Z. Default tilt to 1 degree when a stacking strategy
is present, and arc the poker deck along the surface's bottom edge.
Normalize the games root to POSIX separators so the HMR path guard
matches on Windows, and watch the real on-disk markdown files (which
defMap.files omits in favor of their virtual code blocks) so edits
trigger a re-collect instead of a manual restart.
Replace the per-part lift with a local Y-axis tilt that fans the stack,
and add zStart/zEnd to ramp the stack's height across the curve so it
arches in 3D. Update the poker deck and docs accordingly.
Add a toggleable surface plane, bounds outline, and constant-size HTML
label. Scale placement and mount anchors to world units so parts land on
the surface, and rotate parts to face up.
A piece can appear on more than one path of the same surface (e.g. the
poker deck expands to every card while the flop also places the ace), so
the render key must include the path to stay unique.
Wire the tabletop library into the setup detail route: seed the store
from the setup, resolve the surface mount tree, and render world and HUD
surfaces. Add serializedToPackage to convert the vite-emitted package.
Implement the tabletop library's game state store, setup seeding with
bare-type expansion, surface mount tree resolution, part placement, and
the stacking positioning process with a dependency-free SVG path helper.
Wire the public API and add unit plus vite integration tests.
Lift the trace-to-shape geometry into @tts/mesh (traceToShape/
traceToUvBounds), parameterized by scale so the web token viewer and
@tts/tabletop share one implementation. Move the CORS proxy HTTP helpers
(assetUrl, resolveAssetUrl, traceImage) into a new @tts/http package, and
make @tts/tabletop's ErrorBoundary the single source used by the web app.
This removes the duplicated ErrorBoundary, assetUrl, tabletopHttp, and
trace-to-shape code from apps/web and packages/tabletop.
Vite 8 (rolldown) changed buildStart to take (this, options), so the
test's one-arg call failed typecheck. Add an ambient declaration for the
fixture's virtual bgm modules so its entry typechecks in the editor.
Add a SerializedPackage type for the JSON the plugin emits, and tests
that assert the maps serialize to plain objects, that the packages module
returns every package, and that buildStart watches def files. Add a
second fixture package to exercise multi-package collection.
Rename the plugin's virtual modules to virtual:bgm/packages and
virtual:bgm/package/<id> so they read as plugin-provided modules rather
than real packages. Update the unit and build integration tests.
Move the bgm vite plugin into the package so it can be tested in
isolation from the web app. Serve each package as a JSON module,
serializing its maps, and verify resolution through a real vite build
against a self-contained fixture.
Parse yaml/json/toml and markdown code blocks into packages, expanding
$variants via typed-csv and collecting parts, surfaces, and setups by
include patterns. Ships zod validation, a vitest config, and 16 tests.
Model ColorDiffuse in shared types and multiply each viewer's material color by the per-object tint, including textured faces and loaded custom models.
Model TTSObjectTransform in shared types and convert each object's position, rotation, and scale into three.js placement, with per-class base-size corrections and a lay-flat rotation for extruded meshes.
Split extrudeShapeParts into front, back, and walls so each face can carry
its own material. Flip back textures left/right on the material so they are
not mirrored, and slice card faces from the deck sprite sheet via CardID.
Resolve deck config from the parent deck, which is authoritative over a
card's own CustomDeck. Add unit tests for card resolution, sprite UVs, and
the flip helper.
Trace results can now be inset (negative) or outset (positive) by a
pixel amount. Offset the outline and holes in opposite directions with
clipper-lib's miter joins, then recombine with a boolean difference so
holes grow on inset and shrink on outset. Collapsed shapes return an
empty outline; a split outline keeps the largest ring. Document the
parameter and the new dependency.
Add an optional uvBounds to extrudeShape/extrudeShapeParts so UVs can be derived from a framing rectangle instead of the shape's bounding box, letting a traced silhouette align with its full image. The bottom face now uses the same planar xy UVs as the top face (no horizontal mirror), so the texture is consistent across front, back, and walls.
Add GET /trace which fetches an image, traces it into a vector shape, and returns the result BSON-encoded. Tracing is configurable via mode (alpha, bw, color) and format (shape, svg). The shape format parses the vtracer SVG into an outline/holes polygon matching @tts/mesh's Shape interface.