@jgengine/core 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/dist/audio/uiCues.d.ts +26 -0
  3. package/dist/audio/uiCues.js +118 -0
  4. package/dist/commands/commandRegistry.d.ts +4 -0
  5. package/dist/commands/commandRegistry.js +5 -2
  6. package/dist/editor/index.d.ts +1 -0
  7. package/dist/editor/index.js +1 -0
  8. package/dist/editor/terrainMaterials.d.ts +10 -0
  9. package/dist/editor/terrainMaterials.js +13 -0
  10. package/dist/game/events.d.ts +12 -0
  11. package/dist/game/notices.d.ts +85 -0
  12. package/dist/game/notices.js +105 -0
  13. package/dist/game/playableGame.d.ts +37 -0
  14. package/dist/meta/changelog.d.ts +1 -1
  15. package/dist/meta/changelog.js +107 -1
  16. package/dist/render/lightingBalance.d.ts +13 -0
  17. package/dist/render/lightingBalance.js +32 -0
  18. package/dist/render/lookPreset.d.ts +10 -1
  19. package/dist/render/lookPreset.js +28 -3
  20. package/dist/render/postProcessing.d.ts +6 -1
  21. package/dist/render/postProcessing.js +2 -0
  22. package/dist/runtime/commandDispatch.js +10 -3
  23. package/dist/runtime/commandRunner.d.ts +3 -0
  24. package/dist/runtime/commandRunner.js +3 -1
  25. package/dist/runtime/context/scene.d.ts +3 -0
  26. package/dist/runtime/context/scene.js +6 -0
  27. package/dist/runtime/gameContext.js +13 -0
  28. package/dist/runtime/gameContextTypes.d.ts +19 -0
  29. package/dist/runtime/transport.d.ts +4 -1
  30. package/dist/settings/graphicsProfile.d.ts +7 -0
  31. package/dist/settings/graphicsProfile.js +4 -0
  32. package/dist/settings/graphicsSettings.d.ts +9 -0
  33. package/dist/settings/graphicsSettings.js +29 -0
  34. package/dist/world/buildings.d.ts +7 -0
  35. package/dist/world/buildings.js +20 -0
  36. package/dist/world/environmentSummary.js +7 -3
  37. package/dist/world/features.d.ts +24 -5
  38. package/dist/world/features.js +2 -0
  39. package/dist/world/grassKind.d.ts +10 -0
  40. package/dist/world/grassKind.js +22 -0
  41. package/dist/world/terraform.d.ts +3 -1
  42. package/dist/world/terraform.js +1 -0
  43. package/dist/world/terrain.d.ts +3 -2
  44. package/dist/world/terrain.js +16 -3
  45. package/package.json +1 -1
  46. package/skills/game-design/SKILL.md +2 -0
  47. package/skills/jgengine/SKILL.md +1 -0
  48. package/skills/jgengine/api.md +18 -18
  49. package/skills/jgengine-gameplay/api.md +18 -4
  50. package/skills/jgengine-gameplay/capabilities.md +5 -0
  51. package/skills/jgengine-world/api.md +28 -15
  52. package/skills/jgengine-world/capabilities.md +4 -0
  53. package/skills/jgengine-world/reference.md +30 -0
@@ -1,4 +1,9 @@
1
1
  import type { GraphicsQuality } from "./settingsModel.js";
2
+ /**
3
+ * Procedural surface detail on loaded models: `"full"` adds mottling, wear, roughness break-up, a
4
+ * micro-normal and cavity shading; `"lite"` drops the micro-normal; `"off"` compiles the plain material.
5
+ */
6
+ export type SurfaceDetailLevel = "off" | "lite" | "full";
2
7
  /** Serializable renderer budget selected by the player's graphics tier. */
3
8
  export interface GraphicsProfile {
4
9
  renderScale: number;
@@ -14,6 +19,8 @@ export interface GraphicsProfile {
14
19
  dof: boolean;
15
20
  smaa: boolean;
16
21
  };
22
+ /** Default painted-surface shader on loaded models. */
23
+ surfaceDetail: SurfaceDetailLevel;
17
24
  }
18
25
  /** Conservative defaults that preserve the existing high-quality shell behavior. */
19
26
  export declare const DEFAULT_GRAPHICS_PROFILES: Record<GraphicsQuality, GraphicsProfile>;
@@ -8,6 +8,7 @@ export const DEFAULT_GRAPHICS_PROFILES = {
8
8
  particleCap: 128,
9
9
  anisotropy: 2,
10
10
  postStages: { ao: false, bloom: true, dof: false, smaa: true },
11
+ surfaceDetail: "lite",
11
12
  },
