@jgengine/core 0.22.0 → 0.24.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 (45) hide show
  1. package/CHANGELOG.md +110 -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/game/events.d.ts +12 -0
  7. package/dist/game/notices.d.ts +85 -0
  8. package/dist/game/notices.js +105 -0
  9. package/dist/game/playableGame.d.ts +37 -0
  10. package/dist/meta/changelog.d.ts +1 -1
  11. package/dist/meta/changelog.js +104 -1
  12. package/dist/render/lightingBalance.d.ts +13 -0
  13. package/dist/render/lightingBalance.js +32 -0
  14. package/dist/render/lookPreset.d.ts +10 -1
  15. package/dist/render/lookPreset.js +28 -3
  16. package/dist/render/postProcessing.d.ts +6 -1
  17. package/dist/render/postProcessing.js +2 -0
  18. package/dist/runtime/commandDispatch.js +10 -3
  19. package/dist/runtime/commandRunner.d.ts +3 -0
  20. package/dist/runtime/commandRunner.js +3 -1
  21. package/dist/runtime/context/scene.d.ts +3 -0
  22. package/dist/runtime/context/scene.js +6 -0
  23. package/dist/runtime/gameContext.js +13 -0
  24. package/dist/runtime/gameContextTypes.d.ts +19 -0
  25. package/dist/runtime/transport.d.ts +4 -1
  26. package/dist/settings/graphicsProfile.d.ts +7 -0
  27. package/dist/settings/graphicsProfile.js +4 -0
  28. package/dist/settings/graphicsSettings.d.ts +9 -0
  29. package/dist/settings/graphicsSettings.js +29 -0
  30. package/dist/world/buildings.d.ts +7 -0
  31. package/dist/world/buildings.js +20 -0
  32. package/dist/world/environmentSummary.js +7 -3
  33. package/dist/world/features.d.ts +24 -5
  34. package/dist/world/features.js +2 -0
  35. package/dist/world/terraform.d.ts +2 -0
  36. package/dist/world/terraform.js +1 -0
  37. package/dist/world/terrain.d.ts +3 -2
  38. package/dist/world/terrain.js +16 -3
  39. package/package.json +1 -1
  40. package/skills/jgengine/api.md +18 -18
  41. package/skills/jgengine-gameplay/api.md +18 -4
  42. package/skills/jgengine-gameplay/capabilities.md +5 -0
  43. package/skills/jgengine-world/api.md +27 -14
  44. package/skills/jgengine-world/capabilities.md +4 -0
  45. package/skills/jgengine-world/reference.md +27 -8
@@ -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 }),
@@ -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.22.0",
3
+ "version": "0.24.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",
@@ -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.22.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
9
+ - `VERSION` (const): const VERSION: "0.24.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.22.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
235
+ - `VERSION` (const): const VERSION: "0.24.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.
@@ -502,6 +502,7 @@
502
502
  - `CombatTelegraphCancelledEvent` (interface): interface CombatTelegraphCancelledEvent { id: number } — ⚠ undocumented
503
503
  - `CombatTelegraphEvent` (interface): interface CombatTelegraphEvent { id: number; shape: TelegraphShape; position: [number, number, number]; dir?: number; windupMs: number; kind: string } — ⚠ undocumented
504
504
  - `CombatVfxEvent` (interface): interface CombatVfxEvent — A transient sprite-particle effect the shell renders once and expires — one burst of `kind`, tinted `color`, anchored at `from` (and `to` for travel/beam effects).
505
+ - `CommandRefusedEvent` (interface): interface CommandRefusedEvent — A command run through `ctx.game.commands` (or a bound host) was refused. `reason` is the machine code; `message` is the player-facing text the command's `validate` returned, when it gave one.
505
506
  - `CosmeticsChangedEvent` (interface): interface CosmeticsChangedEvent { userId: string; slots: Record<string, string> } — ⚠ undocumented
