@jgengine/core 0.4.0 → 0.6.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 (51) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/game/defineGame.d.ts +3 -0
  3. package/dist/game/feed.d.ts +2 -0
  4. package/dist/game/feed.js +10 -8
  5. package/dist/game/playableGame.d.ts +10 -1
  6. package/dist/game/progression.d.ts +61 -0
  7. package/dist/game/progression.js +96 -0
  8. package/dist/game/quest.d.ts +39 -0
  9. package/dist/game/quest.js +195 -0
  10. package/dist/game/trade.d.ts +23 -0
  11. package/dist/game/trade.js +50 -8
  12. package/dist/game/unlocks.d.ts +9 -0
  13. package/dist/game/unlocks.js +19 -1
  14. package/dist/inventory/slotModel.d.ts +19 -0
  15. package/dist/inventory/slotModel.js +76 -0
  16. package/dist/meta/changelog.d.ts +8 -0
  17. package/dist/meta/changelog.js +43 -0
  18. package/dist/physics/physicsWorld.d.ts +136 -0
  19. package/dist/physics/physicsWorld.js +489 -0
  20. package/dist/runtime/gameContext.d.ts +2 -0
  21. package/dist/runtime/gameContext.js +3 -0
  22. package/dist/scene/assetCatalog.d.ts +13 -0
  23. package/dist/scene/entityStore.d.ts +6 -0
  24. package/dist/scene/entityStore.js +15 -1
  25. package/dist/time/simClock.d.ts +62 -0
  26. package/dist/time/simClock.js +132 -0
  27. package/dist/world/buildingIndex.d.ts +16 -0
  28. package/dist/world/buildingIndex.js +63 -0
  29. package/dist/world/buildings.d.ts +98 -0
  30. package/dist/world/buildings.js +320 -0
  31. package/dist/world/features.d.ts +105 -1
  32. package/dist/world/features.js +95 -0
  33. package/dist/world/geometry.d.ts +23 -0
  34. package/dist/world/geometry.js +75 -0
  35. package/dist/world/interiors.d.ts +35 -0
  36. package/dist/world/interiors.js +66 -0
  37. package/dist/world/placement.d.ts +30 -0
  38. package/dist/world/placement.js +20 -0
  39. package/dist/world/regions.d.ts +66 -0
  40. package/dist/world/regions.js +158 -0
  41. package/dist/world/scatter.d.ts +24 -0
  42. package/dist/world/scatter.js +126 -0
  43. package/dist/world/scatterItems.d.ts +38 -0
  44. package/dist/world/scatterItems.js +85 -0
  45. package/dist/world/terrain.d.ts +54 -0
  46. package/dist/world/terrain.js +181 -0
  47. package/dist/world/water.d.ts +27 -0
  48. package/dist/world/water.js +125 -0
  49. package/dist/world/wind.d.ts +17 -0
  50. package/dist/world/wind.js +40 -0
  51. package/package.json +5 -2
@@ -2,6 +2,103 @@ export interface WorldBounds {
2
2
  w: number;
3
3
  d: number;
4
4
  }