12
13
  medium: {
13
14
  renderScale: 1.5,
@@ -17,6 +18,7 @@ export const DEFAULT_GRAPHICS_PROFILES = {
17
18
  particleCap: 256,
18
19
  anisotropy: 4,
19
20
  postStages: { ao: true, bloom: true, dof: true, smaa: true },
21
+ surfaceDetail: "full",
20
22
  },
21
23
  high: {
22
24
  renderScale: 2,
@@ -26,6 +28,7 @@ export const DEFAULT_GRAPHICS_PROFILES = {
26
28
  particleCap: 512,
27
29
  anisotropy: 8,
28
30
  postStages: { ao: true, bloom: true, dof: true, smaa: true },
31
+ surfaceDetail: "full",
29
32
  },
30
33
  ultra: {
31
34
  renderScale: 2,
@@ -35,6 +38,7 @@ export const DEFAULT_GRAPHICS_PROFILES = {
35
38
  particleCap: 1024,
36
39
  anisotropy: 16,
37
40
  postStages: { ao: true, bloom: true, dof: true, smaa: true },
41
+ surfaceDetail: "full",
38
42
  },
39
43
  };
40
44
  /** Resolve one tier and apply game-authored field and post-stage overrides. */
@@ -38,3 +38,12 @@ export declare function readGraphicsSettings(store: Pick<SettingsStore, "get">,
38
38
  * resets the individual overrides instead of leaving stale toggles behind.
39
39
  */
40
40
  export declare function applyGraphicsQuality(store: Pick<SettingsStore, "set">, quality: GraphicsQuality, overrides?: GraphicsProfileOverrides): GraphicsProfile;
41
+ /**
42
+ * Wrap a settings store so the graphics tier reads as `quality` for this session only: every
43
+ * graphics field resolves from that tier until the player changes it, and nothing is persisted.
44
+ * Capture hosts use it (`?quality=low`) to render cheap iteration shots without leaving the
45
+ * draft tier in the browser profile for the next full-quality shot.
46
+ */
47
+ export declare function withGraphicsQualityOverride(store: SettingsStore, quality: GraphicsQuality, overrides?: GraphicsProfileOverrides): SettingsStore;
48
+ /** Parse a `?quality=` value into a graphics tier, or null when absent or unknown. */
49
+ export declare function parseGraphicsQuality(raw: string | null | undefined): GraphicsQuality | null;
@@ -79,3 +79,32 @@ export function applyGraphicsQuality(store, quality, overrides) {
79
79
  }
80
80
  return tier;
81
81
  }
82
+ /**
83
+ * Wrap a settings store so the graphics tier reads as `quality` for this session only: every
84
+ * graphics field resolves from that tier until the player changes it, and nothing is persisted.
85
+ * Capture hosts use it (`?quality=low`) to render cheap iteration shots without leaving the
86
+ * draft tier in the browser profile for the next full-quality shot.
87
+ */
88
+ export function withGraphicsQualityOverride(store, quality, overrides) {
89
+ const tier = resolveGraphicsProfile(quality, overrides?.[quality]);
90
+ const pinned = new Map([
91
+ [SETTING_IDS.graphicsQuality, quality],
92
+ [SETTING_IDS.graphicsRenderScale, tier.renderScale],
93
+ [SETTING_IDS.graphicsAnisotropy, String(tier.anisotropy)],
94
+ ...GRAPHICS_POST_STAGES.map((stage) => [graphicsPostStageSettingId(stage), tier.postStages[stage]]),
95
+ ]);
96
+ return {
97
+ get(id, fallback) {
98
+ return pinned.has(id) ? pinned.get(id) : store.get(id, fallback);
99
+ },
100
+ set(id, value) {
101
+ pinned.delete(id);
102
+ store.set(id, value);
103
+ },
104
+ subscribe: store.subscribe,
105
+ };
106
+ }
107
+ /** Parse a `?quality=` value into a graphics tier, or null when absent or unknown. */
108
+ export function parseGraphicsQuality(raw) {
109
+ return isGraphicsQuality(raw) ? raw : null;
110
+ }
@@ -140,6 +140,13 @@ export declare function resolveBuildingSurface(surface: BuildingSurface): Buildi
140
140
  export type BuildingStyle = "generic" | "capital" | "village" | "desert" | "industrial" | "coastal" | "neon" | "ruin" | "frontier" | "aerial";
141
141
  export declare const DEFAULT_BUILDING_STYLE: BuildingStyle;
142
142
  export declare const BUILDING_STYLE_PALETTES: Record<BuildingStyle, Record<BuildingPartKind, string>>;
143
+ /**
144
+ * Facade part probabilities per style, so an industrial yard, a ruined block and a coastal strip
145
+ * differ in shape as well as colour. Unset parts keep the generic probabilities.
146
+ */
147
+ export declare const BUILDING_STYLE_PROBABILITIES: Record<BuildingStyle, Partial<BuildingProbabilities>>;
148
+ /** Style facade probabilities with explicit overrides applied on top. */
149
+ export declare function resolveBuildingProbabilities(style?: BuildingStyle, overrides?: Partial<BuildingProbabilities>): Partial<BuildingProbabilities>;
143
150
  /**
144
151
  * Per-style facade-tone family: the wall colours a district spreads across its buildings so
145
152
  * neighbours differ in hue and value while the block stays one palette. Index 0 is the palette's
@@ -191,6 +191,26 @@ export const BUILDING_STYLE_PALETTES = {
191
191
  corner: "#c3d0d7",
192
192
  },
193
193
  };
194
+ /**
195
+ * Facade part probabilities per style, so an industrial yard, a ruined block and a coastal strip
196
+ * differ in shape as well as colour. Unset parts keep the generic probabilities.
197
+ */
198
+ export const BUILDING_STYLE_PROBABILITIES = {
199
+ generic: {},
200
+ capital: { window: 0.95, awning: 0.3, clothesline: 0.02, storeSign: 0.5 },
201
+ village: { window: 0.7, openStore: 0.25, awning: 0.3, airConditioner: 0.04, clothesline: 0.4, storeSign: 0.18, roofProp: 0.12 },
202
+ desert: { window: 0.45, awning: 0.55, airConditioner: 0.2, clothesline: 0.18, storeSign: 0.35, roofProp: 0.4 },
203
+ industrial: { window: 0.3, openStore: 0.12, awning: 0, airConditioner: 0.55, clothesline: 0, storeSign: 0.08, roofProp: 0.8 },
204
+ coastal: { window: 0.85, awning: 0.62, clothesline: 0.32, storeSign: 0.55 },
205
+ neon: { window: 0.9, awning: 0.2, clothesline: 0.05, storeSign: 0.95, roofProp: 0.5 },
206
+ ruin: { window: 0.5, openStore: 0.2, awning: 0.04, airConditioner: 0.04, clothesline: 0.03, storeSign: 0.04, roofProp: 0.12 },
207
+ frontier: { window: 0.5, openStore: 0.45, awning: 0.58, airConditioner: 0, clothesline: 0.12, storeSign: 0.45, roofProp: 0.1 },
208
+ aerial: { window: 0.95, awning: 0, airConditioner: 0.3, clothesline: 0, storeSign: 0.2, roofProp: 0.6 },
209
+ };
210
+ /** Style facade probabilities with explicit overrides applied on top. */
211
+ export function resolveBuildingProbabilities(style = DEFAULT_BUILDING_STYLE, overrides) {
212
+ return { ...BUILDING_STYLE_PROBABILITIES[style], ...overrides };
213
+ }
194
214
  /**
195
215
  * Per-style facade-tone family: the wall colours a district spreads across its buildings so
196
216
  * neighbours differ in hue and value while the block stays one palette. Index 0 is the palette's
@@ -1,4 +1,4 @@
1
- import { DEFAULT_BUILDING_CONFIG, generateBuilding, generateBuildingDistrict, resolveBuildingPalette, } from "./buildings.js";
1
+ import { DEFAULT_BUILDING_CONFIG, generateBuilding, generateBuildingDistrict, resolveBuildingPalette, resolveBuildingProbabilities, } from "./buildings.js";
2
2
  import { buildingKitVariantCounts } from "./buildingKit.js";
3
3
  import { deriveBuildingLots } from "./buildingLots.js";
4
4
  import { pathLength } from "./roads.js";
@@ -22,7 +22,11 @@ export function resolveStructureBuildings(descriptor) {
22
22
  lotSize: descriptor.footprint,
23
23
  streetWidth: spacing,
24
24
  floorRange: descriptor.stories,
25
- base: { floorHeight: descriptor.storyHeight, ...kitVariants(descriptor.kit) },
25
+ base: {
26
+ floorHeight: descriptor.storyHeight,
27
+ probabilities: resolveBuildingProbabilities(descriptor.style, descriptor.probabilities),
28
+ ...kitVariants(descriptor.kit),
29
+ },
26
30
  ...(descriptor.seed === undefined ? {} : { seed: descriptor.seed }),
27
31
  }).slice(0, descriptor.count);
28
32
  }
@@ -65,7 +69,7 @@ function resolveFrontageBuildings(descriptor, along) {
65
69
  });
66
70
  const [floorsMin, floorsMax] = descriptor.stories;
67
71
  const bayWidth = DEFAULT_BUILDING_CONFIG.bayWidth;
68
- const variants = kitVariants(descriptor.kit);
72
+ const variants = { ...kitVariants(descriptor.kit), probabilities: resolveBuildingProbabilities(descriptor.style, descriptor.probabilities) };
69
73
  return lots.map((lot, index) => buildLot(lot, index, seed, floorsMin, floorsMax, bayWidth, descriptor.storyHeight, variants));
70
74
  }
71
75
  function buildLot(lot, index, seed, floorsMin, floorsMax, bayWidth, storyHeight, variants = {}) {
@@ -1,4 +1,4 @@
1
- import type { BuildingPaletteOverrides, BuildingStyle } from "./buildings.js";
1
+ import type { BuildingPaletteOverrides, BuildingProbabilities, BuildingStyle } from "./buildings.js";
2
2
  import type { BuildingKit } from "./buildingKit.js";
3
3
  import type { AvoidZone } from "./geometry.js";
4
4
  import type { TerrainPathProfile } from "./pathTerrain.js";
@@ -39,7 +39,7 @@ export interface TerrainDetailConfig {
39
39
  rockSlopeStart?: number;
40
40
  /** World height above which snow appears. Default 26. */
41
41
  snowHeight?: number;
42
- /** Water level for the sand band; defaults to the terrain's `waterLevel`. */
42
+ /** Water level for the sand band; defaults to the terrain's `waterLevel`, and no band is drawn when the terrain has none. */
43
43
  waterLevel?: number;
44
44
  /** Fine detail noise wavelength in world units. Default 5. */
45
45
  detailScale?: number;
@@ -49,6 +49,12 @@ export interface TerrainDetailConfig {
49
49
  roughness?: number;
50
50
  /** Overall detail intensity 0..1. Default 1. */
51
51
  strength?: number;
52
+ /**
53
+ * Per-surface ground response 0..1 for painted layers: grass, dirt, gravel, rock, sand, mud, snow
54
+ * and road each draw their own procedural albedo, roughness and micro-normal, meet along
55
+ * noise-broken edges, and slopes turn to rock. Default 1; 0 keeps flat painted colour.
56
+ */
57
+ surfaceStrength?: number;
52
58
  /**
53
59
  * RGB multipliers for the two macro colour sweeps painted over the base ground. `dry` tints the
54
60
  * sun-bleached patches, `wet` the cooler pockets between them. The defaults ([1.14, 1.05, 0.76]
@@ -208,8 +214,12 @@ export interface TerrainEnvironmentConfig {
208
214
  flatten?: readonly TerrainFlattenMask[];
209
215
  /** Authored path profiles that flatten/grade/carve the heightfield along a centerline (roads, rivers, ramps) with optional retaining walls. Applied after `flatten`. */
210
216
  pathProfiles?: readonly TerrainPathProfile[];
211
- /** Procedural detail surface (noise-driven rock/sand/snow over the biome base). Omit for the flat vertex-colour ground. */
212
- detail?: TerrainDetailConfig;
217
+ /**
218
+ * Procedural detail surface (per-surface painted ground, slope rock, sand and snow over the biome
219
+ * base). Omitted, the ground still renders with default detail; `false` opts out to the flat
220
+ * vertex-colour ground.
221
+ */
222
+ detail?: TerrainDetailConfig | false;
213
223
  }
214
224
  export interface RainEnvironmentConfig {
215
225
  /**
@@ -388,6 +398,8 @@ export interface BuildingEnvironmentConfig {
388
398
  style?: BuildingStyle;
389
399
  /** Explicit per-part-kind hex colors; any part left unset falls back to the resolved `style` palette. */
390
400
  palette?: BuildingPaletteOverrides;
401
+ /** Facade part probabilities (windows, shopfronts, awnings, …); unset parts follow the `style`. */
402
+ probabilities?: Partial<BuildingProbabilities>;
391
403
  seed?: string;
392
404
  /**
393
405
  * Street-aware placement: derive lots along the given roads' frontage (facing each street, set
@@ -445,7 +457,7 @@ export type OceanEnvironmentDescriptor = {
445
457
  } & Required<Pick<OceanEnvironmentConfig, "bounds" | "level" | "waveHeight" | "waveScale" | "waveSpeed" | "color">> & Pick<OceanEnvironmentConfig, "position" | "levelAt">;
446
458
  export type BuildingEnvironmentDescriptor = {
447
459
  kind: "building";
448
- } & Required<Pick<BuildingEnvironmentConfig, "count" | "footprint" | "stories" | "storyHeight" | "spacing" | "style">> & Pick<BuildingEnvironmentConfig, "seed" | "position" | "palette" | "along" | "kit" | "solid">;
460
+ } & Required<Pick<BuildingEnvironmentConfig, "count" | "footprint" | "stories" | "storyHeight" | "spacing" | "style">> & Pick<BuildingEnvironmentConfig, "seed" | "position" | "palette" | "probabilities" | "along" | "kit" | "solid">;
449
461
  export type PadEnvironmentDescriptor = {
450
462
  kind: "pad";
451
463
  } & Required<Pick<PadEnvironmentConfig, "center" | "size" | "height" | "color">> & Pick<PadEnvironmentConfig, "rotationY" | "elevation">;
@@ -471,6 +483,11 @@ export interface RoadEnvironmentConfig {
471
483
  width?: number;
472
484
  color?: string;
473
485
  } | false;
486
+ /**
487
+ * Procedural surface the carriageway shades with, named like terrain paint (`"asphalt"`,
488
+ * `"gravel"`, `"dirt"`). `false` keeps flat colour. Default `"road"` (asphalt grain and tyre wear).
489
+ */
490
+ surface?: string | false;
474
491
  }
475
492
  /** Resolved road descriptor produced by {@link road} and rendered by the shell environment scene. */
476
493
  export type RoadEnvironmentDescriptor = {
@@ -481,6 +498,8 @@ export type RoadEnvironmentDescriptor = {
481
498
  width: number;
482
499
  color: string;
483
500
  } | false;
501
+ /** Procedural carriageway surface; absent means `"road"`, `false` flat colour. */
502
+ surface?: string | false;
484
503
  };
485
504
  export type WeatherEnvironmentDescriptor = RainEnvironmentDescriptor | SnowEnvironmentDescriptor;
486
505
  export type VegetationEnvironmentDescriptor = GrassEnvironmentDescriptor;
@@ -233,6 +233,7 @@ export function road(config) {
233
233
  sidewalk: config.sidewalk === false
234
234
  ? false
235
235
  : { width: config.sidewalk?.width ?? 2.6, color: config.sidewalk?.color ?? "#a7adb8" },
236
+ ...(config.surface === undefined ? {} : { surface: config.surface }),
236
237
  };
237
238
  }
238
239
  export function island(config) {
@@ -256,6 +257,7 @@ export function building(config = {}) {
256
257
  ...(config.seed === undefined ? {} : { seed: config.seed }),
257
258
  ...(config.position === undefined ? {} : { position: config.position }),
258
259
  ...(config.palette === undefined ? {} : { palette: config.palette }),
260
+ ...(config.probabilities === undefined ? {} : { probabilities: config.probabilities }),
259
261
  ...(config.along === undefined ? {} : { along: config.along }),
260
262
  ...(config.kit === undefined ? {} : { kit: config.kit }),
261
263
  ...(config.solid === undefined ? {} : { solid: config.solid }),
@@ -16,6 +16,16 @@ export interface GrassRules {
16
16
  density: number;
17
17
  /** Blade height in meters. */
18
18
  bladeHeight: number;
19
+ /** Blade-width range in metres; endpoints are normalized on read. */
20
+ bladeWidthMin: number;
21
+ bladeWidthMax: number;
22
+ /** Bend range, 0..1. */
23
+ bladeBendMin: number;
24
+ bladeBendMax: number;
25
+ /** Spread of blades around each tuft root, in metres. */
26
+ tuftRadius: number;
27
+ /** Normal lift toward the sky, 0..1; controls the card lighting response. */
28
+ normalLift: number;
19
29
  /** Base (root) color, hex. */
20
30
  colorBase: string;
21
31
  /** Tip color, hex. */
@@ -15,6 +15,12 @@ export const GRASS_FIELD_KIND = "grass_field";
15
15
  export const GRASS_DEFAULTS = {
16
16
  density: 12,
17
17
  bladeHeight: 0.6,
18
+ bladeWidthMin: 0.05,
19
+ bladeWidthMax: 0.1,
20
+ bladeBendMin: 0.12,
21
+ bladeBendMax: 0.42,
22
+ tuftRadius: 0.16,
23
+ normalLift: 0.6,
18
24
  colorBase: "#3f6f37",
19
25
  colorTip: "#9ac25a",
20
26
  colorVariation: 0.35,
@@ -29,6 +35,12 @@ export const GRASS_SCHEMA = {
29
35
  fields: [
30
36
  { type: "range", key: "density", label: "density", min: 0.5, max: 160, step: 0.5, default: GRASS_DEFAULTS.density, unit: "/m²" },
31
37
  { type: "range", key: "bladeHeight", label: "blade height", min: 0.1, max: 2, step: 0.05, default: GRASS_DEFAULTS.bladeHeight, unit: "m" },
38
+ { type: "range", key: "bladeWidthMin", label: "minimum blade width", min: 0.005, max: 0.3, step: 0.005, default: GRASS_DEFAULTS.bladeWidthMin, unit: "m" },
39
+ { type: "range", key: "bladeWidthMax", label: "maximum blade width", min: 0.005, max: 0.3, step: 0.005, default: GRASS_DEFAULTS.bladeWidthMax, unit: "m" },
40
+ { type: "range", key: "bladeBendMin", label: "minimum bend", min: 0, max: 1, step: 0.01, default: GRASS_DEFAULTS.bladeBendMin },
41
+ { type: "range", key: "bladeBendMax", label: "maximum bend", min: 0, max: 1, step: 0.01, default: GRASS_DEFAULTS.bladeBendMax },
42
+ { type: "range", key: "tuftRadius", label: "tuft radius", min: 0, max: 0.6, step: 0.01, default: GRASS_DEFAULTS.tuftRadius, unit: "m" },
43
+ { type: "range", key: "normalLift", label: "normal lift", min: 0, max: 1, step: 0.01, default: GRASS_DEFAULTS.normalLift },
32
44
  { type: "color", key: "colorBase", label: "base color", default: GRASS_DEFAULTS.colorBase },
33
45
  { type: "color", key: "colorTip", label: "tip color", default: GRASS_DEFAULTS.colorTip },
34
46
  { type: "range", key: "colorVariation", label: "color variation", min: 0, max: 1, step: 0.02, default: GRASS_DEFAULTS.colorVariation },
@@ -49,9 +61,19 @@ function metaString(meta, key, fallback) {
49
61
  }
50
62
  /** Parse a volume's `meta` into fully-defaulted grass rules. @internal */
51
63
  export function readGrassRules(meta) {
64
+ const widthMin = Math.max(0.005, metaNumber(meta, "bladeWidthMin", GRASS_DEFAULTS.bladeWidthMin));
65
+ const widthMax = Math.max(0.005, metaNumber(meta, "bladeWidthMax", GRASS_DEFAULTS.bladeWidthMax));
66
+ const bendMin = Math.max(0, Math.min(1, metaNumber(meta, "bladeBendMin", GRASS_DEFAULTS.bladeBendMin)));
67
+ const bendMax = Math.max(0, Math.min(1, metaNumber(meta, "bladeBendMax", GRASS_DEFAULTS.bladeBendMax)));
52
68
  return {
53
69
  density: Math.max(0, metaNumber(meta, "density", GRASS_DEFAULTS.density)),
54
70
  bladeHeight: Math.max(0.05, metaNumber(meta, "bladeHeight", GRASS_DEFAULTS.bladeHeight)),
71
+ bladeWidthMin: Math.min(widthMin, widthMax),
72
+ bladeWidthMax: Math.max(widthMin, widthMax),
73
+ bladeBendMin: Math.min(bendMin, bendMax),
74
+ bladeBendMax: Math.max(bendMin, bendMax),
75
+ tuftRadius: Math.max(0, metaNumber(meta, "tuftRadius", GRASS_DEFAULTS.tuftRadius)),
76
+ normalLift: Math.max(0, Math.min(1, metaNumber(meta, "normalLift", GRASS_DEFAULTS.normalLift))),
55
77
  colorBase: metaString(meta, "colorBase", GRASS_DEFAULTS.colorBase),
56
78
  colorTip: metaString(meta, "colorTip", GRASS_DEFAULTS.colorTip),
57
79
  colorVariation: metaNumber(meta, "colorVariation", GRASS_DEFAULTS.colorVariation),
@@ -53,7 +53,7 @@ export interface TerrainMaterialLayer {
53
53
  tiling?: number;
54
54
  /** Project the material on all three axes to hide stretching on cliffs. */
55
55
  triplanar?: boolean;
56
- /** Hex color multiplied over the surface color. */
56
+ /** Authored hex color replacing the default surface palette color in editor and runtime. */
57
57
  tint?: string;
58
58
  /** Layer master opacity 0..1 — scales this layer's blend weight everywhere. */
59
59
  opacity?: number;
@@ -125,6 +125,8 @@ export interface EditableTerrain extends TerrainField {
125
125
  readonly cols: number;
126
126
  readonly rows: number;
127
127
  readonly cellSize: number;
128
+ /** World-space extent of the cell grid; cell `(gx, gz)` spans `cols`/`rows` equal slices of it. */
129
+ readonly gridBounds: Aabb;
128
130
  apply(edit: TerraformEdit): number;
129
131
  /** Apply an edit while reporting every touched vertex to `record` — the seam strokes record deltas through. */
130
132
  applyRecording(edit: TerraformEdit, record: TerraformDeltaRecorder): number;
@@ -412,6 +412,7 @@ export function createEditableTerrain(config) {
412
412
  cols,
413
413
  rows,
414
414
  cellSize,
415
+ gridBounds: { minX: bounds.minX, minZ: bounds.minZ, maxX: bounds.minX + spanX, maxZ: bounds.minZ + spanZ },
415
416
  sampleHeight,
416
417
  sampleNormal,
417
418
  ...(base.bounds === undefined ? {} : { bounds: base.bounds }),
@@ -211,10 +211,11 @@ export type ResolvedTerrainDetail = Required<Omit<TerrainDetailConfig, "waterLev
211
211
  wet: readonly [number, number, number];
212
212
  };
213
213
  };
214
- /** Fill a `TerrainDetailConfig` with defaults; `waterLevel` falls back to the terrain's own water level.
214
+ /** Fill a `TerrainDetailConfig` with defaults; `waterLevel` falls back to the terrain's own water level,
215
+ * and a terrain without one gets no sand band. `undefined` resolves the implicit default detail.
215
216
  * @internal
216
217
  */
217
- export declare function resolveTerrainDetail(config: TerrainDetailConfig, terrainWaterLevel?: number): ResolvedTerrainDetail;
218
+ export declare function resolveTerrainDetail(config: TerrainDetailConfig | undefined, terrainWaterLevel?: number): ResolvedTerrainDetail;
218
219
  /** @internal */
219
220
  export declare function resolveTerrainPalette(descriptor?: Pick<TerrainEnvironmentConfig, "material" | "colors">): TerrainPalette;
220
221
  /**
@@ -475,15 +475,27 @@ const DEFAULT_TERRAIN_DETAIL = {
475
475
  macroScale: 45,
476
476
  roughness: 0.9,
477
477
  strength: 1,
478
+ surfaceStrength: 1,
478
479
  sweeps: { dry: [1.14, 1.05, 0.76], wet: [0.78, 1.0, 0.86] },
479
480
  };
481
+ /** Detail for a terrain that declares none: quieter base sweeps and no altitude snow. */
482
+ const IMPLICIT_TERRAIN_DETAIL = {
483
+ strength: 0.6,
484
+ snowHeight: 1e6,
485
+ sweeps: { dry: [1.08, 1.03, 0.88], wet: [0.92, 0.98, 0.94] },
486
+ };
480
487
  const DEFAULT_TERRAIN_MATERIAL_REPEAT = 4;
481
488
  const DEFAULT_TERRAIN_MATERIAL_STRENGTH = 1;
482
489
  const DEFAULT_TERRAIN_MATERIAL_TINT = 1;
483
- /** Fill a `TerrainDetailConfig` with defaults; `waterLevel` falls back to the terrain's own water level.
490
+ /** Sand-band level for a terrain with no water: far below any ground, so no beach is painted. */
491
+ const NO_WATER_SAND_LEVEL = -1e6;
492
+ /** Fill a `TerrainDetailConfig` with defaults; `waterLevel` falls back to the terrain's own water level,
493
+ * and a terrain without one gets no sand band. `undefined` resolves the implicit default detail.
484
494
  * @internal
485
495
  */
486
- export function resolveTerrainDetail(config, terrainWaterLevel = 0) {
496
+ export function resolveTerrainDetail(config, terrainWaterLevel) {
497
+ if (config === undefined)
498
+ return resolveTerrainDetail(IMPLICIT_TERRAIN_DETAIL, terrainWaterLevel);
487
499
  return {
488
500
  rockColor: config.rockColor ?? DEFAULT_TERRAIN_DETAIL.rockColor,
489
501
  sandColor: config.sandColor ?? DEFAULT_TERRAIN_DETAIL.sandColor,
@@ -494,11 +506,12 @@ export function resolveTerrainDetail(config, terrainWaterLevel = 0) {
494
506
  macroScale: config.macroScale ?? DEFAULT_TERRAIN_DETAIL.macroScale,
495
507
  roughness: config.roughness ?? DEFAULT_TERRAIN_DETAIL.roughness,
496
508
  strength: clamp01(config.strength ?? DEFAULT_TERRAIN_DETAIL.strength),
509
+ surfaceStrength: clamp01(config.surfaceStrength ?? DEFAULT_TERRAIN_DETAIL.surfaceStrength),
497
510
  sweeps: {
498
511
  dry: config.sweeps?.dry ?? DEFAULT_TERRAIN_DETAIL.sweeps.dry,
499
512
  wet: config.sweeps?.wet ?? DEFAULT_TERRAIN_DETAIL.sweeps.wet,
500
513
  },
501
- waterLevel: config.waterLevel ?? terrainWaterLevel,
514
+ waterLevel: config.waterLevel ?? terrainWaterLevel ?? NO_WATER_SAND_LEVEL,
502
515
  ...(config.material === undefined
503
516
  ? {}
504
517
  : {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgengine/core",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "jgengine core — pure-TypeScript game framework SDK (npm @jgengine/core). Entity stores, commands, combat, inventory, multiplayer seams. Zero dependencies. Not an ECS. Agent skills ship in the tarball. https://jgengine.com",
5
5
  "keywords": [
6
6
  "jgengine",
@@ -11,6 +11,8 @@ Own the intended player experience and the rules that produce it: fantasy, pilla
11
11
 
12
12
  Use `level-design` for spatial realization; route implementation to `jgengine-gameplay`, `jgengine-world`, `jgengine-combat`, `jgengine-ui`, `jgengine-multiplayer` once intent is coherent.
13
13
 
14
+ For a substantial existing-game quality pass spanning concept, depth, world and presentation, use `game-improvement` to sequence these owners and keep a per-game implementation plan and acceptance evidence.
15
+
14
16
  For a greenfield build, read [references/application-playbook.md](references/application-playbook.md) and produce its greenfield contract (promise, pillars, anti-pillars, core decision, and falsifier) before writing code. Audits may read only the playbook's audit sections. Read [references/field-guide.md](references/field-guide.md) when the task needs deeper loop, economy, progression, genre, accessibility, or research guidance.
15
17
 
16
18
  ## Choose the mode
@@ -53,6 +53,7 @@ Load only rows the target needs. Do not preload every domain.
53
53
  | Need | Skill | Owner |
54
54
  | --- | --- | --- |
55
55
  | player promise, pillars, verbs, loops, economy, progression, balance | `game-design` | experience and rule intent |
56
+ | substantial existing-game improvement across depth, world and presentation | `game-improvement` | ordered improvement plan and per-game completion |
56
57
  | metrics, topology, encounters, navigation, spatial pacing | `level-design` | playable space intent |
57
58
  | scene placement, terrain painting, paths, asset authoring | `jgengine-editor` | editor document and authoring tools |
58
59
  | runtime world, movement, input, interaction, AI, navigation, audio | `jgengine-world` | spatial simulation and world runtime |
@@ -6,7 +6,7 @@
6
6
 
7
7
  - `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
8
8
  - `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
9
- - `VERSION` (const): const VERSION: "0.21.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
9
+ - `VERSION` (const): const VERSION: "0.23.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
10
10
 
11
11
  ## @jgengine/core/authoring
12
12
 
@@ -65,8 +65,8 @@
65
65
  - `CommandDecoder` (type): type CommandDecoder<TInput> = (input: unknown) => CommandDecodeResult<TInput> — Parses raw `unknown` transport input into `TInput`, rejecting anything that doesn't match the command's declared shape. Runs before `validate`/`apply`, so a malformed payload never reaches game logic.
66
66
  - `CommandDefinition` (interface): interface CommandDefinition<TState, TInput> { decode?: CommandDecoder<TInput>; validate?(state: TState, input: TInput): CommandRejection | null; apply(state: TState, input: TInput): TState | void } — ⚠ undocumented
67
67
  - `CommandRegistry` (interface): interface CommandRegistry<TState> { define<TInput>(name: string, definition: CommandDefinition<TState, TInput>): void; has(name: string): boolean; names(): string[]; run(state: TState, name: string, input: unknown): CommandResult<TState> } — ⚠ undocumented
68
- - `CommandRejection` (interface): interface CommandRejection { reason: string } — ⚠ undocumented · used by `validateCommandInput` (@jgengine/ws): Validates a `runCommand` input against a declared catalog.
69
- - `CommandResult` (type): type CommandResult<TState> = | { status: "applied"; state: TState } | { status: "rejected"; reason: string } | { status: "unknown-command" } — ⚠ undocumented
68
+ - `CommandRejection` (interface): interface CommandRejection — Why a command refused: `reason` is a stable machine code; `message`, when set, is shown to the player.
69
+ - `CommandResult` (type): type CommandResult<TState> = | { status: "applied"; state: TState } | { status: "rejected"; reason: string; message?: string } | { status: "unknown-command" } — ⚠ undocumented
70
70
  - `createCommandRegistry` (function): function createCommandRegistry<TState>(): CommandRegistry<TState> — ⚠ undocumented
71
71
 
72
72
  ## @jgengine/core/devtools/controls
@@ -232,7 +232,7 @@
232
232
 
233
233
  - `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
234
234
  - `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
235
- - `VERSION` (const): const VERSION: "0.21.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
235
+ - `VERSION` (const): const VERSION: "0.23.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
236
236
 
237
237
  ## @jgengine/core/runtime/adapter
238
238
 
@@ -292,9 +292,9 @@
292
292
  - `CommandDef` (type): type CommandDef<TInput = unknown> = { /** * `"server"` when `input` carries facts only the host may decide (unlock state, prices, multipliers): * public transports then refuse it and only trusted host calls such as `helpers.runCommand` run it. * Defaults to `"client"`, so every field of `input` must… — ⚠ undocumented · used by `defineCommand`: Declare a command with a typed input: `parse` narrows the wire value once, and `scope`, `validate` and `apply` receive the parsed type.
293
293
  - `CommandScope` (type): type CommandScope = { /** Member ids to hydrate; omit for the whole roster. */ players?: readonly string[]; /** Chunk keys to hydrate; omit for every chunk of the server, `[]` for none. */ chunkKeys?: readonly string[]; } — Which slice of a server has to be hydrated before some work runs against it. Both fields default to "everything", which costs a read per member plus one per chunk — the cost that makes a large shared world unaffordable per mutation. Narrow them when the caller knows what it will touch.
294
294
  - `CommandScopeDefinition` (type): type CommandScopeDefinition = Omit<CommandScope, "players"> & { players?: readonly string[] | "actor" } — Declarative read scope; actor is resolved before host hydration.
295
- - `CommandValidationError` (type): type CommandValidationError = { reason: string } — ⚠ undocumented
295
+ - `CommandValidationError` (type): type CommandValidationError = { reason: string; message?: string } — `reason` is a stable machine code; `message`, when set, is player-facing text the client shows on refusal.
296
296
  - `MALFORMED_COMMAND_INPUT_REASON` (const): const MALFORMED_COMMAND_INPUT_REASON: "Malformed command input" — Reason every host returns when a command's `parse` rejects its input.
297
- - `RunCommandResult` (type): type RunCommandResult = | { ok: true; snapshot: GameRuntimeSnapshot } | { ok: false; reason: string } — ⚠ undocumented
297
+ - `RunCommandResult` (type): type RunCommandResult = | { ok: true; snapshot: GameRuntimeSnapshot } | { ok: false; reason: string; message?: string } — ⚠ undocumented
298
298
  - `SERVER_ONLY_COMMAND_REASON` (const): const SERVER_ONLY_COMMAND_REASON: "Command is server-only" — Reason a public transport returns for a {@link CommandDef} marked `access: "server"`.
299
299
  - `defineCommand` (function): function defineCommand<TInput>(definition: CommandDef<TInput> & { parse: (input: unknown) => TInput | null }): CommandDef — Declare a command with a typed input: `parse` narrows the wire value once, and `scope`, `validate` and `apply` receive the parsed type. Returns the untyped {@link CommandDef} a runtime's command table holds.
300
300
 
@@ -321,7 +321,7 @@
321
321
  - `GameContextItemUse` (interface): interface GameContextItemUse { register(handlers: Record<string, ItemUseHandler<GameContext>>): void; registered(): string[]; can(input: ItemUseInput): ItemUseRejection | null; use(input: ItemUseInput): ItemUseResult<GameContext> } — ⚠ undocumented
322
322
  - `GameContextLoot` (interface): interface GameContextLoot { register(def: LootTableDef): void; has(id: string): boolean; roll(id: string, rng?: () => number): Drop[]; grantToPlayer(userId: string, drops: Drop[], source?: string): void } — ⚠ undocumented
323
323
  - `GameContextModels` (interface): interface GameContextModels — Per-kind render-model lookup for {@link GameContextOptions.models}; a resolved `ModelConfig` satisfies {@link ModelBodySource} structurally.
324
- - `GameContextObjectEntry` (interface): interface GameContextObjectEntry { proximityPrompt?: ProximityPrompt; breakable?: false | {baseBreakTime: number}; slotInventory?: InventoryLayout; verbs?: readonly ContextVerb[]; colliders?: EntityColliderSet; halfExtents?: readonly [number, number, number] } — ⚠ undocumented
324
+ - `GameContextObjectEntry` (interface): interface GameContextObjectEntry { proximityPrompt?: ProximityPrompt; breakable?: false | {baseBreakTime: number}; slotInventory?: InventoryLayout; verbs?: readonly ContextVerb[]; colliders?: EntityColliderSet; halfExtents?: readonly [number, number, number]; scenery?: boo… — ⚠ undocumented
325
325
  - `GameContextOptions` (interface): interface GameContextOptions<TAssetRef extends ModelAssetRef = ModelAssetRef, TMultiplayer = unknown> { definition: GameDefinition<TAssetRef, TMultiplayer>; content: GameContextContent; player: {userId: string; isNew: boolean}; territory?: Territory; now?: () => number; occluder?: (from: EntityPosition, to: EntityPosition) => boolean; seed?… — ⚠ undocumented
326
326
  - `GameContextRace` (interface): interface GameContextRace { state(id: string, config?: RaceStateConfig): RaceState } — ⚠ undocumented
327
327
  - `GameContextTurn` (interface): interface GameContextTurn { loop(id: string, config?: TurnLoopConfig): TurnLoop } — ⚠ undocumented
@@ -352,7 +352,7 @@
352
352
  - `GameContextItemUse` (interface): interface GameContextItemUse { register(handlers: Record<string, ItemUseHandler<GameContext>>): void; registered(): string[]; can(input: ItemUseInput): ItemUseRejection | null; use(input: ItemUseInput): ItemUseResult<GameContext> } — ⚠ undocumented
353
353
  - `GameContextLoot` (interface): interface GameContextLoot { register(def: LootTableDef): void; has(id: string): boolean; roll(id: string, rng?: () => number): Drop[]; grantToPlayer(userId: string, drops: Drop[], source?: string): void } — ⚠ undocumented
354
354
  - `GameContextModels` (interface): interface GameContextModels — Per-kind render-model lookup for {@link GameContextOptions.models}; a resolved `ModelConfig` satisfies {@link ModelBodySource} structurally.
355
- - `GameContextObjectEntry` (interface): interface GameContextObjectEntry { proximityPrompt?: ProximityPrompt; breakable?: false | {baseBreakTime: number}; slotInventory?: InventoryLayout; verbs?: readonly ContextVerb[]; colliders?: EntityColliderSet; halfExtents?: readonly [number, number, number] } — ⚠ undocumented
355
+ - `GameContextObjectEntry` (interface): interface GameContextObjectEntry { proximityPrompt?: ProximityPrompt; breakable?: false | {baseBreakTime: number}; slotInventory?: InventoryLayout; verbs?: readonly ContextVerb[]; colliders?: EntityColliderSet; halfExtents?: readonly [number, number, number]; scenery?: boo… — ⚠ undocumented
356
356
  - `GameContextOptions` (interface): interface GameContextOptions<TAssetRef extends ModelAssetRef = ModelAssetRef, TMultiplayer = unknown> { definition: GameDefinition<TAssetRef, TMultiplayer>; content: GameContextContent; player: {userId: string; isNew: boolean}; territory?: Territory; now?: () => number; occluder?: (from: EntityPosition, to: EntityPosition) => boolean; seed?… — ⚠ undocumented
357
357
  - `GameContextRace` (interface): interface GameContextRace { state(id: string, config?: RaceStateConfig): RaceState } — ⚠ undocumented
358
358
  - `GameContextTurn` (interface): interface GameContextTurn { loop(id: string, config?: TurnLoopConfig): TurnLoop } — ⚠ undocumented
@@ -593,7 +593,7 @@
593
593
  - `PresenceSync` (type): type PresenceSync = { subscribe: (serverId: string, onChange: (rows: PresencePoseRow[]) => void) => FeedUnsubscribe; syncPose: (serverId: string, pose: PlayerPose) => void; } — ⚠ undocumented
594
594
  - `ResumeTicket` (type): type ResumeTicket = { userId: string; serverId: string; token: string; } — A short-lived credential allowing a disconnected client to reclaim one server membership.
595
595
  - `RunCommandArgs` (type): type RunCommandArgs = { serverId: string; command: string; input: unknown; } — ⚠ undocumented
596
- - `TransportRunCommandResult` (type): type TransportRunCommandResult = | { ok: true } | { ok: false; reason: string } — ⚠ undocumented
596
+ - `TransportRunCommandResult` (type): type TransportRunCommandResult = | { ok: true } /** `message` is the command's player-facing refusal text, when its `validate` gave one. */ | { ok: false; reason: string; message?: string } — ⚠ undocumented
597
597
  - `WorldSyncFrame` (type): type WorldSyncFrame = | { kind: "baseline"; revision: number; snapshot: WorldSnapshot } | { kind: "diff"; revision: number; diff: WorldDiff } — A revision-stamped world baseline or compact diff sent to a server-authoritative client.
598
598
  - `createTransportSessionId` (function): function createTransportSessionId(): string — Fresh id for one client session's join/leave pair (see {@link GameRuntimeTransport.joinServer}).
599
599
 
@@ -661,8 +661,8 @@
661
661
 
662
662
  ## @jgengine/shell/gameKit
663
663
 
664
- - `Clock` (function): function Clock({ format, showDay, controls, style, className, }: { format?: "24h" | "12h"; showDay?: boolean; controls?: boolean; style?: CSSProperties; className?: string; }): JSX.Element — A time-of-day clock reading the sim calendar — `Day N · HH:MM`, 24h or 12h. `controls` adds pause + the game's speed multipliers as clickable pills (the "fast-forward" bar), off by default so a game opts into letting the player scrub time.
665
- - `Coins` (function): function Coins({ currencyId, icon, style, className, }: { currencyId: string; icon?: ReactNode; style?: CSSProperties; className?: string; }): JSX.Element — A currency counter — an icon (emoji/char, default a coin) plus the live amount for `currencyId`. *
664
+ - `Clock` (function): function Clock({ format = "24h", showDay = true, controls = false, style, className, }: { format?: "24h" | "12h"; showDay?: boolean; controls?: boolean; style?: CSSProperties; className?: string; }): React.JSX.Element — A time-of-day clock reading the sim calendar — `Day N · HH:MM`, 24h or 12h. `controls` adds pause + the game's speed multipliers as clickable pills (the "fast-forward" bar), off by default so a game opts into letting the player scrub time.
665
+ - `Coins` (function): function Coins({ currencyId, icon = "🪙", style, className, }: { currencyId: string; icon?: ReactNode; style?: CSSProperties; className?: string; }): React.JSX.Element — A currency counter — an icon (emoji/char, default a coin) plus the live amount for `currencyId`. *
666
666
  - `DEFAULT_WALK_CODES` (const): const DEFAULT_WALK_CODES: { readonly moveForward: readonly ["KeyW"]; readonly moveBack: readonly ["KeyS"]; readonly moveLeft: readonly ["KeyA"]; readonly moveRight: readonly ["KeyD"]; readonly jump: readonly ["Space"]; } — Default keyboard codes for the shell walk controller (WASD + Space). Use as the base of `defineGame({ input: { ...DEFAULT_WALK_CODES, interact: ["KeyE"] } })` instead of re-typing the same five bindings in every game.
667
667
  - `EditorCatalogDefinition` (interface): interface EditorCatalogDefinition — Game-exported catalog definition: a `ParamSchema` plus default entries. Schemas stay in code; entry values merge into `document.catalogs` and are what the editor/RPC edits and saves.
668
668
  - `EditorDocument` (interface): interface EditorDocument — The full authored scene: every marker, volume, path, note, and sculpted terrain for a game.
@@ -673,21 +673,21 @@
673
673
  - `GameHost` (function): function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultiplayer, editor, creator, creatorOpen = false, onCreatorOpenChange }: GameHostProps): React.JSX.Element — The one documented mount: resolves multiplayer for the playable and renders the shell. With the `editor` loader prop it also owns the whole editor summon (F2+E, `?mode=editor`, dev save endpoint).
674
674
  - `GameHostProps` (interface): interface GameHostProps — Props for {@link GameHost}: the playable, optional multiplayer overrides, and the optional editor loader that enables the engine-owned F2+E summon.
675
675
  - `GameLoop` (interface): interface GameLoop<TContext = unknown> — Lifecycle hooks a game implements to drive init, per-tick simulation, and player join/leave.
676
- - `Hotbar` (function): function Hotbar({ inventoryId, activeSlot, keys, slotSize, itemIcon, style, className, }: { inventoryId: string; activeSlot?: number; keys?: readonly string[]; slotSize?: number; /** Caller-supplied item id → icon registry; return null/undefined to fall back to the default glyph. */ itemIcon?: (item… — A numbered hotbar bound to an inventory — painted iconed slots (a `GameIcon` glyph over a school-keyed gradient with a count badge, #1035), an active-slot highlight, and a keycap per slot. Place it and pass the `inventoryId`; `activeSlot` highlights the equipped one. Supply `itemIcon` to map your item ids to your own glyphs — the default resolves a `GameIcon` from the item id.
676
+ - `Hotbar` (function): function Hotbar({ inventoryId, activeSlot, keys, slotSize = 46, itemIcon, style, className, }: { inventoryId: string; activeSlot?: number; keys?: readonly string[]; slotSize?: number; /** Caller-supplied item id → icon registry; return null/undefined to fall back to the default glyph. */ itemIcon?: … — A numbered hotbar bound to an inventory — painted iconed slots (a `GameIcon` glyph over a school-keyed gradient with a count badge, #1035), an active-slot highlight, and a keycap per slot. Place it and pass the `inventoryId`; `activeSlot` highlights the equipped one. Supply `itemIcon` to map your item ids to your own glyphs — the default resolves a `GameIcon` from the item id.
677
677
  - `HudCanvas` (function): function HudCanvas({ layout, editChord, compactScale, showDuring, className, style, children, }: { layout: HudLayoutStore; editChord?: HudEditChord | false; /** Zoom applied to the whole HUD on compact displays. Default 0.85. */ compactScale?: number; /** Opt-in play-phase gate: render the HUD only … — Full-viewport HUD surface. Panels declared with `HudPanel` flow into nine anchor regions and stack automatically with a gap — no per-panel pixel offsets, no manual clearance for sibling panels, the touch-control dock (`--jg-hud-dock-clearance`), or device safe areas. On compact displays the whole surface scales down and each panel applies its `compact` behavior.
678
- - `HudPanel` (function): function HudPanel({ id, anchor, order, compact: compactMode, chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region, width, height, type, className, style, children, }: { id: string; anchor?: HudAnchor; /** Stack position within the region, a… — A HUD block that lives in one of the nine anchor regions. Panels sharing a region stack outward from the screen edge in ascending `order`. On fine pointers panels stay draggable through the edit chord; a dragged panel leaves the flow and keeps its custom placement. On compact displays custom placements are ignored and the `compact` behavior applies.
679
- - `PlayableGame` (type): type PlayableGame = EnginePlayableGame< ComponentType, ComponentType, RenderEntity, RenderObject, ComponentType<ViewmodelProps>, ComponentType<WorldOverlayProps> > — The concrete React-bound playable a shell `defineGame` returns and `GameHost` mounts — the one PlayableGame games handle.
680
- - `StatBar` (function): function StatBar({ statId, entityId, tone, label, showValue, width, icon, style, className, }: { statId?: string; entityId?: string; tone?: StatTone; label?: string; showValue?: boolean; width?: number; icon?: ReactNode; style?: CSSProperties; className?: string; }): JSX.Element | null — A polished stat bar (health/mana/stamina/shield/xp) — a rounded, glassy meter with a tone-colored fill and an optional value readout. Reads `statId` off `entityId` (defaults to the local player). Renders nothing until the stat exists.
678
+ - `HudPanel` (function): function HudPanel({ id, anchor = "top-left", order, compact: compactMode = "keep", chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, width, height, type, className, style, children, }: { id: string; anchor?: HudAnchor; /** Stack … — A HUD block that lives in one of the nine anchor regions. Panels sharing a region stack outward from the screen edge in ascending `order`. On fine pointers panels stay draggable through the edit chord; a dragged panel leaves the flow and keeps its custom placement. On compact displays custom placements are ignored and the `compact` behavior applies.
679
+ - `PlayableGame` (type): type PlayableGame = EnginePlayableGame< ComponentType, ComponentType, RenderEntity, RenderObject, ComponentType<ViewmodelProps>, ComponentType<WorldOverlayProps> > & { /** HUD theme the shell applies at the `GameUI` root. Unset uses `defaultHudTheme`; `false` opts out of the default theme and the en… — The concrete React-bound playable a shell `defineGame` returns and `GameHost` mounts — the one PlayableGame games handle.
680
+ - `StatBar` (function): function StatBar({ statId = "health", entityId, tone = "health", label, showValue = true, width = 200, icon, style, className, }: { statId?: string; entityId?: string; tone?: StatTone; label?: string; showValue?: boolean; width?: number; icon?: ReactNode; style?: CSSProperties; className?: string; }… — A polished stat bar (health/mana/stamina/shield/xp) — a rounded, glassy meter with a tone-colored fill and an optional value readout. Reads `statId` off `entityId` (defaults to the local player). Renders nothing until the stat exists.
681
681
  - `StoreHandle` (interface): interface StoreHandle<T> — A typed, cast-free handle onto one slot of the reactive game store (`ctx.game.store`). The single type parameter is fixed at definition; `read`/`write`/`update` never widen to `unknown`, so no call site repeats `store.get(key) as T`. Writes flow through the engine store, so they bump `ctx.version()`, serialize into a {@link WorldSnapshot}, and replay under host authority — the same guarantees a hand-rolled `store.get`/`store.set` pair gives up the moment it forks run state into a module-level singleton.
682
682
  - `SystemDefinition` (interface): interface SystemDefinition — A reusable game capability — lifecycle, timing, events, and optional save / replication / reset / disposal. Pass instances via `defineGame({ systems })`. Prefer one system per meaningful capability (`combat`, `quests`), not per micro-tick.
683
- - `authoredSpawnPosition` (function): function authoredSpawnPosition(document: AuthoredSpawnDocumentLike, options?: AuthoredSpawnOptions): [number, number, number] | null — Position of the authored spawn marker as a spawn-ready `[x, y, z]` tuple, or null when the document has none. Reads the first `player_spawn` marker by default, so dragging the marker in the editor moves where players spawn — no coordinates copied into game code.
683
+ - `authoredSpawnPosition` (function): function authoredSpawnPosition(document: AuthoredSpawnDocumentLike, options: AuthoredSpawnOptions = {}): [number, number, number] | null — Position of the authored spawn marker as a spawn-ready `[x, y, z]` tuple, or null when the document has none. Reads the first `player_spawn` marker by default, so dragging the marker in the editor moves where players spawn — no coordinates copied into game code.
684
684
  - `collectAuthoredTriggers` (function): function collectAuthoredTriggers(document: SceneDocumentLike): AuthoredTrigger[] — Collect every authored trigger on a document's markers and volumes. Pure — no runtime state. Action params use the live {@link registerTriggerAction} registry when present.
685
685
  - `defineGame` (function): function defineGame<TAssetRef extends ModelAssetRef = ModelAssetRef>(config: GameConfig<TAssetRef>): PlayableGame — The one public authoring entry point: compose engine fields (systems, world, physics, input) and presentation fields (camera, HUD, audio, authored scene) into a `PlayableGame` ready for `GameHost`. Defaults to solo/offline multiplayer; `editorLayers` auto-mounts the authored scene document.
686
686
  - `defineKeyedStore` (function): function defineKeyedStore<T>(keyFor: (id: string) => string, initial: T | (() => T)): KeyedStoreHandle<T> — Define a typed per-owner keyed family on the game store: game code expresses `read(ctx, userId)`/`write(ctx, userId, v)`/`update(...)` with the value type it means, and both the `` `prefix:${id}` `` key composition and the `unknown → T` cast live once, here, behind the boundary. Reach for this over N separate `defineStore` calls whenever the owning id varies at runtime (per-user class, per-instance auras) — `defineStore` stays the right call for a single fixed slot.
687
687
  - `defineStore` (function): function defineStore<T>(key: string, initial: T | (() => T)): StoreHandle<T> — Define a typed slot on the game store: game code expresses `read(ctx)`/`write(ctx, v)`/`update(...)` with the value type it means, and the `unknown → T` cast lives once, here, behind the boundary. Pass a factory for `initial` when the fallback is a fresh mutable object; the factory runs at most once and its result is reused, so an unwritten slot keeps a stable identity across reads (no per-read churn for a React selector, no allocation on a hot path). A value that carries functions (a closure-based run store, an input handle) is runtime-only: snapshots, saves and replication skip it and hydration keeps it in place. Hold such objects with `perContext` when they need no store notifications.
688
688
  - `defineSystem` (function): function defineSystem(definition: SystemDefinition): SystemDefinition — Declare a composable game system. Pure data + hooks — the engine compiles the schedule and installs lifecycle when the game boots.
689
- - `environment` (function): function environment(config?: EnvironmentWorldConfig): EnvironmentWorldFeature — Composes an `environment()` feature from terrain, sky, weather, vegetation, water, structures, roads, and pads.
690
- - `environmentContentFromDocument` (function): function environmentContentFromDocument(doc: EditorDocument, options?: EnvironmentContentOptions): EnvironmentContent — Derives the coordinate/placement content of an `environment()` world from its scene document: terrain footprint (via {@link terrainBoundsFromDocument}), ground clearings under authored spawns/POIs (via `clearanceZonesFrom`), and the document's sculpt snapshot. This is the seam that lets a game author its world footprint and flatten regions in the editor instead of hardcoding them — the `world.ts` that consumes it carries only engine tuning.
689
+ - `environment` (function): function environment(config: EnvironmentWorldConfig = {}): EnvironmentWorldFeature — Composes an `environment()` feature from terrain, sky, weather, vegetation, water, structures, roads, and pads.
690
+ - `environmentContentFromDocument` (function): function environmentContentFromDocument(doc: EditorDocument, options: EnvironmentContentOptions = {}): EnvironmentContent — Derives the coordinate/placement content of an `environment()` world from its scene document: terrain footprint (via {@link terrainBoundsFromDocument}), ground clearings under authored spawns/POIs (via `clearanceZonesFrom`), and the document's sculpt snapshot. This is the seam that lets a game author its world footprint and flatten regions in the editor instead of hardcoding them — the `world.ts` that consumes it carries only engine tuning.
691
691
  - `grounded` (function): function grounded(ctx: Pick<GameContext, "world">, x: number, z: number): readonly [number, number, number] — Ground an XZ point onto the live world surface as `[x, groundY, z]`.
692
692
  - `offline` (function): function offline(): MultiplayerAdapterConfig — Explicit single-player adapter. Solo games never need this — omitting `multiplayer` in the shell `defineGame` already defaults to offline; pass it only where an adapter value is structurally required.
693
693
  - `seededRng` (function): function seededRng(seed: string | number): SeededRng — Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.