@jgengine/core 0.7.0 → 0.8.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.
- package/CHANGELOG.md +47 -0
- package/README.md +2 -0
- package/dist/ai/spawnDirector.d.ts +5 -0
- package/dist/ai/spawnDirector.js +13 -1
- package/dist/anim/easing.d.ts +14 -0
- package/dist/anim/easing.js +55 -0
- package/dist/behaviour/behaviour.d.ts +136 -0
- package/dist/behaviour/behaviour.js +388 -0
- package/dist/cards/cardPile.d.ts +1 -0
- package/dist/cards/cardPile.js +12 -3
- package/dist/combat/abilityKit.d.ts +5 -0
- package/dist/combat/abilityKit.js +10 -0
- package/dist/combat/effects.d.ts +1 -1
- package/dist/combat/effects.js +9 -6
- package/dist/combat/projectiles.d.ts +42 -5
- package/dist/combat/projectiles.js +98 -12
- package/dist/combat/telegraph.d.ts +5 -1
- package/dist/combat/telegraph.js +14 -0
- package/dist/commands/commandRegistry.d.ts +1 -1
- package/dist/commands/commandRegistry.js +2 -1
- package/dist/data/dataSource.d.ts +26 -0
- package/dist/data/dataSource.js +93 -0
- package/dist/data/devProxy.d.ts +11 -0
- package/dist/data/devProxy.js +38 -0
- package/dist/data/fetchJson.d.ts +20 -0
- package/dist/data/fetchJson.js +43 -0
- package/dist/data/jsonDataSource.d.ts +4 -0
- package/dist/data/jsonDataSource.js +6 -0
- package/dist/devtools/devtools.d.ts +157 -0
- package/dist/devtools/devtools.js +484 -0
- package/dist/devtools/transformTunables.d.ts +14 -0
- package/dist/devtools/transformTunables.js +38 -0
- package/dist/game/chat.d.ts +66 -0
- package/dist/game/chat.js +165 -0
- package/dist/game/defineGame.d.ts +8 -2
- package/dist/game/defineGame.js +2 -1
- package/dist/game/events.d.ts +25 -0
- package/dist/game/levelSequence.d.ts +44 -0
- package/dist/game/levelSequence.js +78 -0
- package/dist/game/playableGame.d.ts +237 -6
- package/dist/game/playableGame.js +6 -1
- package/dist/game/progression.d.ts +1 -0
- package/dist/game/progression.js +23 -2
- package/dist/game/race.d.ts +5 -0
- package/dist/game/race.js +20 -0
- package/dist/game/social.d.ts +42 -0
- package/dist/game/social.js +122 -0
- package/dist/game/spawnPoints.d.ts +19 -0
- package/dist/game/spawnPoints.js +23 -0
- package/dist/game/worldItem.d.ts +3 -0
- package/dist/game/worldItem.js +12 -6
- package/dist/input/actionBindings.d.ts +10 -0
- package/dist/input/actionBindings.js +14 -0
- package/dist/input/gestureSurface.d.ts +31 -0
- package/dist/input/gestureSurface.js +120 -0
- package/dist/input/pointer.d.ts +45 -0
- package/dist/input/pointer.js +48 -0
- package/dist/input/touchScheme.d.ts +83 -0
- package/dist/input/touchScheme.js +115 -0
- package/dist/item/affix.d.ts +1 -1
- package/dist/item/affix.js +1 -19
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +37 -1
- package/dist/movement/movementModel.d.ts +36 -3
- package/dist/movement/movementModel.js +81 -5
- package/dist/movement/voxelController.d.ts +61 -0
- package/dist/movement/voxelController.js +229 -0
- package/dist/multiplayer/chatContract.d.ts +42 -0
- package/dist/multiplayer/chatContract.js +57 -0
- package/dist/multiplayer/identity.d.ts +13 -0
- package/dist/multiplayer/identity.js +20 -0
- package/dist/multiplayer/poseSyncGate.d.ts +1 -0
- package/dist/multiplayer/poseSyncGate.js +13 -1
- package/dist/multiplayer/presenceModel.d.ts +3 -0
- package/dist/multiplayer/presenceModel.js +15 -1
- package/dist/multiplayer/voiceContract.d.ts +44 -0
- package/dist/multiplayer/voiceContract.js +108 -0
- package/dist/nav/navConstrain.d.ts +14 -0
- package/dist/nav/navConstrain.js +26 -0
- package/dist/nav/navFromEnvironment.d.ts +11 -0
- package/dist/nav/navFromEnvironment.js +16 -0
- package/dist/nav/navGrid.d.ts +10 -0
- package/dist/nav/navGrid.js +18 -1
- package/dist/nav/pathFollow.d.ts +4 -2
- package/dist/nav/pathFollow.js +5 -3
- package/dist/physics/ballisticSweep.d.ts +18 -0
- package/dist/physics/ballisticSweep.js +49 -0
- package/dist/physics/forceVolume.js +5 -5
- package/dist/physics/physicsWorld.d.ts +41 -2
- package/dist/physics/physicsWorld.js +232 -40
- package/dist/physics/spatialGrid.d.ts +1 -1
- package/dist/physics/spatialGrid.js +5 -0
- package/dist/physics/vehicleBody.d.ts +6 -0
- package/dist/physics/vehicleBody.js +16 -17
- package/dist/puzzle/cellGrid.d.ts +26 -0
- package/dist/puzzle/cellGrid.js +110 -0
- package/dist/puzzle/fallingPiece.d.ts +24 -0
- package/dist/puzzle/fallingPiece.js +51 -0
- package/dist/random/nameGen.d.ts +18 -0
- package/dist/random/nameGen.js +35 -0
- package/dist/random/rng.d.ts +2 -0
- package/dist/random/rng.js +23 -0
- package/dist/runtime/adapter.d.ts +35 -0
- package/dist/runtime/adapter.js +37 -1
- package/dist/runtime/cameraDirector.d.ts +10 -0
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/gameContext.d.ts +44 -2
- package/dist/runtime/gameContext.js +100 -4
- package/dist/runtime/hostPersistence.d.ts +21 -0
- package/dist/runtime/hostPersistence.js +50 -0
- package/dist/runtime/inputSnapshot.d.ts +7 -0
- package/dist/runtime/inputSnapshot.js +12 -0
- package/dist/runtime/motionIntents.d.ts +25 -0
- package/dist/runtime/motionIntents.js +25 -0
- package/dist/runtime/transport.d.ts +32 -0
- package/dist/scene/entityStore.d.ts +8 -0
- package/dist/scene/entityStore.js +25 -2
- package/dist/scene/movementSpeed.d.ts +35 -0
- package/dist/scene/movementSpeed.js +20 -0
- package/dist/scene/objectQuery.d.ts +18 -0
- package/dist/scene/objectQuery.js +108 -0
- package/dist/scene/objectStore.d.ts +11 -0
- package/dist/scene/objectStore.js +109 -4
- package/dist/scene/paintLayer.d.ts +15 -0
- package/dist/scene/paintLayer.js +53 -0
- package/dist/scene/spatial.d.ts +21 -0
- package/dist/scene/spatial.js +63 -1
- package/dist/sensor/concealment.d.ts +21 -0
- package/dist/sensor/concealment.js +63 -0
- package/dist/sensor/freezeMonitor.d.ts +17 -0
- package/dist/sensor/freezeMonitor.js +35 -0
- package/dist/session/roles.d.ts +6 -0
- package/dist/session/roles.js +32 -0
- package/dist/session/roundState.d.ts +41 -17
- package/dist/session/roundState.js +67 -38
- package/dist/tactics/fallingGrid.d.ts +62 -0
- package/dist/tactics/fallingGrid.js +158 -0
- package/dist/tactics/tacticalGrid.d.ts +11 -0
- package/dist/tactics/tacticalGrid.js +17 -0
- package/dist/time/simClock.d.ts +25 -1
- package/dist/time/simClock.js +28 -3
- package/dist/turn/intent.d.ts +14 -0
- package/dist/turn/intent.js +29 -0
- package/dist/turn/turnLoop.d.ts +2 -0
- package/dist/turn/turnLoop.js +8 -1
- package/dist/world/environmentSummary.d.ts +53 -0
- package/dist/world/environmentSummary.js +129 -0
- package/dist/world/features.d.ts +87 -6
- package/dist/world/features.js +52 -2
- package/dist/world/gridInstances.d.ts +13 -0
- package/dist/world/gridInstances.js +22 -0
- package/dist/world/placementController.d.ts +22 -1
- package/dist/world/placementController.js +56 -0
- package/dist/world/terrain.d.ts +23 -1
- package/dist/world/terrain.js +64 -1
- package/dist/world/voxelField.d.ts +54 -0
- package/dist/world/voxelField.js +241 -0
- package/llms.txt +4102 -0
- package/package.json +3 -2
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type GeneratedBuilding } from "./buildings.js";
|
|
2
|
+
import type { BuildingEnvironmentDescriptor, EnvironmentWorldFeature, WorldBounds } from "./features.js";
|
|
3
|
+
import type { Aabb } from "./geometry.js";
|
|
4
|
+
export declare function resolveStructureBuildings(descriptor: BuildingEnvironmentDescriptor): GeneratedBuilding[];
|
|
5
|
+
export interface TerrainHeightStats {
|
|
6
|
+
min: number;
|
|
7
|
+
max: number;
|
|
8
|
+
mean: number;
|
|
9
|
+
finite: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface TerrainSummary {
|
|
12
|
+
bounds: WorldBounds;
|
|
13
|
+
height: TerrainHeightStats;
|
|
14
|
+
waterLevel?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface StructureSummary {
|
|
17
|
+
style: string;
|
|
18
|
+
requested: number;
|
|
19
|
+
buildings: number;
|
|
20
|
+
parts: number;
|
|
21
|
+
bounds: Aabb;
|
|
22
|
+
}
|
|
23
|
+
export interface VegetationSummary {
|
|
24
|
+
area: WorldBounds;
|
|
25
|
+
density: number;
|
|
26
|
+
}
|
|
27
|
+
export interface WaterSummary {
|
|
28
|
+
level: number;
|
|
29
|
+
bounds: WorldBounds;
|
|
30
|
+
}
|
|
31
|
+
export interface WeatherSummary {
|
|
32
|
+
kind: "rain" | "snow";
|
|
33
|
+
density: number;
|
|
34
|
+
}
|
|
35
|
+
export interface EnvironmentCounts {
|
|
36
|
+
terrain: number;
|
|
37
|
+
structureGroups: number;
|
|
38
|
+
buildings: number;
|
|
39
|
+
buildingParts: number;
|
|
40
|
+
vegetationFields: number;
|
|
41
|
+
waterBodies: number;
|
|
42
|
+
weatherSystems: number;
|
|
43
|
+
}
|
|
44
|
+
export interface EnvironmentSummary {
|
|
45
|
+
terrain?: TerrainSummary;
|
|
46
|
+
structures: readonly StructureSummary[];
|
|
47
|
+
vegetation: readonly VegetationSummary[];
|
|
48
|
+
water: readonly WaterSummary[];
|
|
49
|
+
weather: readonly WeatherSummary[];
|
|
50
|
+
counts: EnvironmentCounts;
|
|
51
|
+
isEmpty: boolean;
|
|
52
|
+
}
|
|
53
|
+
export declare function summarizeEnvironment(feature: EnvironmentWorldFeature): EnvironmentSummary;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { generateBuildingDistrict } from "./buildings.js";
|
|
2
|
+
import { resolveTerrainField } from "./terrain.js";
|
|
3
|
+
export function resolveStructureBuildings(descriptor) {
|
|
4
|
+
const columns = Math.max(1, Math.ceil(Math.sqrt(descriptor.count)));
|
|
5
|
+
const rows = Math.max(1, Math.ceil(descriptor.count / columns));
|
|
6
|
+
const spacing = descriptor.spacing;
|
|
7
|
+
const [centerX, centerZ] = descriptor.position ?? [0, 0];
|
|
8
|
+
const origin = [
|
|
9
|
+
centerX - ((columns - 1) * (descriptor.footprint.w + spacing)) / 2,
|
|
10
|
+
centerZ - ((rows - 1) * (descriptor.footprint.d + spacing)) / 2,
|
|
11
|
+
];
|
|
12
|
+
return generateBuildingDistrict({
|
|
13
|
+
rows,
|
|
14
|
+
columns,
|
|
15
|
+
origin,
|
|
16
|
+
lotSize: descriptor.footprint,
|
|
17
|
+
streetWidth: spacing,
|
|
18
|
+
floorRange: descriptor.stories,
|
|
19
|
+
base: { floorHeight: descriptor.storyHeight },
|
|
20
|
+
...(descriptor.seed === undefined ? {} : { seed: descriptor.seed }),
|
|
21
|
+
}).slice(0, descriptor.count);
|
|
22
|
+
}
|
|
23
|
+
const TERRAIN_SAMPLE_RESOLUTION = 16;
|
|
24
|
+
function sampleTerrainHeights(descriptor) {
|
|
25
|
+
const field = resolveTerrainField(descriptor);
|
|
26
|
+
const halfW = descriptor.bounds.w / 2;
|
|
27
|
+
const halfD = descriptor.bounds.d / 2;
|
|
28
|
+
const steps = TERRAIN_SAMPLE_RESOLUTION;
|
|
29
|
+
let min = Infinity;
|
|
30
|
+
let max = -Infinity;
|
|
31
|
+
let sum = 0;
|
|
32
|
+
let finite = true;
|
|
33
|
+
for (let ix = 0; ix <= steps; ix += 1) {
|
|
34
|
+
for (let iz = 0; iz <= steps; iz += 1) {
|
|
35
|
+
const x = -halfW + (descriptor.bounds.w * ix) / steps;
|
|
36
|
+
const z = -halfD + (descriptor.bounds.d * iz) / steps;
|
|
37
|
+
const height = field.sampleHeight(x, z);
|
|
38
|
+
if (!Number.isFinite(height)) {
|
|
39
|
+
finite = false;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (height < min)
|
|
43
|
+
min = height;
|
|
44
|
+
if (height > max)
|
|
45
|
+
max = height;
|
|
46
|
+
sum += height;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const count = (steps + 1) * (steps + 1);
|
|
50
|
+
if (!finite)
|
|
51
|
+
return { min: NaN, max: NaN, mean: NaN, finite: false };
|
|
52
|
+
return { min, max, mean: sum / count, finite: true };
|
|
53
|
+
}
|
|
54
|
+
function unionAabb(buildings) {
|
|
55
|
+
if (buildings.length === 0)
|
|
56
|
+
return { minX: 0, minZ: 0, maxX: 0, maxZ: 0 };
|
|
57
|
+
let minX = Infinity;
|
|
58
|
+
let minZ = Infinity;
|
|
59
|
+
let maxX = -Infinity;
|
|
60
|
+
let maxZ = -Infinity;
|
|
61
|
+
for (const building of buildings) {
|
|
62
|
+
if (building.bounds.minX < minX)
|
|
63
|
+
minX = building.bounds.minX;
|
|
64
|
+
if (building.bounds.minZ < minZ)
|
|
65
|
+
minZ = building.bounds.minZ;
|
|
66
|
+
if (building.bounds.maxX > maxX)
|
|
67
|
+
maxX = building.bounds.maxX;
|
|
68
|
+
if (building.bounds.maxZ > maxZ)
|
|
69
|
+
maxZ = building.bounds.maxZ;
|
|
70
|
+
}
|
|
71
|
+
return { minX, minZ, maxX, maxZ };
|
|
72
|
+
}
|
|
73
|
+
function summarizeStructures(descriptor) {
|
|
74
|
+
const buildings = resolveStructureBuildings(descriptor);
|
|
75
|
+
return {
|
|
76
|
+
style: descriptor.style,
|
|
77
|
+
requested: descriptor.count,
|
|
78
|
+
buildings: buildings.length,
|
|
79
|
+
parts: buildings.reduce((sum, building) => sum + building.parts.length, 0),
|
|
80
|
+
bounds: unionAabb(buildings),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function summarizeTerrain(descriptor) {
|
|
84
|
+
return {
|
|
85
|
+
bounds: descriptor.bounds,
|
|
86
|
+
height: sampleTerrainHeights(descriptor),
|
|
87
|
+
...(descriptor.waterLevel === undefined ? {} : { waterLevel: descriptor.waterLevel }),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function summarizeEnvironment(feature) {
|
|
91
|
+
const terrain = feature.terrain === undefined ? undefined : summarizeTerrain(feature.terrain);
|
|
92
|
+
const structures = (feature.structures ?? []).map(summarizeStructures);
|
|
93
|
+
const vegetation = (feature.vegetation ?? []).map((entry) => ({
|
|
94
|
+
area: entry.area,
|
|
95
|
+
density: entry.density,
|
|
96
|
+
}));
|
|
97
|
+
const water = (feature.water ?? []).map((entry) => ({
|
|
98
|
+
level: entry.level,
|
|
99
|
+
bounds: entry.bounds,
|
|
100
|
+
}));
|
|
101
|
+
const weather = (feature.weather ?? []).map((entry) => ({
|
|
102
|
+
kind: entry.kind,
|
|
103
|
+
density: entry.density,
|
|
104
|
+
}));
|
|
105
|
+
const buildings = structures.reduce((sum, entry) => sum + entry.buildings, 0);
|
|
106
|
+
const counts = {
|
|
107
|
+
terrain: terrain === undefined ? 0 : 1,
|
|
108
|
+
structureGroups: structures.length,
|
|
109
|
+
buildings,
|
|
110
|
+
buildingParts: structures.reduce((sum, entry) => sum + entry.parts, 0),
|
|
111
|
+
vegetationFields: vegetation.length,
|
|
112
|
+
waterBodies: water.length,
|
|
113
|
+
weatherSystems: weather.length,
|
|
114
|
+
};
|
|
115
|
+
const isEmpty = counts.terrain === 0 &&
|
|
116
|
+
buildings === 0 &&
|
|
117
|
+
counts.vegetationFields === 0 &&
|
|
118
|
+
counts.waterBodies === 0 &&
|
|
119
|
+
counts.weatherSystems === 0;
|
|
120
|
+
return {
|
|
121
|
+
...(terrain === undefined ? {} : { terrain }),
|
|
122
|
+
structures,
|
|
123
|
+
vegetation,
|
|
124
|
+
water,
|
|
125
|
+
weather,
|
|
126
|
+
counts,
|
|
127
|
+
isEmpty,
|
|
128
|
+
};
|
|
129
|
+
}
|
package/dist/world/features.d.ts
CHANGED
|
@@ -6,17 +6,36 @@ export type EnvironmentVec2 = readonly [number, number];
|
|
|
6
6
|
export interface EnvironmentArea extends WorldBounds {
|
|
7
7
|
h?: number;
|
|
8
8
|
}
|
|
9
|
+
export interface TerrainColors {
|
|
10
|
+
low?: string;
|
|
11
|
+
high?: string;
|
|
12
|
+
waterline?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface TerrainFlattenMask {
|
|
15
|
+
center: EnvironmentVec2;
|
|
16
|
+
radius: number;
|
|
17
|
+
/** Target flat height; defaults to the noise field's height at `center`. */
|
|
18
|
+
height?: number;
|
|
19
|
+
/** Extra blend-ring width outside `radius` where the flat height smoothsteps back to the noise value; default `radius * 0.5`. */
|
|
20
|
+
falloff?: number;
|
|
21
|
+
}
|
|
9
22
|
export interface TerrainEnvironmentConfig {
|
|
10
23
|
bounds?: WorldBounds;
|
|
11
24
|
height?: number;
|
|
12
25
|
heightMap?: string;
|
|
26
|
+
/** Named palette preset (see `TERRAIN_MATERIAL_PALETTES` in `world/terrain`); default "grass". Overridden field-by-field by `colors`. */
|
|
13
27
|
material?: string;
|
|
28
|
+
/** Explicit low/high/waterline hex colors; any field left unset falls back to the resolved `material` preset. */
|
|
29
|
+
colors?: TerrainColors;
|
|
30
|
+
segments?: number;
|
|
14
31
|
seed?: string;
|
|
15
32
|
frequency?: number;
|
|
16
33
|
octaves?: number;
|
|
17
34
|
ridged?: boolean;
|
|
18
35
|
baseHeight?: number;
|
|
19
36
|
waterLevel?: number;
|
|
37
|
+
/** Flat pads carved into the noise field, e.g. for building pads or spawn circles. */
|
|
38
|
+
flatten?: readonly TerrainFlattenMask[];
|
|
20
39
|
}
|
|
21
40
|
export interface RainEnvironmentConfig {
|
|
22
41
|
area?: EnvironmentArea;
|
|
@@ -46,14 +65,31 @@ export interface GrassEnvironmentConfig {
|
|
|
46
65
|
}
|
|
47
66
|
export interface OceanEnvironmentConfig {
|
|
48
67
|
bounds?: WorldBounds;
|
|
68
|
+
position?: EnvironmentVec2;
|
|
49
69
|
level?: number;
|
|
50
70
|
waveHeight?: number;
|
|
51
71
|
waveScale?: number;
|
|
52
72
|
waveSpeed?: number;
|
|
53
73
|
color?: string;
|
|
54
74
|
}
|
|
75
|
+
export interface SkyEnvironmentConfig {
|
|
76
|
+
/** Fixed look used when `timeOfDay` is off (or no clock is available); default "day". */
|
|
77
|
+
preset?: "day" | "dusk" | "night";
|
|
78
|
+
/** Drive sun/sky from the world clock's `calendar().dayFraction` instead of the fixed `preset`. */
|
|
79
|
+
timeOfDay?: boolean;
|
|
80
|
+
horizonColor?: string;
|
|
81
|
+
zenithColor?: string;
|
|
82
|
+
sunIntensity?: number;
|
|
83
|
+
ambientIntensity?: number;
|
|
84
|
+
fog?: {
|
|
85
|
+
color?: string;
|
|
86
|
+
near?: number;
|
|
87
|
+
far?: number;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
55
90
|
export interface BuildingEnvironmentConfig {
|
|
56
91
|
count?: number;
|
|
92
|
+
position?: EnvironmentVec2;
|
|
57
93
|
footprint?: WorldBounds;
|
|
58
94
|
stories?: readonly [number, number];
|
|
59
95
|
storyHeight?: number;
|
|
@@ -61,6 +97,18 @@ export interface BuildingEnvironmentConfig {
|
|
|
61
97
|
style?: string;
|
|
62
98
|
seed?: string;
|
|
63
99
|
}
|
|
100
|
+
export type PadSize = readonly [number, number] | {
|
|
101
|
+
radius: number;
|
|
102
|
+
};
|
|
103
|
+
export interface PadEnvironmentConfig {
|
|
104
|
+
center: EnvironmentVec2;
|
|
105
|
+
size: PadSize;
|
|
106
|
+
/** Surface offset above the terrain height at `center`; default `0.05` — just proud of the ground. */
|
|
107
|
+
height?: number;
|
|
108
|
+
color?: string;
|
|
109
|
+
/** Rotation applied to rectangular pads; ignored for circular pads. */
|
|
110
|
+
rotationY?: number;
|
|
111
|
+
}
|
|
64
112
|
export type TerrainEnvironmentDescriptor = {
|
|
65
113
|
kind: "terrain";
|
|
66
114
|
} & Required<Pick<TerrainEnvironmentConfig, "bounds" | "height">> & Omit<TerrainEnvironmentConfig, "bounds" | "height">;
|
|
@@ -75,10 +123,16 @@ export type GrassEnvironmentDescriptor = {
|
|
|
75
123
|
} & Required<Pick<GrassEnvironmentConfig, "area" | "density" | "bladeHeight" | "bladeWidth" | "windStrength" | "colors">> & Pick<GrassEnvironmentConfig, "seed">;
|
|
76
124
|
export type OceanEnvironmentDescriptor = {
|
|
77
125
|
kind: "ocean";
|
|
78
|
-
} & Required<Pick<OceanEnvironmentConfig, "bounds" | "level" | "waveHeight" | "waveScale" | "waveSpeed" | "color"
|
|
126
|
+
} & Required<Pick<OceanEnvironmentConfig, "bounds" | "level" | "waveHeight" | "waveScale" | "waveSpeed" | "color">> & Pick<OceanEnvironmentConfig, "position">;
|
|
79
127
|
export type BuildingEnvironmentDescriptor = {
|
|
80
128
|
kind: "building";
|
|
81
|
-
} & Required<Pick<BuildingEnvironmentConfig, "count" | "footprint" | "stories" | "storyHeight" | "spacing" | "style">> & Pick<BuildingEnvironmentConfig, "seed">;
|
|
129
|
+
} & Required<Pick<BuildingEnvironmentConfig, "count" | "footprint" | "stories" | "storyHeight" | "spacing" | "style">> & Pick<BuildingEnvironmentConfig, "seed" | "position">;
|
|
130
|
+
export type PadEnvironmentDescriptor = {
|
|
131
|
+
kind: "pad";
|
|
132
|
+
} & Required<Pick<PadEnvironmentConfig, "center" | "size" | "height" | "color">> & Pick<PadEnvironmentConfig, "rotationY">;
|
|
133
|
+
export type SkyEnvironmentDescriptor = {
|
|
134
|
+
kind: "sky";
|
|
135
|
+
} & Required<Pick<SkyEnvironmentConfig, "preset" | "timeOfDay">> & Omit<SkyEnvironmentConfig, "preset" | "timeOfDay">;
|
|
82
136
|
export type WeatherEnvironmentDescriptor = RainEnvironmentDescriptor | SnowEnvironmentDescriptor;
|
|
83
137
|
export type VegetationEnvironmentDescriptor = GrassEnvironmentDescriptor;
|
|
84
138
|
export type WaterEnvironmentDescriptor = OceanEnvironmentDescriptor;
|
|
@@ -86,36 +140,59 @@ export type StructureEnvironmentDescriptor = BuildingEnvironmentDescriptor;
|
|
|
86
140
|
export type EnvironmentDescriptorList<T> = T | readonly T[];
|
|
87
141
|
export interface EnvironmentWorldConfig {
|
|
88
142
|
terrain?: TerrainEnvironmentDescriptor;
|
|
143
|
+
sky?: SkyEnvironmentDescriptor;
|
|
89
144
|
weather?: EnvironmentDescriptorList<WeatherEnvironmentDescriptor>;
|
|
90
145
|
vegetation?: EnvironmentDescriptorList<VegetationEnvironmentDescriptor>;
|
|
91
146
|
water?: EnvironmentDescriptorList<WaterEnvironmentDescriptor>;
|
|
92
147
|
structures?: EnvironmentDescriptorList<StructureEnvironmentDescriptor>;
|
|
148
|
+
/** Ground pads, e.g. platforms or paved patches; each implicitly flattens the terrain beneath it. */
|
|
149
|
+
pads?: readonly PadEnvironmentDescriptor[];
|
|
93
150
|
}
|
|
94
151
|
export interface EnvironmentWorldFeature {
|
|
95
152
|
kind: "environment";
|
|
96
153
|
terrain?: TerrainEnvironmentDescriptor;
|
|
154
|
+
sky?: SkyEnvironmentDescriptor;
|
|
97
155
|
weather?: readonly WeatherEnvironmentDescriptor[];
|
|
98
156
|
vegetation?: readonly VegetationEnvironmentDescriptor[];
|
|
99
157
|
water?: readonly WaterEnvironmentDescriptor[];
|
|
100
158
|
structures?: readonly StructureEnvironmentDescriptor[];
|
|
159
|
+
pads?: readonly PadEnvironmentDescriptor[];
|
|
160
|
+
}
|
|
161
|
+
export interface WorldGridCell {
|
|
162
|
+
x: number;
|
|
163
|
+
z: number;
|
|
164
|
+
/** Extruded box height for this cell; falls back to the config's `baseHeight`, then `1`. */
|
|
165
|
+
height?: number;
|
|
166
|
+
/** Per-cell tint; falls back to the config's `defaultColor`. */
|
|
167
|
+
color?: string;
|
|
168
|
+
}
|
|
169
|
+
/** Shared by `biomes()`/`voxel()`/`plots()`/`tilemap()` so the shell can render their declared content as instanced boxes without a hand-written renderer. */
|
|
170
|
+
export interface WorldGridConfig {
|
|
171
|
+
cells?: readonly WorldGridCell[];
|
|
172
|
+
/** World-unit size of one grid cell. Default 1. */
|
|
173
|
+
cellSize?: number;
|
|
174
|
+
/** Height used for cells that omit their own `height`. Default 1. */
|
|
175
|
+
baseHeight?: number;
|
|
176
|
+
/** Color used for cells that omit their own `color`. */
|
|
177
|
+
defaultColor?: string;
|
|
101
178
|
}
|
|
102
|
-
export interface BiomesWorldConfig {
|
|
179
|
+
export interface BiomesWorldConfig extends WorldGridConfig {
|
|
103
180
|
map: string;
|
|
104
181
|
zones: string;
|
|
105
182
|
bounds?: WorldBounds;
|
|
106
183
|
}
|
|
107
|
-
export interface VoxelWorldConfig {
|
|
184
|
+
export interface VoxelWorldConfig extends WorldGridConfig {
|
|
108
185
|
seed: string;
|
|
109
186
|
generate?: string;
|
|
110
187
|
streaming?: {
|
|
111
188
|
radius: number;
|
|
112
189
|
};
|
|
113
190
|
}
|
|
114
|
-
export interface PlotsWorldConfig {
|
|
191
|
+
export interface PlotsWorldConfig extends WorldGridConfig {
|
|
115
192
|
city?: string;
|
|
116
193
|
interiors?: string;
|
|
117
194
|
}
|
|
118
|
-
export interface TilemapWorldConfig {
|
|
195
|
+
export interface TilemapWorldConfig extends WorldGridConfig {
|
|
119
196
|
map: string;
|
|
120
197
|
}
|
|
121
198
|
export type WorldFeature = ({
|
|
@@ -129,13 +206,17 @@ export type WorldFeature = ({
|
|
|
129
206
|
} & TilemapWorldConfig) | EnvironmentWorldFeature | {
|
|
130
207
|
kind: "flat";
|
|
131
208
|
};
|
|
209
|
+
/** Derives implicit `TerrainFlattenMask`s carving each pad's footprint into the terrain beneath it. */
|
|
210
|
+
export declare function padFlattenMasks(pads: readonly PadEnvironmentDescriptor[]): readonly TerrainFlattenMask[];
|
|
132
211
|
export declare function environment(config?: EnvironmentWorldConfig): EnvironmentWorldFeature;
|
|
133
212
|
export declare function terrain(config?: TerrainEnvironmentConfig): TerrainEnvironmentDescriptor;
|
|
213
|
+
export declare function sky(config?: SkyEnvironmentConfig): SkyEnvironmentDescriptor;
|
|
134
214
|
export declare function rain(config?: RainEnvironmentConfig): RainEnvironmentDescriptor;
|
|
135
215
|
export declare function snow(config?: SnowEnvironmentConfig): SnowEnvironmentDescriptor;
|
|
136
216
|
export declare function grass(config?: GrassEnvironmentConfig): GrassEnvironmentDescriptor;
|
|
137
217
|
export declare function ocean(config?: OceanEnvironmentConfig): OceanEnvironmentDescriptor;
|
|
138
218
|
export declare function building(config?: BuildingEnvironmentConfig): BuildingEnvironmentDescriptor;
|
|
219
|
+
export declare function pad(config: PadEnvironmentConfig): PadEnvironmentDescriptor;
|
|
139
220
|
export declare function biomes(config: BiomesWorldConfig): WorldFeature;
|
|
140
221
|
export declare function voxel(config: VoxelWorldConfig): WorldFeature;
|
|
141
222
|
export declare function plots(config?: PlotsWorldConfig): WorldFeature;
|
package/dist/world/features.js
CHANGED
|
@@ -7,18 +7,39 @@ function list(value) {
|
|
|
7
7
|
function withOptional(base, optional) {
|
|
8
8
|
return optional === undefined ? base : { ...base, ...optional };
|
|
9
9
|
}
|
|
10
|
+
/** Derives implicit `TerrainFlattenMask`s carving each pad's footprint into the terrain beneath it. */
|
|
11
|
+
export function padFlattenMasks(pads) {
|
|
12
|
+
return pads.map((padDescriptor) => ({
|
|
13
|
+
center: padDescriptor.center,
|
|
14
|
+
radius: padHalfExtent(padDescriptor.size) * 1.2,
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
function padHalfExtent(size) {
|
|
18
|
+
return "radius" in size ? size.radius : Math.max(size[0], size[1]) / 2;
|
|
19
|
+
}
|
|
20
|
+
function withPadFlatten(terrainDescriptor, pads) {
|
|
21
|
+
if (terrainDescriptor === undefined || pads === undefined || pads.length === 0)
|
|
22
|
+
return terrainDescriptor;
|
|
23
|
+
return {
|
|
24
|
+
...terrainDescriptor,
|
|
25
|
+
flatten: [...(terrainDescriptor.flatten ?? []), ...padFlattenMasks(pads)],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
10
28
|
export function environment(config = {}) {
|
|
11
29
|
const weather = list(config.weather);
|
|
12
30
|
const vegetation = list(config.vegetation);
|
|
13
31
|
const water = list(config.water);
|
|
14
32
|
const structures = list(config.structures);
|
|
33
|
+
const terrainDescriptor = withPadFlatten(config.terrain, config.pads);
|
|
15
34
|
return {
|
|
16
35
|
kind: "environment",
|
|
17
|
-
...(
|
|
36
|
+
...(terrainDescriptor === undefined ? {} : { terrain: terrainDescriptor }),
|
|
37
|
+
...(config.sky === undefined ? {} : { sky: config.sky }),
|
|
18
38
|
...(weather === undefined ? {} : { weather }),
|
|
19
39
|
...(vegetation === undefined ? {} : { vegetation }),
|
|
20
40
|
...(water === undefined ? {} : { water }),
|
|
21
41
|
...(structures === undefined ? {} : { structures }),
|
|
42
|
+
...(config.pads === undefined ? {} : { pads: config.pads }),
|
|
22
43
|
};
|
|
23
44
|
}
|
|
24
45
|
export function terrain(config = {}) {
|
|
@@ -29,12 +50,28 @@ export function terrain(config = {}) {
|
|
|
29
50
|
}, {
|
|
30
51
|
...(config.heightMap === undefined ? {} : { heightMap: config.heightMap }),
|
|
31
52
|
...(config.material === undefined ? {} : { material: config.material }),
|
|
53
|
+
...(config.colors === undefined ? {} : { colors: config.colors }),
|
|
54
|
+
...(config.segments === undefined ? {} : { segments: config.segments }),
|
|
32
55
|
...(config.seed === undefined ? {} : { seed: config.seed }),
|
|
33
56
|
...(config.frequency === undefined ? {} : { frequency: config.frequency }),
|
|
34
57
|
...(config.octaves === undefined ? {} : { octaves: config.octaves }),
|
|
35
58
|
...(config.ridged === undefined ? {} : { ridged: config.ridged }),
|
|
36
59
|
...(config.baseHeight === undefined ? {} : { baseHeight: config.baseHeight }),
|
|
37
60
|
...(config.waterLevel === undefined ? {} : { waterLevel: config.waterLevel }),
|
|
61
|
+
...(config.flatten === undefined ? {} : { flatten: config.flatten }),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export function sky(config = {}) {
|
|
65
|
+
return withOptional({
|
|
66
|
+
kind: "sky",
|
|
67
|
+
preset: config.preset ?? "day",
|
|
68
|
+
timeOfDay: config.timeOfDay ?? false,
|
|
69
|
+
}, {
|
|
70
|
+
...(config.horizonColor === undefined ? {} : { horizonColor: config.horizonColor }),
|
|
71
|
+
...(config.zenithColor === undefined ? {} : { zenithColor: config.zenithColor }),
|
|
72
|
+
...(config.sunIntensity === undefined ? {} : { sunIntensity: config.sunIntensity }),
|
|
73
|
+
...(config.ambientIntensity === undefined ? {} : { ambientIntensity: config.ambientIntensity }),
|
|
74
|
+
...(config.fog === undefined ? {} : { fog: config.fog }),
|
|
38
75
|
});
|
|
39
76
|
}
|
|
40
77
|
export function rain(config = {}) {
|
|
@@ -80,6 +117,7 @@ export function ocean(config = {}) {
|
|
|
80
117
|
waveScale: config.waveScale ?? 18,
|
|
81
118
|
waveSpeed: config.waveSpeed ?? 0.55,
|
|
82
119
|
color: config.color ?? "#1d7fa3",
|
|
120
|
+
...(config.position === undefined ? {} : { position: config.position }),
|
|
83
121
|
};
|
|
84
122
|
}
|
|
85
123
|
export function building(config = {}) {
|
|
@@ -91,7 +129,19 @@ export function building(config = {}) {
|
|
|
91
129
|
storyHeight: config.storyHeight ?? 3,
|
|
92
130
|
spacing: config.spacing ?? 2,
|
|
93
131
|
style: config.style ?? "generic",
|
|
94
|
-
},
|
|
132
|
+
}, {
|
|
133
|
+
...(config.seed === undefined ? {} : { seed: config.seed }),
|
|
134
|
+
...(config.position === undefined ? {} : { position: config.position }),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
export function pad(config) {
|
|
138
|
+
return withOptional({
|
|
139
|
+
kind: "pad",
|
|
140
|
+
center: config.center,
|
|
141
|
+
size: config.size,
|
|
142
|
+
height: config.height ?? 0.05,
|
|
143
|
+
color: config.color ?? "#8b8680",
|
|
144
|
+
}, config.rotationY === undefined ? undefined : { rotationY: config.rotationY });
|
|
95
145
|
}
|
|
96
146
|
export function biomes(config) {
|
|
97
147
|
return { kind: "biomes", ...config };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WorldGridConfig } from "./features.js";
|
|
2
|
+
export interface GridInstanceTransform {
|
|
3
|
+
position: readonly [number, number, number];
|
|
4
|
+
scale: readonly [number, number, number];
|
|
5
|
+
color: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Pure cell→instance math for `biomes()`/`voxel()`/`plots()`/`tilemap()` world features: one
|
|
9
|
+
* extruded box per declared cell, grounded at y=0 and centered on its cell in x/z. Consumed by
|
|
10
|
+
* the shell's grid-world renderer so it can drive a single `THREE.InstancedMesh` without
|
|
11
|
+
* duplicating this math per game.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveGridInstances(config: WorldGridConfig): readonly GridInstanceTransform[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const DEFAULT_CELL_SIZE = 1;
|
|
2
|
+
const DEFAULT_BASE_HEIGHT = 1;
|
|
3
|
+
const DEFAULT_GRID_COLOR = "#8a8f98";
|
|
4
|
+
/**
|
|
5
|
+
* Pure cell→instance math for `biomes()`/`voxel()`/`plots()`/`tilemap()` world features: one
|
|
6
|
+
* extruded box per declared cell, grounded at y=0 and centered on its cell in x/z. Consumed by
|
|
7
|
+
* the shell's grid-world renderer so it can drive a single `THREE.InstancedMesh` without
|
|
8
|
+
* duplicating this math per game.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveGridInstances(config) {
|
|
11
|
+
const cells = config.cells ?? [];
|
|
12
|
+
const cellSize = config.cellSize ?? DEFAULT_CELL_SIZE;
|
|
13
|
+
const defaultColor = config.defaultColor ?? DEFAULT_GRID_COLOR;
|
|
14
|
+
return cells.map((cell) => {
|
|
15
|
+
const height = cell.height ?? config.baseHeight ?? DEFAULT_BASE_HEIGHT;
|
|
16
|
+
return {
|
|
17
|
+
position: [cell.x * cellSize, height / 2, cell.z * cellSize],
|
|
18
|
+
scale: [cellSize, height, cellSize],
|
|
19
|
+
color: cell.color ?? defaultColor,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -6,12 +6,24 @@ export interface PlacementHit {
|
|
|
6
6
|
point: PlacementVec3;
|
|
7
7
|
normal: PlacementVec3;
|
|
8
8
|
}
|
|
9
|
+
/** A fixed buildable slot the placement controller snaps to instead of grid/free positioning. */
|
|
10
|
+
export interface PlacementSlot {
|
|
11
|
+
id: string;
|
|
12
|
+
center: PlacementVec3;
|
|
13
|
+
/** Capture radius around `center`. Defaults to `config.slotRadius`, then `1.5`. */
|
|
14
|
+
radius?: number;
|
|
15
|
+
/** `false` hides the slot from hover resolution without releasing it. Default `true`. */
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
}
|
|
9
18
|
export interface PlacementControllerConfig {
|
|
10
19
|
footprint: Footprint;
|
|
11
20
|
rules?: PlacementRules;
|
|
12
21
|
snapMode?: SnapMode;
|
|
13
22
|
grid?: number;
|
|
14
23
|
quarterTurns?: number;
|
|
24
|
+
slots?: readonly PlacementSlot[];
|
|
25
|
+
/** Fallback capture radius for slots that don't set their own. Default `1.5`. */
|
|
26
|
+
slotRadius?: number;
|
|
15
27
|
}
|
|
16
28
|
export interface PlacementPreview {
|
|
17
29
|
center: Vec2;
|
|
@@ -20,9 +32,11 @@ export interface PlacementPreview {
|
|
|
20
32
|
footprint: Footprint;
|
|
21
33
|
aabb: Aabb;
|
|
22
34
|
valid: boolean;
|
|
23
|
-
reason?: "out-of-bounds" | "overlap";
|
|
35
|
+
reason?: "out-of-bounds" | "overlap" | "no-slot";
|
|
24
36
|
snapMode: SnapMode;
|
|
25
37
|
normal: PlacementVec3;
|
|
38
|
+
/** Id of the resolved slot, when the controller is in slot mode and a slot matched. */
|
|
39
|
+
slotId?: string;
|
|
26
40
|
}
|
|
27
41
|
export interface PlacementCommit {
|
|
28
42
|
center: Vec2;
|
|
@@ -31,6 +45,8 @@ export interface PlacementCommit {
|
|
|
31
45
|
quarterTurns: number;
|
|
32
46
|
footprint: Footprint;
|
|
33
47
|
aabb: Aabb;
|
|
48
|
+
/** Id of the slot this commit occupied, when the controller is in slot mode. */
|
|
49
|
+
slotId?: string;
|
|
34
50
|
}
|
|
35
51
|
export interface PlacementController {
|
|
36
52
|
hover(hit: PlacementHit): PlacementPreview;
|
|
@@ -41,6 +57,11 @@ export interface PlacementController {
|
|
|
41
57
|
setFootprint(footprint: Footprint): void;
|
|
42
58
|
setRules(rules: PlacementRules): void;
|
|
43
59
|
setGrid(grid: number): void;
|
|
60
|
+
setSlots(slots: readonly PlacementSlot[]): void;
|
|
61
|
+
/** Id of the currently hovered slot, or `null` when idle, out of slot mode, or unmatched. */
|
|
62
|
+
hoveredSlotId(): string | null;
|
|
63
|
+
/** Frees a previously committed slot so it can match again. */
|
|
64
|
+
releaseSlot(id: string): void;
|
|
44
65
|
commit(): PlacementCommit | null;
|
|
45
66
|
reset(): void;
|
|
46
67
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { footprintAabb, snapToGrid } from "./geometry.js";
|
|
2
2
|
import { validatePlacement } from "./placement.js";
|
|
3
3
|
const SNAP_ORDER = ["grid", "free", "surface"];
|
|
4
|
+
const DEFAULT_SLOT_RADIUS = 1.5;
|
|
5
|
+
function distanceVec3(a, b) {
|
|
6
|
+
return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
7
|
+
}
|
|
4
8
|
export function quarterTurnsToRotationY(quarterTurns) {
|
|
5
9
|
const turns = ((quarterTurns % 4) + 4) % 4;
|
|
6
10
|
return turns === 0 ? 0 : -turns * (Math.PI / 2);
|
|
@@ -11,6 +15,9 @@ export function createPlacementController(config) {
|
|
|
11
15
|
let snapMode = config.snapMode ?? "grid";
|
|
12
16
|
let grid = config.grid ?? rules.snap ?? 1;
|
|
13
17
|
let quarterTurns = ((config.quarterTurns ?? 0) % 4 + 4) % 4;
|
|
18
|
+
let slots = config.slots;
|
|
19
|
+
const slotRadius = config.slotRadius ?? DEFAULT_SLOT_RADIUS;
|
|
20
|
+
const occupiedSlots = new Set();
|
|
14
21
|
let lastHit = null;
|
|
15
22
|
let preview = null;
|
|
16
23
|
function snapCenter(raw) {
|
|
@@ -18,7 +25,40 @@ export function createPlacementController(config) {
|
|
|
18
25
|
return snapToGrid(raw, grid);
|
|
19
26
|
return raw;
|
|
20
27
|
}
|
|
28
|
+
function nearestSlot(hit) {
|
|
29
|
+
let best = null;
|
|
30
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
31
|
+
for (const slot of slots ?? []) {
|
|
32
|
+
if (slot.enabled === false || occupiedSlots.has(slot.id))
|
|
33
|
+
continue;
|
|
34
|
+
const distance = distanceVec3(hit.point, slot.center);
|
|
35
|
+
if (distance <= (slot.radius ?? slotRadius) && distance < bestDistance) {
|
|
36
|
+
best = slot;
|
|
37
|
+
bestDistance = distance;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return best;
|
|
41
|
+
}
|
|
42
|
+
function computeSlotPreview(hit) {
|
|
43
|
+
const slot = nearestSlot(hit);
|
|
44
|
+
const center = slot === null ? [hit.point[0], hit.point[2]] : [slot.center[0], slot.center[2]];
|
|
45
|
+
const next = {
|
|
46
|
+
center,
|
|
47
|
+
y: slot === null ? hit.point[1] : slot.center[1],
|
|
48
|
+
quarterTurns,
|
|
49
|
+
footprint,
|
|
50
|
+
aabb: footprintAabb(center, footprint, quarterTurns),
|
|
51
|
+
valid: slot !== null,
|
|
52
|
+
snapMode,
|
|
53
|
+
normal: hit.normal,
|
|
54
|
+
...(slot === null ? { reason: "no-slot" } : { slotId: slot.id }),
|
|
55
|
+
};
|
|
56
|
+
preview = next;
|
|
57
|
+
return next;
|
|
58
|
+
}
|
|
21
59
|
function compute(hit) {
|
|
60
|
+
if (slots !== undefined)
|
|
61
|
+
return computeSlotPreview(hit);
|
|
22
62
|
const raw = [hit.point[0], hit.point[2]];
|
|
23
63
|
const center = snapCenter(raw);
|
|
24
64
|
const rulesWithoutSnap = snapMode === "grid" ? { ...rules, snap: undefined } : rules;
|
|
@@ -76,9 +116,24 @@ export function createPlacementController(config) {
|
|
|
76
116
|
if (lastHit !== null)
|
|
77
117
|
compute(lastHit);
|
|
78
118
|
},
|
|
119
|
+
setSlots(next) {
|
|
120
|
+
slots = next;
|
|
121
|
+
if (lastHit !== null)
|
|
122
|
+
compute(lastHit);
|
|
123
|
+
},
|
|
124
|
+
hoveredSlotId() {
|
|
125
|
+
return preview?.slotId ?? null;
|
|
126
|
+
},
|
|
127
|
+
releaseSlot(id) {
|
|
128
|
+
occupiedSlots.delete(id);
|
|
129
|
+
if (lastHit !== null)
|
|
130
|
+
compute(lastHit);
|
|
131
|
+
},
|
|
79
132
|
commit() {
|
|
80
133
|
if (preview === null || !preview.valid)
|
|
81
134
|
return null;
|
|
135
|
+
if (preview.slotId !== undefined)
|
|
136
|
+
occupiedSlots.add(preview.slotId);
|
|
82
137
|
return {
|
|
83
138
|
center: preview.center,
|
|
84
139
|
y: preview.y,
|
|
@@ -86,6 +141,7 @@ export function createPlacementController(config) {
|
|
|
86
141
|
quarterTurns,
|
|
87
142
|
footprint: preview.footprint,
|
|
88
143
|
aabb: preview.aabb,
|
|
144
|
+
...(preview.slotId !== undefined ? { slotId: preview.slotId } : {}),
|
|
89
145
|
};
|
|
90
146
|
},
|
|
91
147
|
reset() {
|