5
+ export type EnvironmentVec2 = readonly [number, number];
6
+ export interface EnvironmentArea extends WorldBounds {
7
+ h?: number;
8
+ }
9
+ export interface TerrainEnvironmentConfig {
10
+ bounds?: WorldBounds;
11
+ height?: number;
12
+ heightMap?: string;
13
+ material?: string;
14
+ seed?: string;
15
+ frequency?: number;
16
+ octaves?: number;
17
+ ridged?: boolean;
18
+ baseHeight?: number;
19
+ waterLevel?: number;
20
+ }
21
+ export interface RainEnvironmentConfig {
22
+ area?: EnvironmentArea;
23
+ density?: number;
24
+ speed?: number;
25
+ dropLength?: number;
26
+ wind?: EnvironmentVec2;
27
+ color?: string;
28
+ }
29
+ export interface SnowEnvironmentConfig {
30
+ area?: EnvironmentArea;
31
+ density?: number;
32
+ speed?: number;
33
+ flakeSize?: number;
34
+ drift?: number;
35
+ wind?: EnvironmentVec2;
36
+ color?: string;
37
+ }
38
+ export interface GrassEnvironmentConfig {
39
+ area?: WorldBounds;
40
+ density?: number;
41
+ bladeHeight?: readonly [number, number];
42
+ bladeWidth?: number;
43
+ windStrength?: number;
44
+ colors?: readonly string[];
45
+ seed?: string;
46
+ }
47
+ export interface OceanEnvironmentConfig {
48
+ bounds?: WorldBounds;
49
+ level?: number;
50
+ waveHeight?: number;
51
+ waveScale?: number;
52
+ waveSpeed?: number;
53
+ color?: string;
54
+ }
55
+ export interface BuildingEnvironmentConfig {
56
+ count?: number;
57
+ footprint?: WorldBounds;
58
+ stories?: readonly [number, number];
59
+ storyHeight?: number;
60
+ spacing?: number;
61
+ style?: string;
62
+ seed?: string;
63
+ }
64
+ export type TerrainEnvironmentDescriptor = {
65
+ kind: "terrain";
66
+ } & Required<Pick<TerrainEnvironmentConfig, "bounds" | "height">> & Omit<TerrainEnvironmentConfig, "bounds" | "height">;
67
+ export type RainEnvironmentDescriptor = {
68
+ kind: "rain";
69
+ } & Required<Pick<RainEnvironmentConfig, "area" | "density" | "speed" | "dropLength" | "wind" | "color">>;
70
+ export type SnowEnvironmentDescriptor = {
71
+ kind: "snow";
72
+ } & Required<Pick<SnowEnvironmentConfig, "area" | "density" | "speed" | "flakeSize" | "drift" | "wind" | "color">>;
73
+ export type GrassEnvironmentDescriptor = {
74
+ kind: "grass";
75
+ } & Required<Pick<GrassEnvironmentConfig, "area" | "density" | "bladeHeight" | "bladeWidth" | "windStrength" | "colors">> & Pick<GrassEnvironmentConfig, "seed">;
76
+ export type OceanEnvironmentDescriptor = {
77
+ kind: "ocean";
78
+ } & Required<Pick<OceanEnvironmentConfig, "bounds" | "level" | "waveHeight" | "waveScale" | "waveSpeed" | "color">>;
79
+ export type BuildingEnvironmentDescriptor = {
80
+ kind: "building";
81
+ } & Required<Pick<BuildingEnvironmentConfig, "count" | "footprint" | "stories" | "storyHeight" | "spacing" | "style">> & Pick<BuildingEnvironmentConfig, "seed">;
82
+ export type WeatherEnvironmentDescriptor = RainEnvironmentDescriptor | SnowEnvironmentDescriptor;
83
+ export type VegetationEnvironmentDescriptor = GrassEnvironmentDescriptor;
84
+ export type WaterEnvironmentDescriptor = OceanEnvironmentDescriptor;
85
+ export type StructureEnvironmentDescriptor = BuildingEnvironmentDescriptor;
86
+ export type EnvironmentDescriptorList<T> = T | readonly T[];
87
+ export interface EnvironmentWorldConfig {
88
+ terrain?: TerrainEnvironmentDescriptor;
89
+ weather?: EnvironmentDescriptorList<WeatherEnvironmentDescriptor>;
90
+ vegetation?: EnvironmentDescriptorList<VegetationEnvironmentDescriptor>;
91
+ water?: EnvironmentDescriptorList<WaterEnvironmentDescriptor>;
92
+ structures?: EnvironmentDescriptorList<StructureEnvironmentDescriptor>;
93
+ }
94
+ export interface EnvironmentWorldFeature {
95
+ kind: "environment";
96
+ terrain?: TerrainEnvironmentDescriptor;
97
+ weather?: readonly WeatherEnvironmentDescriptor[];
98
+ vegetation?: readonly VegetationEnvironmentDescriptor[];
99
+ water?: readonly WaterEnvironmentDescriptor[];
100
+ structures?: readonly StructureEnvironmentDescriptor[];
101
+ }
5
102
  export interface BiomesWorldConfig {
6
103
  map: string;
7
104
  zones: string;
@@ -29,9 +126,16 @@ export type WorldFeature = ({
29
126
  kind: "plots";
30
127
  } & PlotsWorldConfig) | ({
31
128
  kind: "tilemap";
32
- } & TilemapWorldConfig) | {
129
+ } & TilemapWorldConfig) | EnvironmentWorldFeature | {
33
130
  kind: "flat";
34
131
  };