506
507
  - `DeathReason` (type): type DeathReason = | { kind: "player_kill"; killerUserId: string; via?: { item?: string } } | { kind: "environment"; source: string } | { kind: "self"; source: string } — Why an entity died — who or what gets credit, for drop/command rules and the `entity.died` event.
507
508
  - `EmotePlayedEvent` (interface): interface EmotePlayedEvent { from: string; emoteId: string; at: readonly [number, number, number]; recipients: readonly string[] } — ⚠ undocumented
@@ -663,6 +664,18 @@
663
664
  - `freezeNarrativeCanon` (function): function freezeNarrativeCanon(canon: NarrativeCanon): NarrativeCanon — Detach and freeze reviewed canon before passing it to an asynchronous authoring callback. Duplicate character/event ids and an empty revision throw; facts retain exact caller values.
664
665
  - `validateNarrativeBatch` (function): function validateNarrativeBatch(canon: NarrativeCanon, batch: NarrativeBatch, limits: NarrativeBatchLimits): NarrativeIssue[] — Check a typed candidate against caller budgets, canon revision, explicit claims and graph topology. Stops at the first exceeded budget before topology work; stale revisions also stop immediately. Claims check exact fact values and the declared strict event order, never prose, speaker labels, motivations, branch consequences or story quality. O(canon + bounded nodes + choices + claims). Validate/parse external JSON before calling; this is not an untrusted JSON decoder.
665
666
 
667
+ ## @jgengine/core/game/notices
668
+
669
+ - `Notice` (interface): interface Notice — A live notice as the HUD renders it.
670
+ - `NoticeFeed` (interface): interface NoticeFeed — A capped, self-expiring, deduping notice list every game reaches through `ctx.game.notices`.
671
+ - `NoticeFeedConfig` (interface): interface NoticeFeedConfig — Tunables for {@link createNoticeFeed}; all retunable at runtime.
672
+ - `NoticeFeedState` (interface): interface NoticeFeedState — Plain JSON state of a {@link NoticeFeed}.
673
+ - `NoticeInput` (interface): interface NoticeInput — What a caller pushes: plain text, or text with a tone, a dedupe key and a lifetime.
674
+ - `NoticeTone` (type): type NoticeTone = "info" | "success" | "warn" | "deny" — How a notice reads: neutral info, a win, a caution, or a refused action.
675
+ - `RefusalNoticeOptions` (interface): interface RefusalNoticeOptions — Options for {@link attachRefusalNotices}.
676
+ - `attachRefusalNotices` (function): function attachRefusalNotices(events: Pick<GameEvents, "on">, notices: Pick<NoticeFeed, "push">, options: RefusalNoticeOptions = {}): () => void — Show every player-facing command refusal (`validate` returned `{ reason, message }`) as a deny notice. Refusals without a `message` stay silent: `reason` is a machine string. Returns the detach function.
677
+ - `createNoticeFeed` (function): function createNoticeFeed(config: NoticeFeedConfig & { now?: () => number } = {}): NoticeFeed — Player-facing notice feed: info/success/warn/deny messages with stacking dedupe and TTL. Pure state with an injected clock, so it runs headless; the shell renders it and plays a UI cue per push.
678
+
666
679
  ## @jgengine/core/game/notifications
667
680
 
668
681
  - `NotificationCenterOptions` (interface): interface NotificationCenterOptions — Options for {@link createNotificationCenter}.
