@velo/crown-base (0.8.0)
Installation
@velo:registry=npm install @velo/crown-base@0.8.0"@velo/crown-base": "0.8.0"About this package
@velo/crown-base
Static Crownfall map contracts for the frontend editor, with no React, Babylon, simulation or
server dependencies. Zod is the only runtime dependency. The package uses NodeNext .js imports
and publishes JavaScript and declarations from dist/. The game retains thin source re-exports;
the package never imports the game. helloWorld remains available for compatibility.
Editor consumer contract
Both the controlled authoring view (#714) and file controls (#715) use the same MapDocument.
It is a branded, validated document with typed terrain arrays. Obtain it from a successful result,
rather than a type assertion. The public entry point exports:
| API | Contract |
|---|---|
MapDocument |
Validated document: format, artifactVersion, schematic, optional assets |
StaticMap, TerrainField, MapAssetReference |
Represented static data, terrain authority and asset reference types |
validateMapDocument(input: unknown): MapDocumentResult |
Validate an in-memory candidate, returning independently owned data |
readMapArtifact(text: string): MapDocumentResult |
Parse JSON and validate every represented field before accepting it |
writeMapArtifact(document: MapDocument): string |
Revalidate edits and serialize to JSON; throws MapArtifactError on invalid data |
MapDocumentResult |
{ ok: true, document } or { ok: false, error } |
MapArtifactError, MapArtifactIssue |
Error with issues: { code, path, message }[]; codes are json or validation |
mapDocumentSchema, staticMapSchema, terrainFieldSchema |
Strict schemas for consumers needing schema composition; use the operations above for ownership and stable feedback |
MAP_ARTIFACT_FORMAT, MAP_ARTIFACT_VERSION |
Current format identifier and external schema version |
The authoring view receives document: MapDocument and reports
onDocumentChange(document: MapDocument). Clone the current document, edit the candidate,
raise candidate.schematic.field.version for any terrain change, validate, then report only a
successful result. Preserve the rest of the document, including unknown-to-the-view asset roles,
site names, instructions and tuning. Do not rebuild a document from only visible controls.
File controls receive the same current document. Open uses await file.text() followed by
readMapArtifact; only a successful result invokes onDocumentChange. Report
result.error.issues as validation feedback and keep the current document on failure. Save uses
writeMapArtifact(currentDocument) and reports any thrown MapArtifactError. These APIs perform
no file access or downloads and have no UI dependencies. Callbacks are a consumer convention,
not UI components exported by this package.
For package consumption, the editor implementation must declare @velo/crown-base as a runtime
dependency and import these names from @velo/crown-base. Its published dependency range must
include the release containing this contract. This change does not add an editor UI or require
consumers to import games/crownfall. In this repository, tests and the narrow game adapters use
relative source imports so no generated dist/ is required for development verification.
Container and references, version 3
Save a UTF-8 JSON manifest, conventionally manifest.crownfall.json. It is the entire static
map document. No archive is required for local editor round trips. Optional companion files live
under assets/ beside the manifest; a portable copy must keep that directory with the manifest.
Opening or saving the manifest does not load, copy, process or verify asset bytes. A manifest
can round-trip references even when companion files are unavailable.
The manifest has exactly format: 'crownfall-map', artifactVersion: 3, schematic and optional
assets. Unknown properties are errors at every object boundary, including private spawns,
trade assignments, ownership, remaining stock, inventories and participation. Future formats
must declare another supported version; version 3 does not silently discard unknown data.
schematic represents the supported dimensions, seed, settings, public clearings, objectives,
gold-node placements with positive initial stock, outposts, generator provenance and the terrain
field. Forests, quarries and sheep remain battle-start placements, outside this static contract.
Horizontal coordinates and lengths are metres; terrain heights are whole centimetres. Numeric
site IDs are unique across objectives, mines and outposts. Maps have no rivers, bridges or water.
The field is the sole terrain authority. Saved heights and materials are JSON number arrays
in the existing row-major order. The reader checks number ranges and lengths before constructing
typed arrays, then uses the shared field/content and static-site validators. It never stores derived navigation, rendering
geometry, terrain identities or battle state. JSON whitespace is not preserved, but all represented
values and optional asset references survive an unedited read/write round trip.
An asset entry is one of:
{ id: 'music', kind: 'audio', source: 'custom', path: 'assets/audio/field.ogg' }
{ id: 'keep', kind: 'model', source: 'engine', key: 'keep/default' }
kind is audio, model or texture; id is a document-local unique reference name.
A custom path is case-sensitive, relative to the manifest directory, begins with assets/, and
has no empty, . or .. segments, backslashes, URL syntax, query/fragment, percent escapes or
control characters. It is a literal companion path, not a URL to resolve or fetch. Engine keys
are opaque engine-provided identifiers; engine bytes are never packaged. Entries declare optional
references only: this version assigns no runtime asset loading or site/material override behavior.
Asset bytes, media types, existence, size limits, storage and processing remain deferred.
Versions and preview edits
| Value | Meaning |
|---|---|
artifactVersion: 3 |
External JSON schema/container contract, including JSON layer representation |
schematic.schematicVersion: 1 |
Static site schema shared with existing game schematics |
schematic.generatorVersion |
Generator provenance; must equal field.generatorVersion; zero means authored |
schematic.field.version |
Unsigned 32-bit terrain mutation/cache revision; increment on terrain edits and preserve through save/reopen |
Binary codec header format: 2 |
Little-endian network field encoding of heights and materials; not the JSON artifact representation. Format 1, which also carried water and span layers, is refused |
No version is a battle tick, ownership or stock counter. An edit does not change generator
provenance or rerun the generator. The existing game convertSchematic adapter still requires
field.version === 0 and validates through parseStaticMap; public delivery still requires an
identity version matching the field. The artifact's authoring contract allows nonzero cache
revisions so a preview can invalidate its caches and save/reopen. It does not reset versions or
feed documents into a live battle. Future battle adoption must deliberately construct fresh
initial schematic data and validate it at that boundary; no artifact loader is supplied here.
Older manifests
readMapArtifact opens every earlier version and writeMapArtifact always writes version 3:
- Version 1 stored node stock as
initialOreQuantity. It is upgraded toinitialQuantityin memory. Supplying both quantity fields is rejected. - Versions 1 and 2 stored river, crossing and bridge sites (
riverLeft,riverRight,bridgeHalfWidth,bridges,riverPath,riverBranches,neutralBridges), and in the field awaterlayer,waterBodiesandspans. Each must still have its old shape. It is then dropped, so the map loads without it and the next save leaves it out. Water cells become dry ground at their stored heights, and deck cells keep the ground beneath the deck. Version 3 rejects all of these properties as unknown.
The riverbed material keeps its position in TERRAIN_MATERIALS, so saved material indices keep
their meaning.
Small round trip
import {
readMapArtifact,
validateMapDocument,
writeMapArtifact,
type MapDocument,
} from '@velo/crown-base';
export function editAndReopen(current: MapDocument): MapDocument {
const candidate = structuredClone(current);
candidate.schematic.field.heights[0] += 10; // centimetres
candidate.schematic.field.version += 1;
const checked = validateMapDocument(candidate);
if (!checked.ok) {
throw checked.error;
}
const json = writeMapArtifact(checked.document);
const reopened = readMapArtifact(json);
if (!reopened.ok) {
throw reopened.error;
}
return reopened.document;
}
The focused package tests cover authored documents, every represented layer/site/reference,
malformed input, strict static boundaries and cache-version persistence. Game adapter tests add
generated maps at both sizes, classic conversion and an actual preview-rule cache refresh.
Run package checks locally as below. Game adapter coverage runs from games/crownfall/ with
node scripts/checks.ts unit tests/map-artifact.test.ts. The root bridge remains temporarily
for CI discovery until #763 selects the package directly.
Build
From this directory, install the package's development dependency and compile the TypeScript:
npm install
npm run build
The compiler writes JavaScript, source maps, and type declarations to dist/. The package tarball
contains that output and this README.
Forgejo npm registry
The package is published to the Forgejo npm registry owned by velo:
https://git.amhk.dk/api/packages/velo/npm/
The package is public, so installing it does not require a Forgejo access token. Configure npm to
use the Forgejo registry for the @velo scope, either in your project .npmrc:
@velo:registry=https://git.amhk.dk/api/packages/velo/npm/
Then install the library in that project:
npm install @velo/crown-base
Publishing requires a Forgejo access token with package write permission. To publish manually, first build and then publish against the Forgejo registry:
npm run build
npm publish --registry=https://git.amhk.dk/api/packages/velo/npm/
The Publish crown-base workflow publishes on pushes to main touching this package and
manual dispatch. It selects the next 0.x.0 above registry versions, independently of the
committed manifest version. Reuse this workflow for the additive exports below.
Crownfall public-package handoff (#774)
Crownfall imports only @velo/crown-base, never package source or private test fixtures.
The entry point additionally exports staticMapShape, checkStaticMapBounds and pointSchema
so terrain delivery can compose the existing strict geometry validation with its wire schema.
Their validation and artifact semantics are unchanged. Zod remains the declared runtime dependency;
consumers composing these schemas should resolve one compatible Zod 4 instance (^4.6.7).
Registry inspection during #774 found releases through 0.5.0. The existing publisher therefore
selects 0.6.0 for this addition if no intervening release lands. The project-configuration sibling
must adopt the actual release containing these three exports (expected ^0.6.0) and update its
lockfile. The committed 0.1.0 manifest does not identify that release.
After installing package-local dependencies, npm run test:consumer compiles and packs this
package, installs it into a temporary project, type-checks public schema composition and runs
terrain codec checks with declared dependencies only. It leaves no tarball in the repository.
Package-owned checks
Run from this package directory, using Node 24:
npm ci
node node_modules/tsx/dist/cli.mjs --test test/index.test.ts test/map-artifact.test.ts
The test script discovers test/*.test.ts locally with its declared tsx dev dependency.
test:consumer remains the separate packed public-consumer check. Neither needs a root install
or a game. Root bridge tests remain for #763 to remove when CI selects package commands;
package-owned artifact/paint coverage must stay selected during that transition.
Dependencies
Dependencies
| ID | Version |
|---|---|
| zod | ^4.6.7 |
Development dependencies
| ID | Version |
|---|---|
| tsx | ^4.23.15 |
| typescript | ^5.9.3 |