132
+ export declare function environment(config?: EnvironmentWorldConfig): EnvironmentWorldFeature;
133
+ export declare function terrain(config?: TerrainEnvironmentConfig): TerrainEnvironmentDescriptor;
134
+ export declare function rain(config?: RainEnvironmentConfig): RainEnvironmentDescriptor;
135
+ export declare function snow(config?: SnowEnvironmentConfig): SnowEnvironmentDescriptor;
136
+ export declare function grass(config?: GrassEnvironmentConfig): GrassEnvironmentDescriptor;
137
+ export declare function ocean(config?: OceanEnvironmentConfig): OceanEnvironmentDescriptor;
138
+ export declare function building(config?: BuildingEnvironmentConfig): BuildingEnvironmentDescriptor;
35
139
  export declare function biomes(config: BiomesWorldConfig): WorldFeature;
36
140
  export declare function voxel(config: VoxelWorldConfig): WorldFeature;
37
141
  export declare function plots(config?: PlotsWorldConfig): WorldFeature;
@@ -1,3 +1,98 @@
1
+ function list(value) {
2
+ if (value === undefined) {
3
+ return undefined;
4
+ }
5
+ return Array.isArray(value) ? value : [value];
6
+ }
7
+ function withOptional(base, optional) {
8
+ return optional === undefined ? base : { ...base, ...optional };
9
+ }
10
+ export function environment(config = {}) {
11
+ const weather = list(config.weather);
12
+ const vegetation = list(config.vegetation);
13
+ const water = list(config.water);
14
+ const structures = list(config.structures);
15
+ return {
16
+ kind: "environment",
17
+ ...(config.terrain === undefined ? {} : { terrain: config.terrain }),
18
+ ...(weather === undefined ? {} : { weather }),
19
+ ...(vegetation === undefined ? {} : { vegetation }),
20
+ ...(water === undefined ? {} : { water }),
21
+ ...(structures === undefined ? {} : { structures }),
22
+ };
23
+ }
24
+ export function terrain(config = {}) {
25
+ return withOptional({
26
+ kind: "terrain",
27
+ bounds: config.bounds ?? { w: 512, d: 512 },
28
+ height: config.height ?? 0,
29
+ }, {
30
+ ...(config.heightMap === undefined ? {} : { heightMap: config.heightMap }),
31
+ ...(config.material === undefined ? {} : { material: config.material }),
32
+ ...(config.seed === undefined ? {} : { seed: config.seed }),
33
+ ...(config.frequency === undefined ? {} : { frequency: config.frequency }),
34
+ ...(config.octaves === undefined ? {} : { octaves: config.octaves }),
35
+ ...(config.ridged === undefined ? {} : { ridged: config.ridged }),
36
+ ...(config.baseHeight === undefined ? {} : { baseHeight: config.baseHeight }),
37
+ ...(config.waterLevel === undefined ? {} : { waterLevel: config.waterLevel }),
38
+ });
39
+ }
40
+ export function rain(config = {}) {
41
+ return {
42
+ kind: "rain",
43
+ area: config.area ?? { w: 256, d: 256, h: 80 },
44
+ density: config.density ?? 0.65,
45
+ speed: config.speed ?? 18,
46
+ dropLength: config.dropLength ?? 0.8,
47
+ wind: config.wind ?? [0, 0],
48
+ color: config.color ?? "#9ec8ff",
49
+ };
50
+ }
51
+ export function snow(config = {}) {
52
+ return {
53
+ kind: "snow",
54
+ area: config.area ?? { w: 256, d: 256, h: 80 },
55
+ density: config.density ?? 0.35,
56
+ speed: config.speed ?? 2.4,
57
+ flakeSize: config.flakeSize ?? 0.08,
58
+ drift: config.drift ?? 0.4,
59
+ wind: config.wind ?? [0, 0],
60
+ color: config.color ?? "#ffffff",
61
+ };
62
+ }
63
+ export function grass(config = {}) {
64
+ return withOptional({
65
+ kind: "grass",
66
+ area: config.area ?? { w: 128, d: 128 },
67
+ density: config.density ?? 4,
68
+ bladeHeight: config.bladeHeight ?? [0.25, 0.9],
69
+ bladeWidth: config.bladeWidth ?? 0.035,
70
+ windStrength: config.windStrength ?? 0.35,
71
+ colors: config.colors ?? ["#3f7d2d", "#66a83f"],
72
+ }, config.seed === undefined ? undefined : { seed: config.seed });
73
+ }
74
+ export function ocean(config = {}) {
75
+ return {
76
+ kind: "ocean",
77
+ bounds: config.bounds ?? { w: 1024, d: 1024 },
78
+ level: config.level ?? 0,
79
+ waveHeight: config.waveHeight ?? 1.2,
80
+ waveScale: config.waveScale ?? 18,
81
+ waveSpeed: config.waveSpeed ?? 0.55,
82
+ color: config.color ?? "#1d7fa3",
83
+ };
84
+ }
85
+ export function building(config = {}) {
86
+ return withOptional({
87
+ kind: "building",
88
+ count: config.count ?? 1,
89
+ footprint: config.footprint ?? { w: 8, d: 8 },
90
+ stories: config.stories ?? [1, 4],
91
+ storyHeight: config.storyHeight ?? 3,
92
+ spacing: config.spacing ?? 2,
93
+ style: config.style ?? "generic",
94
+ }, config.seed === undefined ? undefined : { seed: config.seed });
95
+ }
1
96
  export function biomes(config) {
2
97
  return { kind: "biomes", ...config };
3
98
  }
