@velo/crown-base (0.15.0)

Published 2026-11-01 12:47:45 +01:00 by r59q

Installation

@velo:registry=
npm install @velo/crown-base@0.15.0
"@velo/crown-base": "0.15.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, resource-node placements with positive initial stock, outposts, generator provenance and the terrain field. Nodes accept registered kinds and only their matching resource, including trees, quarries, carcasses and berry bushes. Animal templates remain in the game-owned entities extension. ENTITY_DEFINITIONS, kind arrays/types and NODE_RESOURCES are generated from Crownfall's entity inventory; they contain no live state. Horizontal coordinates and lengths are metres; terrain heights are whole centimetres. Numeric site IDs are unique across objectives, resource nodes 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 to initialQuantity in 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 a water layer, waterBodies and spans. 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. Sand (5), mud (6), snow (7) and deep snow (8) are appended after rock. Each entry also states its gameplay meaning: speedMultiplier (0.8 for mud and deep snow, otherwise 1) and buildable (false only for mud). Validation, the codec and artifacts accept exactly the catalogue's indices; until a consuming application installs a release with these entries, it must keep its own compatible catalogue, as Crownfall's shared/terrain-materials.ts does.

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.

Static maps may carry public cosmetic decorations: up to 8,192 grass, pebble, flower, stone or ruin objects (DECORATION_KINDS), with in-bounds x/z metres, scale 0.5–2, rotation 0–2π radians and variant 0 or 1. The optional member round-trips in version 3 artifacts; it has no gameplay meaning. Older strict package releases reject decorated files. Consumers, including @velo/editor, must use a base release containing this schema before opening them. Files without decoration remain valid.

Static maps may also carry public waterBodies and neutral bridgeSpans (Crownfall #984). A water body is { x, z, surfaceCentimetres }: a source point inside the battlefield and a whole centimetre surface on the field's height datum; the game fills the ground joined to the source below that surface and derives depth and passability itself. A bridge span is { from: { x, z }, to: { x, z } }, both ends inside the battlefield. At most 32 bodies and 64 spans; no owners, IDs or live state. Eligibility of a span depends on the terrain and is checked by the game, not by this schema. Files without water remain valid; older strict releases reject files that use these members.

Dependencies

Dependencies

ID Version
zod ^4.6.7

Development dependencies

ID Version
tsx ^4.23.15
typescript ^5.9.3
Details
npm
2026-11-01 12:47:45 +01:00
0
UNLICENSED
36 KiB
Assets (1)
Versions (20) View all
0.20.0 2026-11-04
0.19.0 2026-11-04
0.18.0 2026-11-03
0.17.0 2026-11-03
0.16.0 2026-11-02