@@ -748,7 +761,7 @@
748
761
  - `ChaseCameraConfig` (interface): interface ChaseCameraConfig — Speed-reactive vehicle chase rig (#27) — speed→FOV, spring arm, procedural shake, interior views.
749
762
  - `ChaseView` (type): type ChaseView = "chase" | "cockpit" | "hood" | "rear" — Fixed interior view for the chase rig (#27).
750
763
  - `CinematicCameraConfig` (interface): interface CinematicCameraConfig — Scripted keyframe / path player (#29). When set it overrides the active rig.
751
- - `DirectionalLightingConfig` (interface): interface DirectionalLightingConfig { color?: string; intensity?: number; position: readonly [number, number, number]; castShadow?: boolean; shadowMapSize?: number; shadowCameraSize?: number; cascades?: number; shadowMaxFar?: number; shadowBias?: number; shadowNormalBias?: nu… — ⚠ undocumented
764
+ - `DirectionalLightingConfig` (interface): interface DirectionalLightingConfig { color?: string; intensity?: number; position: readonly [number, number, number]; castShadow?: boolean; shadowMapSize?: number; shadowCameraSize?: number; cascades?: number; shadowMaxFar?: number; shadowBias?: number; shadowNormalBias?: nu… — ⚠ undocumented · used by `FALLBACK_KEY_LIGHT` (@jgengine/core/render/lightingBalance): Soft key added when authored lighting has no directional light: high, off to one side, shadowless.
752
765
  - `EntitySpriteConfig` (interface): interface EntitySpriteConfig { url: string; width: number; height: number; y: number; clip?: {atlas: SpriteAtlas; animation: string} } — ⚠ undocumented
753
766
  - `FirstPersonCameraConfig` (interface): interface FirstPersonCameraConfig { eyeHeight?: number; sensitivity?: number; maxPitch?: number; reticle?: boolean; viewmodel?: boolean } — ⚠ undocumented
754
767
  - `FlightConfig` (interface): interface FlightConfig — Free-flight families moved into the walk controller so one seam covers ground + air — creative/spectator/noclip/hover are weightless, aircraft, rotorcraft and rockets use `physics/aircraftDynamics`.
@@ -763,7 +776,7 @@
763
776
  - `ModelAnimationConfig` (interface): interface ModelAnimationConfig — Rig playback for a `ModelConfig`'s GLTF animation clips — looping idles, one-shots, and held poses.
764
777
  - `ModelAnimationStates` (interface): interface ModelAnimationStates — Movement-state clip set for `ModelAnimationConfig.states`: the shell reads the entity's live speed each frame and crossfades between these clips, so a walking mob animates without any game-side driver.
765
778
  - `ModelAttachment` (interface): interface ModelAttachment — Parents a prop/weapon model to a named bone or node on the host model's rig — a sword on `handslot.r`, a spellbook offhand — following the bone's animated transform each frame.
766
- - `ModelConfig` (interface): interface ModelConfig { url: string; scale?: number; targetHeight?: number; y?: number; anchor?: "center" | "origin"; dims?: ModelDims; collisionMesh?: CollisionMeshData; material?: ModelMaterialOverride; materialAssets?: readonly MaterialAsset[]; materialAssign… — ⚠ undocumented · used by `PartMotionRig` (@jgengine/shell/render/PartMotion): Procedural motion rig for a rig-less part-composed character (`ModelPart.role` — see `@jgengine/core/game/partAnimation`).
779
+ - `ModelConfig` (interface): interface ModelConfig { url: string; scale?: number; targetHeight?: number; y?: number; anchor?: "center" | "origin"; dims?: ModelDims; collisionMesh?: CollisionMeshData; material?: ModelMaterialOverride; surfaceDetail?: boolean; materialAssets?: readonly Materi… — ⚠ undocumented · used by `PartMotionRig` (@jgengine/shell/render/PartMotion): Procedural motion rig for a rig-less part-composed character (`ModelPart.role` — see `@jgengine/core/game/partAnimation`).
767
780
  - `ModelIkConfig` (interface): interface ModelIkConfig — Bone names and limits for {@link ModelConfig.ik}.
768
781
  - `ModelMaterialMaps` (interface): interface ModelMaterialMaps extends Partial<Record<MaterialTextureRole, string>> — Real PBR map URLs (e.g. `buildMaterialCatalog(...).resolve(id)!.maps` from `@jgengine/assets`) layered onto a model's material — the seam for texturing an otherwise-flat/untextured GLB. Any supported MaterialTextureRole may be omitted to keep the model's own map. Physical roles use the renderer's physical material; map URLs and response parameters remain caller-owned.
769
782
  - `ModelMaterialOverride` (interface): interface ModelMaterialOverride extends MaterialSurfaceParameters — Per-entity PBR material override (#151.3) applied to every standard or physical material in the model's cloned scene graph.
@@ -771,6 +784,7 @@
771
784
  - `ModelRimLight` (interface): interface ModelRimLight — Fresnel rim-light term added on top of a model's lighting — see {@link ModelMaterialOverride.rim}.
772
785
  - `MovementCommitFrame` (interface): interface MovementCommitFrame — One frame's movement resolution handed to `PlayerMovementConfig.beforeCommit`.
773
786
  - `MovementFeelConfig` (interface): interface MovementFeelConfig — Feel knobs for the built-in walk controller; see {@link PlayerMovementConfig.feel}.
787
+ - `NoticePresentationConfig` (interface): interface NoticePresentationConfig extends NoticeFeedConfig — How the shell presents `ctx.game.notices`; every field is optional.
774
788
  - `ObjectStyle` (interface): interface ObjectStyle { color?: string; opacity?: number; hidden?: boolean } — ⚠ undocumented
775
789
  - `ObserverCameraConfig` (interface): interface ObserverCameraConfig — Detached spectator/photo cam (#120) — binds to any entity or fixed point, never reads player input.
776
790
  - `PlayableGame` (interface): interface PlayableGame<TUi = unknown, TWorldOverlay = unknown, TRenderEntity = never, TRenderObject = never, TViewmodel = unknown, TOverlay = TWorldOverlay> { game: GameDefinition; content: GameContextContent; loop: Required<Omit<GameLoop<GameContext>, "onPlayerLeave">> & Pick<GameLoop<GameContext>, "onPlayerLeave">; GameUI: TUi; presentation?: "3d" | "hud"; WorldOverlay?: TOverlay; environment… — ⚠ undocumented · used by `defineGame` (@jgengine/shell/defineGame): The one public authoring entry point: compose engine fields (systems, world, physics, input) and presentation fields (camera, HUD, audio, au…
@@ -1173,7 +1187,7 @@
1173
1187
  - `DialogueGraphView` (interface): interface DialogueGraphView — The render-ready snapshot of a conversation at one node — everything a view needs to draw speaker, line, and choice buttons, with no traversal logic in the component.
1174
1188
  - `DialogueRun` (interface): interface DialogueRun — An observable walk through a {@link DialogueGraph}: current view, choose to advance, serialize.
1175
1189
  - `DialogueRunOptions` (interface): interface DialogueRunOptions — Options for {@link createDialogueRun}.
1176
- - `DirectionalLightingConfig` (interface): interface DirectionalLightingConfig { color?: string; intensity?: number; position: readonly [number, number, number]; castShadow?: boolean; shadowMapSize?: number; shadowCameraSize?: number; cascades?: number; shadowMaxFar?: number; shadowBias?: number; shadowNormalBias?: nu… — ⚠ undocumented
1190
+ - `DirectionalLightingConfig` (interface): interface DirectionalLightingConfig { color?: string; intensity?: number; position: readonly [number, number, number]; castShadow?: boolean; shadowMapSize?: number; shadowCameraSize?: number; cascades?: number; shadowMaxFar?: number; shadowBias?: number; shadowNormalBias?: nu… — ⚠ undocumented · used by `FALLBACK_KEY_LIGHT` (@jgengine/core/render/lightingBalance): Soft key added when authored lighting has no directional light: high, off to one side, shadowless.
1177
1191
  - `Drop` (interface): interface Drop — A resolved loot outcome — one item or currency grant with its rolled count.
1178
1192
  - `DurabilitySpec` (interface): interface DurabilitySpec { max: number; wearPerUse?: number; wearPerHit?: number; disableAtZero?: boolean; repair?: RepairSpec } — ⚠ undocumented
1179
1193
  - `DurabilityState` (interface): interface DurabilityState { current: number; max: number } — ⚠ undocumented · used by `applyWear`: Apply wear to an item, tracking breakage and repair eligibility.
@@ -1275,7 +1289,7 @@
1275
1289
  - `LootStageStatus` (type): type LootStageStatus = "rolled" | "skipped" | "empty" | "replaced" | "fell-through" — The disposition of a stage after resolution — did it roll, get gated, fall through, override, or find nothing.
1276
1290
  - `LootStageTrace` (interface): interface LootStageTrace — Per-stage record of what happened during resolution, for debugging and replay auditing.
1277
1291
  - `LootTableDef` (interface): interface LootTableDef — A named, validated loot table — its roll count, weighted-vs-independent mode, and candidate entries.
1278
- - `ModelConfig` (interface): interface ModelConfig { url: string; scale?: number; targetHeight?: number; y?: number; anchor?: "center" | "origin"; dims?: ModelDims; collisionMesh?: CollisionMeshData; material?: ModelMaterialOverride; materialAssets?: readonly MaterialAsset[]; materialAssign… — ⚠ undocumented · used by `PartMotionRig` (@jgengine/shell/render/PartMotion): Procedural motion rig for a rig-less part-composed character (`ModelPart.role` — see `@jgengine/core/game/partAnimation`).
1292
+ - `ModelConfig` (interface): interface ModelConfig { url: string; scale?: number; targetHeight?: number; y?: number; anchor?: "center" | "origin"; dims?: ModelDims; collisionMesh?: CollisionMeshData; material?: ModelMaterialOverride; surfaceDetail?: boolean; materialAssets?: readonly Materi… — ⚠ undocumented · used by `PartMotionRig` (@jgengine/shell/render/PartMotion): Procedural motion rig for a rig-less part-composed character (`ModelPart.role` — see `@jgengine/core/game/partAnimation`).
1279
1293
  - `ModelMaterialMaps` (interface): interface ModelMaterialMaps extends Partial<Record<MaterialTextureRole, string>> — Real PBR map URLs (e.g. `buildMaterialCatalog(...).resolve(id)!.maps` from `@jgengine/assets`) layered onto a model's material — the seam for texturing an otherwise-flat/untextured GLB. Any supported MaterialTextureRole may be omitted to keep the model's own map. Physical roles use the renderer's physical material; map URLs and response parameters remain caller-owned.
1280
1294
  - `ModelMaterialOverride` (interface): interface ModelMaterialOverride extends MaterialSurfaceParameters — Per-entity PBR material override (#151.3) applied to every standard or physical material in the model's cloned scene graph.
1281
1295
  - `ModularItemDef` (interface): interface ModularItemDef { id: string; baseStats: Record<string, number>; slots: readonly MountSlotDef[] } — ⚠ undocumented
@@ -332,6 +332,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
332
332
  ## notice-feed — serializable tone-tagged notice/event feed bounded by count and age (toasts, life events, killfeed)
333
333
 
334
334
  - `appendFeed` (function) · `import { appendFeed } from "@jgengine/core/gameplay"`
335
+ - `createNoticeFeed` (function) · `import { createNoticeFeed } from "@jgengine/core/game/notices"`
335
336
  - `pruneFeed` (function) · `import { pruneFeed } from "@jgengine/core/gameplay"`
336
337
 
337
338
  ## notification-center — persistent, read-tracked notification log (newest-first, capped, serializable) behind an unread badge — the durable counterpart to transient toasts
@@ -441,6 +442,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
441
442
 
442
443
  - `validateRecipeCatalog` (function) · `import { validateRecipeCatalog } from "@jgengine/core/crafting/recipeCatalog"`
443
444
 
445
+ ## refusal-feedback — surface a refused command's player-facing message in the notice feed automatically
446
+
447
+ - `attachRefusalNotices` (function) · `import { attachRefusalNotices } from "@jgengine/core/game/notices"`
448
+
444
449
  ## resolve — the current tier/band label for a keyed or scalar value
445
450
 
446
451
  - `tierAt` (function) · `import { tierAt } from "@jgengine/core/gameplay"`