Rendering, assets, and external packs
The published package ships the FREE KayKit assets and manifest. Purchased EXTRA assets and third-party fixtures stay local-only unless an application chooses to copy its own generated manifest and source URL map into its codebase.
FREE Assets
Section titled “FREE Assets”Use freeManifest for packaged assets. It includes asset ids, categories,
subcategories, factions, texture sets, bounds, material slots, local package
paths, and KayKit license metadata.
import { freeManifest } from 'declarative-hex-worlds/manifest/free';import { createManifestBundle, getManifestAsset, resolveManifestAssetUrl,} from 'declarative-hex-worlds/manifest/schema';
const bundle = createManifestBundle([freeManifest]);const asset = getManifestAsset(bundle, 'hex_river_A');
const url = asset ? resolveManifestAssetUrl(asset, { baseUrl: '/node_modules/declarative-hex-worlds/assets/free', }) : undefined;Browser bundles often rewrite package asset URLs. Keep that mapping at the app boundary instead of baking local absolute paths into manifests.
Use describeKayKitAssetTreatment(assetId) from ./catalog when an editor or
tool needs to explain what an asset is for. Treatment records connect each
FREE/EXTRA asset id to a role, placement kind/layer, extracted guide image, and
the public builder or selector API that exercises it. This prevents an asset
from being merely present in a manifest without a gameboard-facing path.
Use listKayKitGuideScenarios() when the tool needs the page-level source
contract: each extracted guide page lists its source PNG, covered assets, public
APIs, docs, and visual artifacts. Use listKayKitGuideScenarioTreatments(id) or
describeKayKitGuideScenarioCoverage(id) to join a page back to the asset
treatment records, listKayKitGuideAssetCoverages() when a tool needs to start
from an exact asset id, listKayKitGuideRoleCoverages() when it needs to start
from a gameplay role, listKayKitGuidePublicApiCoverages() when it needs to
start from a public API surface, listKayKitGuideScenarioAssetRenderRequests()
when it needs URL-resolved render queues, listKayKitGuideScenarioAssetRenderGroups()
when it needs guide-page contact-sheet groups, guide-render-requests when it
needs the same queue from the CLI, and
summarizeKayKitGuideCoverage() when a build tool or editor needs stable counts
for pages, editions, roles, unique assets, repeated page-level asset
occurrences, docs, and visual artifacts.
EXTRA Assets
Section titled “EXTRA Assets”EXTRA assets are supported through local ingest. They must not be published by this package. Apps that own the assets can generate an app-local manifest and combine it with the packaged FREE manifest:
import { freeManifest } from 'declarative-hex-worlds/manifest/free';import { createManifestBundle } from 'declarative-hex-worlds/manifest/schema';import extraManifest from './generated/kaykit-extra-manifest.json';
const bundle = createManifestBundle([freeManifest, extraManifest]);The static import extraManifest from '...json' shown above assumes the
generated manifest lives in your source tree. If you wrote it under a Vite
public/ root (the natural outcome of extract --out public/assets/...),
Vite refuses to import it (“Assets in public directory cannot be imported from
JavaScript”) — fetch() it at runtime instead and pass the parsed JSON to
createManifestBundle.
Every placement that points at an EXTRA asset should keep requiresExtra: true.
That lets validation and renderers distinguish missing local content from
package bugs.
Three.js Bridge
Section titled “Three.js Bridge”The ./three subpath is a thin bridge around public board state. It does not
own the game loop. Use it to:
- resolve a placement to a loadable URL.
- load GLTFs with an app-provided loader.
- apply position, rotation, scale, and placement metadata.
- keep a scene in sync with added, changed, and removed placements.
- tag objects so raycasts can resolve placement ids, tile keys, actor ids, and source asset ids.
- attach optional animation clips for rigged units.
import { createGameboardPlacementAssetUrlResolver, gameboardInteractionTargetForObject, syncGameboardPlacementObjects,} from 'declarative-hex-worlds/three';
const sourceAssetUrls = runtime.createScenarioPieceSourceUrlMap({ sourceRoots: { 'Kenney Castle Kit': '/assets/kenney/castle-kit' },});const resolveAssetUrl = createGameboardPlacementAssetUrlResolver({ catalog: manifestBundle, assetUrls: sourceAssetUrls, baseUrl: '/assets/kaykit/free',});const previewUrl = resolveAssetUrl(runtime.plan().placements[0]);
const sync = await syncGameboardPlacementObjects(runtime.plan().placements, { parent: scene, loader, catalog: manifestBundle, assetUrls: sourceAssetUrls, baseUrl: '/assets/kaykit/free',});
const target = gameboardInteractionTargetForObject(raycasterHit.object);Animation loading is explicit because different games manage mixers and clips differently. The bridge exposes clip metadata so an app can connect the loaded clips to its own animation system.
Performance at scale
Section titled “Performance at scale”syncGameboardPlacementObjects (and loadGameboardPlacementObject) dedupe
GLTF loads by URL automatically: placements that resolve to the same model or
animation URL trigger exactly one loader.loadAsync call per sync
generation, no matter how many placements share it. A failed load is evicted
from the cache so the next sync retries instead of caching the failure. This
is on by default; pass cacheLoads: false if a caller needs every placement
to issue its own load.
Deduping loads does not change the draw-call story: each placement still gets
its own cloned Object3D subtree. A single-mesh asset is one draw call per
placement; multi-mesh or multi-material GLTFs (rigged units especially) issue
one draw call per mesh/material group. For a 98-placement showcase board of
mostly single-mesh tiles this measures as 98 draw calls
(renderer.info.render.calls) — fine at that scale. Boards with hundreds to
thousands of tiles should batch per-asset-id with THREE.InstancedMesh on
top of the placement snapshot the bridge already produces; the bridge
intentionally doesn’t own that batching decision.
External Compatibility
Section titled “External Compatibility”Run compatibility checks on local GLB/GLTF files before registering them as pieces. A mesh that is not a KayKit-compatible hex tile can still be useful as a prop, landmark, building, tree, scatter item, or unit.
declarative-hex-worlds compatibility \ --asset "references/kenney_castle-kit/Models/GLB format/tower-hexagon-base.glb" \ --intendedRole tile \ --sourcePack "Kenney Castle Kit" \ --modelForward +z \ --boardForwardEdge 1
declarative-hex-worlds pieces-from-assets \ --assets "references/kenney_castle-kit/Models/GLB format" \ --sourcePack "Kenney Castle Kit" \ --intendedRole tile \ --assetIdPrefix kenney \ --pieceIdPrefix kenney-castle \ --tags castle \ --pieceOverrides docs/examples/local-piece-overrides.kenney-castle.json \ --includeReports \ --out /tmp/kenney-pieces.jsonThe batch output omits absolute paths by default. Renderer URL maps should be generated from local source roots at app build time:
declarative-hex-worlds pieces \ --pieces /tmp/kenney-pieces.json \ --emitSourceUrls \ --pieceSourceRoots docs/examples/local-piece-source-roots.example.json \ --jsonPlacement Criteria
Section titled “Placement Criteria”Use explicit criteria for non-KayKit assets:
- towers and large structures usually need multi-tile footprints and occupancy reservations.
- trees and loose props should use scatter slots,
maxPerTile, and a soft slot group. - units should declare facing and animation expectations.
- circular towers, square walls, and other non-hex meshes should be props or landmarks unless the compatibility report proves a matching tile footprint.
- ships, docks, ports, and harbor props should target coast tiles adjacent to water.
This placement metadata is what lets seeded boards stay plausible while still accepting assets from another designer.
Browser Verification
Section titled “Browser Verification”Rendering tests should exercise public fixtures:
pnpm test:browser:freepnpm test:browser:extrapnpm test:e2e:local-assetspnpm test:visualScreenshots are deterministic artifacts. The test suite checks image size,
variance, and flat-output failures after Chromium captures the PNGs. When adding
new visible behavior, prefer a scenario or recipe fixture that can also be used
by CLI validation and headless simulation tests. The FREE browser suite also
captures free-guide-source-pages.png and
free-guide-scenarios-by-extracted-page.png so visual review can start from the
decomposed KayKit guide pages and then inspect every FREE treatment associated
with those pages. The EXTRA browser suite follows the same page contract through
extra-guide-scenarios-pages-02-15.png and
extra-guide-scenarios-pages-16-18.png, covering mixed and EXTRA page-level
asset occurrences for buildings, transitions, units, harbors, stables,
workshops, siege pieces, and unit combinations.