@@ -0,0 +1,23 @@
1
+ export type Vec2 = readonly [number, number];
2
+ export interface Footprint {
3
+ w: number;
4
+ d: number;
5
+ }
6
+ export interface Aabb {
7
+ minX: number;
8
+ minZ: number;
9
+ maxX: number;
10
+ maxZ: number;
11
+ }
12
+ export interface MoveOptions {
13
+ bounds?: Aabb;
14
+ radius?: number;
15
+ }
16
+ export declare function snapToGrid(point: Vec2, size: number): Vec2;
17
+ export declare function footprintAabb(center: Vec2, footprint: Footprint, quarterTurns?: number): Aabb;
18
+ export declare function expandAabb(aabb: Aabb, margin: number): Aabb;
19
+ export declare function aabbOverlap(a: Aabb, b: Aabb): boolean;
20
+ export declare function aabbContains(outer: Aabb, inner: Aabb): boolean;
21
+ export declare function pointInAabb(point: Vec2, aabb: Aabb): boolean;
22
+ export declare function clampToAabb(point: Vec2, aabb: Aabb): Vec2;
23
+ export declare function resolveMove(from: Vec2, delta: Vec2, blockers: readonly Aabb[], options?: MoveOptions): Vec2;
@@ -0,0 +1,75 @@
1
+ export function snapToGrid(point, size) {
2
+ if (!(size > 0))
3
+ return point;
4
+ return [Math.round(point[0] / size) * size, Math.round(point[1] / size) * size];
5
+ }
6
+ export function footprintAabb(center, footprint, quarterTurns = 0) {
7
+ const turned = ((quarterTurns % 2) + 2) % 2 === 1;
8
+ const halfW = (turned ? footprint.d : footprint.w) / 2;
9
+ const halfD = (turned ? footprint.w : footprint.d) / 2;
10
+ return {
11
+ minX: center[0] - halfW,
12
+ maxX: center[0] + halfW,
13
+ minZ: center[1] - halfD,
14
+ maxZ: center[1] + halfD,
15
+ };
16
+ }
17
+ export function expandAabb(aabb, margin) {
18
+ return {
19
+ minX: aabb.minX - margin,
20
+ maxX: aabb.maxX + margin,
21
+ minZ: aabb.minZ - margin,
22
+ maxZ: aabb.maxZ + margin,
23
+ };
24
+ }
25
+ export function aabbOverlap(a, b) {
26
+ return a.minX < b.maxX && a.maxX > b.minX && a.minZ < b.maxZ && a.maxZ > b.minZ;
27
+ }
28
+ export function aabbContains(outer, inner) {
29
+ return (inner.minX >= outer.minX &&
30
+ inner.maxX <= outer.maxX &&
31
+ inner.minZ >= outer.minZ &&
32
+ inner.maxZ <= outer.maxZ);
33
+ }
34
+ export function pointInAabb(point, aabb) {
35
+ return point[0] >= aabb.minX && point[0] <= aabb.maxX && point[1] >= aabb.minZ && point[1] <= aabb.maxZ;
36
+ }
37
+ export function clampToAabb(point, aabb) {
38
+ return [
39
+ Math.min(Math.max(point[0], aabb.minX), aabb.maxX),
40
+ Math.min(Math.max(point[1], aabb.minZ), aabb.maxZ),
41
+ ];
42
+ }
43
+ function resolveAxis(main, cross, delta, blockers, axisX) {
44
+ let next = main + delta;
45
+ if (delta === 0)
46
+ return next;
47
+ for (const blocker of blockers) {
48
+ const minMain = axisX ? blocker.minX : blocker.minZ;
49
+ const maxMain = axisX ? blocker.maxX : blocker.maxZ;
50
+ const minCross = axisX ? blocker.minZ : blocker.minX;
51
+ const maxCross = axisX ? blocker.maxZ : blocker.maxX;
52
+ if (cross <= minCross || cross >= maxCross)
53
+ continue;
54
+ if (delta > 0) {
55
+ if (main <= minMain && next > minMain)
56
+ next = minMain;
57
+ }
58
+ else {
59
+ if (main >= maxMain && next < maxMain)
60
+ next = maxMain;
61
+ }
62
+ }
63
+ return next;
64
+ }
65
+ export function resolveMove(from, delta, blockers, options = {}) {
66
+ const radius = options.radius ?? 0;
67
+ const expanded = radius === 0 ? blockers : blockers.map((blocker) => expandAabb(blocker, radius));
68
+ let x = resolveAxis(from[0], from[1], delta[0], expanded, true);
69
+ if (options.bounds)
70
+ x = Math.min(Math.max(x, options.bounds.minX + radius), options.bounds.maxX - radius);
71
+ let z = resolveAxis(from[1], x, delta[1], expanded, false);
72
+ if (options.bounds)
73
+ z = Math.min(Math.max(z, options.bounds.minZ + radius), options.bounds.maxZ - radius);
74
+ return [x, z];
75
+ }
@@ -0,0 +1,35 @@
1
+ import { type Aabb, type Vec2 } from "./geometry.js";
2
+ export type SpaceRef = {
3
+ kind: "exterior";
4
+ } | {
5
+ kind: "interior";
6
+ id: string;
7
+ };
8
+ export interface EntityLocation {
9
+ space: SpaceRef;
10
+ position: Vec2;
11
+ }
12
+ export interface Interior {
13
+ id: string;
14
+ origin: Vec2;
15
+ rotation?: number;
16
+ bounds: Aabb;
17
+ obstacles?: readonly Aabb[];
18
+ }
19
+ export interface Exterior {
20
+ bounds?: Aabb;
21
+ obstacles?: readonly Aabb[];
22
+ }
23
+ export interface InteriorsConfig {
24
+ exterior?: Exterior;
25
+ interiors?: readonly Interior[];
26
+ radius?: number;
27
+ }
28
+ export interface Interiors {
29
+ move(location: EntityLocation, delta: Vec2): EntityLocation;
30
+ enter(location: EntityLocation, id: string): EntityLocation | null;
31
+ leave(location: EntityLocation): EntityLocation | null;
32
+ toInterior(id: string, exterior: Vec2): Vec2 | null;
33
+ toExterior(id: string, interior: Vec2): Vec2 | null;
34
+ }
35
+ export declare function createInteriors(config?: InteriorsConfig): Interiors;
@@ -0,0 +1,66 @@
1
+ import { resolveMove } from "./geometry.js";
2
+ function rotate(point, angle) {
3
+ if (angle === 0)
4
+ return point;
5
+ const cos = Math.cos(angle);
6
+ const sin = Math.sin(angle);
7
+ return [point[0] * cos - point[1] * sin, point[0] * sin + point[1] * cos];
8
+ }
9
+ function toInteriorFrame(interior, exterior) {
10
+ const local = [exterior[0] - interior.origin[0], exterior[1] - interior.origin[1]];
11
+ return rotate(local, -(interior.rotation ?? 0));
12
+ }
13
+ function toExteriorFrame(interior, local) {
14
+ const rotated = rotate(local, interior.rotation ?? 0);
15
+ return [rotated[0] + interior.origin[0], rotated[1] + interior.origin[1]];
16
+ }
17
+ export function createInteriors(config = {}) {
18
+ const radius = config.radius ?? 0;
19
+ const exterior = config.exterior ?? {};
20
+ const byId = new Map();
21
+ for (const interior of config.interiors ?? [])
22
+ byId.set(interior.id, interior);
23
+ return {
24
+ move(location, delta) {
25
+ if (location.space.kind === "exterior") {
26
+ const position = resolveMove(location.position, delta, exterior.obstacles ?? [], {
27
+ bounds: exterior.bounds,
28
+ radius,
29
+ });
30
+ return { space: { kind: "exterior" }, position };
31
+ }
32
+ const interior = byId.get(location.space.id);
33
+ if (interior === undefined)
34
+ return location;
35
+ const position = resolveMove(location.position, delta, interior.obstacles ?? [], {
36
+ bounds: interior.bounds,
37
+ radius,
38
+ });
39
+ return { space: { kind: "interior", id: interior.id }, position };
40
+ },
41
+ enter(location, id) {
42
+ if (location.space.kind !== "exterior")
43
+ return null;
44
+ const interior = byId.get(id);
45
+ if (interior === undefined)
46
+ return null;
47
+ return { space: { kind: "interior", id }, position: toInteriorFrame(interior, location.position) };
48
+ },
49
+ leave(location) {
50
+ if (location.space.kind !== "interior")
51
+ return null;
52
+ const interior = byId.get(location.space.id);
53
+ if (interior === undefined)
54
+ return null;
55
+ return { space: { kind: "exterior" }, position: toExteriorFrame(interior, location.position) };
56
+ },
57
+ toInterior(id, exterior) {
58
+ const interior = byId.get(id);
59
+ return interior === undefined ? null : toInteriorFrame(interior, exterior);
60
+ },
61
+ toExterior(id, interior) {
62
+ const frame = byId.get(id);
63
+ return frame === undefined ? null : toExteriorFrame(frame, interior);
64
+ },
65
+ };
66
+ }
@@ -0,0 +1,30 @@
1
+ import { type Aabb, type Footprint, type Vec2 } from "./geometry.js";
2
+ export interface PlacementObstacle {
3
+ aabb: Aabb;
4
+ id?: string;
5
+ }
6
+ export interface PlacementRequest {
7
+ center: Vec2;
8
+ footprint: Footprint;
9
+ quarterTurns?: number;
10
+ }
11
+ export interface PlacementRules {
12
+ bounds?: Aabb;
13
+ obstacles?: readonly PlacementObstacle[];
14
+ snap?: number;
15
+ }
16
+ export type PlacementResult = {
17
+ status: "ok";
18
+ center: Vec2;
19
+ aabb: Aabb;
20
+ } | {
21
+ status: "rejected";
22
+ reason: "out-of-bounds";
23
+ } | {
24
+ status: "rejected";
25
+ reason: "overlap";
26
+ obstacle: PlacementObstacle;
27
+ index: number;
28
+ };
29
+ export declare function validatePlacement(request: PlacementRequest, rules?: PlacementRules): PlacementResult;
30
+ export declare function footprintObstacle(request: PlacementRequest, id?: string): PlacementObstacle;
@@ -0,0 +1,20 @@
1
+ import { aabbContains, aabbOverlap, footprintAabb, snapToGrid, } from "./geometry.js";
2
+ export function validatePlacement(request, rules = {}) {
3
+ const center = rules.snap === undefined ? request.center : snapToGrid(request.center, rules.snap);
4
+ const aabb = footprintAabb(center, request.footprint, request.quarterTurns ?? 0);
5
+ if (rules.bounds !== undefined && !aabbContains(rules.bounds, aabb)) {
6
+ return { status: "rejected", reason: "out-of-bounds" };
7
+ }
8
+ const obstacles = rules.obstacles ?? [];
9
+ for (let index = 0; index < obstacles.length; index++) {
10
+ const obstacle = obstacles[index];
11
+ if (aabbOverlap(aabb, obstacle.aabb)) {
12
+ return { status: "rejected", reason: "overlap", obstacle, index };
13
+ }
14
+ }
15
+ return { status: "ok", center, aabb };
16
+ }
17
+ export function footprintObstacle(request, id) {
18
+ const aabb = footprintAabb(request.center, request.footprint, request.quarterTurns ?? 0);
19
+ return id === undefined ? { aabb } : { aabb, id };
20
+ }
@@ -0,0 +1,66 @@
1
+ import type { WorldBounds } from "./features.js";
2
+ import { type TerrainField } from "./terrain.js";
3
+ export type Rgb = readonly [number, number, number];
4
+ export type ColorInput = string | Rgb;
5
+ export interface RegionHeight {
6
+ baseHeight: number;
7
+ amplitude: number;
8
+ frequency: number;
9
+ octaves?: number;
10
+ lacunarity?: number;
11
+ persistence?: number;
12
+ ridged?: boolean;
13
+ }
14
+ /**
15
+ * A content-agnostic region. The engine blends `height` and the visual/physical
16
+ * knobs (`tint`, `water`, `fog`, `speedMultiplier`, …) across region borders and
17
+ * selects the dominant region by nearest center in `selector` space. `data` is an
18
+ * opaque payload the engine never inspects — games hang their own content off it
19
+ * (spawn tables, prop palettes, structure ids, names, …).
20
+ */
21
+ export interface RegionDef<T = unknown> {
22
+ id: string;
23
+ selector: readonly number[];
24
+ height: RegionHeight;
25
+ tint?: ColorInput;
26
+ steepTint?: ColorInput;
27
+ water?: ColorInput;
28
+ fog?: ColorInput;
29
+ fogDensity?: number;
30
+ speedMultiplier?: number;
31
+ data?: T;
32
+ }
33
+ export interface RegionSample<T = unknown> {
34
+ region: RegionDef<T>;
35
+ weight: number;
36
+ selector: readonly number[];
37
+ tint: Rgb;
38
+ steepTint: Rgb;
39
+ water: Rgb | null;
40
+ fog: Rgb | null;
41
+ fogDensity: number;
42
+ speedMultiplier: number;
43
+ data: T | undefined;
44
+ }
45
+ export interface RegionField<T = unknown> extends TerrainField {
46
+ sampleRegion(x: number, z: number): RegionSample<T>;
47
+ readonly regions: readonly RegionDef<T>[];
48
+ readonly seaLevel: number;
49
+ readonly seed: number;
50
+ }
51
+ export interface RegionFieldConfig<T = unknown> {
52
+ regions: readonly RegionDef<T>[];
53
+ seed?: number;
54
+ bounds?: WorldBounds;
55
+ seaLevel?: number;
56
+ /** Noise frequency per selector axis (defaults to 0.004 for every axis). */
57
+ selectorFrequencies?: readonly number[];
58
+ /** Per-axis distance weight when picking the nearest region (defaults to 1). */
59
+ axisWeights?: readonly number[];
60
+ /** Border sharpness; higher = tighter blends. */
61
+ sharpness?: number;
62
+ /** Domain-warp distance applied to selector sampling (0 disables). */
63
+ warp?: number;
64
+ }
65
+ export declare function createRegionField<T = unknown>(config: RegionFieldConfig<T>): RegionField<T>;
66
+ export declare function isRegionField(field: TerrainField): field is RegionField;