@jgengine/core 0.13.0 → 0.15.0-next.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 +529 -1
- package/dist/ai/difficulty.d.ts +154 -0
- package/dist/ai/difficulty.js +194 -0
- package/dist/ai/driver.d.ts +107 -0
- package/dist/ai/driver.js +175 -0
- package/dist/ai/flock.js +13 -23
- package/dist/ai/laneSelect.js +2 -1
- package/dist/ai/mobBrain.js +2 -1
- package/dist/ai/waveRunner.d.ts +81 -0
- package/dist/ai/waveRunner.js +104 -0
- package/dist/authoring.d.ts +6 -4
- package/dist/authoring.js +6 -4
- package/dist/combat/castRunner.d.ts +13 -1
- package/dist/combat/castRunner.js +5 -1
- package/dist/combat/comboMeter.d.ts +140 -0
- package/dist/combat/comboMeter.js +215 -0
- package/dist/combat/comboPoints.d.ts +11 -0
- package/dist/combat/comboPoints.js +11 -2
- package/dist/combat/damageResolution.d.ts +1 -1
- package/dist/combat/defensiveWindow.d.ts +5 -0
- package/dist/combat/effects.d.ts +24 -0
- package/dist/combat/effects.js +6 -1
- package/dist/combat/projectiles.d.ts +6 -0
- package/dist/combat/projectiles.js +2 -2
- package/dist/combat/regenShield.d.ts +40 -2
- package/dist/combat/regenShield.js +66 -18
- package/dist/combat/resourcePool.d.ts +19 -0
- package/dist/combat/resourcePool.js +20 -2
- package/dist/combat/shotOrigin.d.ts +5 -0
- package/dist/combat/statusApplication.d.ts +1 -1
- package/dist/combat/statusApplication.js +2 -1
- package/dist/combat/statusEffectView.d.ts +56 -0
- package/dist/combat/statusEffectView.js +52 -0
- package/dist/combat.d.ts +4 -2
- package/dist/combat.js +2 -0
- package/dist/devtools/fallbackSeams.d.ts +3 -2
- package/dist/devtools/fallbackSeams.js +1 -1
- package/dist/devtools/textureErrors.d.ts +40 -0
- package/dist/devtools/textureErrors.js +68 -0
- package/dist/economy/shopStock.d.ts +110 -0
- package/dist/economy/shopStock.js +157 -0
- package/dist/economy/wallet.d.ts +6 -0
- package/dist/economy/wallet.js +1 -0
- package/dist/game/achievements.d.ts +80 -0
- package/dist/game/achievements.js +124 -0
- package/dist/game/cameraConfig.d.ts +7 -0
- package/dist/game/codex.d.ts +60 -0
- package/dist/game/codex.js +89 -0
- package/dist/game/credits.d.ts +36 -0
- package/dist/game/credits.js +0 -0
- package/dist/game/defineGame.d.ts +28 -3
- package/dist/game/defineGame.js +5 -0
- package/dist/game/dialogueGraph.d.ts +116 -0
- package/dist/game/dialogueGraph.js +103 -0
- package/dist/game/eventTicker.d.ts +95 -0
- package/dist/game/eventTicker.js +91 -0
- package/dist/game/events.d.ts +24 -5
- package/dist/game/events.js +18 -0
- package/dist/game/gamePhase.d.ts +1 -1
- package/dist/game/gamePhase.js +1 -1
- package/dist/game/leaderboardRank.d.ts +71 -0
- package/dist/game/leaderboardRank.js +76 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +4 -2
- package/dist/game/notifications.d.ts +57 -0
- package/dist/game/notifications.js +107 -0
- package/dist/game/playableGame.d.ts +39 -2
- package/dist/game/quest.d.ts +26 -0
- package/dist/game/quest.js +31 -0
- package/dist/game/runDraft.js +2 -1
- package/dist/game/saveSlots.d.ts +102 -0
- package/dist/game/saveSlots.js +136 -0
- package/dist/game/talentTreeView.d.ts +106 -0
- package/dist/game/talentTreeView.js +114 -0
- package/dist/game/worldItem.js +2 -1
- package/dist/gameplay.d.ts +15 -4
- package/dist/gameplay.js +13 -2
- package/dist/i18n/i18n.d.ts +51 -0
- package/dist/i18n/i18n.js +82 -0
- package/dist/input/axisInput.d.ts +1 -1
- package/dist/input/axisInput.js +18 -2
- package/dist/input/rebindSession.d.ts +141 -0
- package/dist/input/rebindSession.js +235 -0
- package/dist/input/touchControlsMode.d.ts +16 -0
- package/dist/input/touchControlsMode.js +23 -0
- package/dist/input/touchScheme.d.ts +30 -1
- package/dist/input/touchScheme.js +31 -1
- package/dist/interaction/promptRegistry.d.ts +78 -0
- package/dist/interaction/promptRegistry.js +130 -0
- package/dist/inventory/storageTier.js +3 -2
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +21 -1
- package/dist/movement/movementModel.d.ts +26 -3
- package/dist/movement/movementModel.js +121 -15
- package/dist/movement/playerMovement.js +161 -25
- package/dist/physics/drivableVehicle.js +3 -1
- package/dist/physics/kinematicVehicle.d.ts +56 -1
- package/dist/physics/kinematicVehicle.js +99 -10
- package/dist/physics/vehicleObstacles.js +19 -0
- package/dist/random/resolveRng.d.ts +14 -0
- package/dist/random/resolveRng.js +31 -0
- package/dist/render/postProcessing.d.ts +12 -1
- package/dist/runtime/cameraDirector.d.ts +2 -2
- package/dist/runtime/context/combat.js +8 -0
- package/dist/runtime/context/scene.d.ts +2 -0
- package/dist/runtime/context/scene.js +48 -1
- package/dist/runtime/gameContext.js +8 -2
- package/dist/runtime/gameContextTypes.d.ts +39 -0
- package/dist/runtime/inputSnapshot.d.ts +10 -1
- package/dist/runtime/inputSnapshot.js +13 -1
- package/dist/runtime/runtimeSave.d.ts +3 -3
- package/dist/runtime/runtimeSave.js +11 -2
- package/dist/scene/assetDiagnostics.js +2 -2
- package/dist/scene/autoTarget.js +2 -1
- package/dist/scene/builtinSceneKinds.d.ts +6 -1
- package/dist/scene/builtinSceneKinds.js +10 -3
- package/dist/scene/captureCheck.js +3 -2
- package/dist/scene/colliders.d.ts +20 -0
- package/dist/scene/colliders.js +78 -0
- package/dist/scene/collisionMesh.d.ts +8 -0
- package/dist/scene/collisionMesh.js +48 -0
- package/dist/scene/entityStore.d.ts +4 -1
- package/dist/scene/entityStore.js +3 -0
- package/dist/scene/objectStore.d.ts +10 -0
- package/dist/scene/objectStore.js +29 -6
- package/dist/scene/sceneKinds.d.ts +37 -0
- package/dist/scene/sceneKinds.js +19 -0
- package/dist/scene/sequenceDirector.d.ts +110 -0
- package/dist/scene/sequenceDirector.js +146 -0
- package/dist/session/roles.js +3 -2
- package/dist/settings/settingsModel.d.ts +1 -0
- package/dist/settings/settingsModel.js +1 -0
- package/dist/stats/rollCheck.js +4 -2
- package/dist/time/dayNightCycle.d.ts +114 -0
- package/dist/time/dayNightCycle.js +208 -0
- package/dist/time/timerSet.d.ts +132 -0
- package/dist/time/timerSet.js +243 -0
- package/dist/ui/accessibility.d.ts +51 -0
- package/dist/ui/accessibility.js +83 -0
- package/dist/ui/coachMarks.d.ts +99 -0
- package/dist/ui/coachMarks.js +126 -0
- package/dist/ui/floatingText.d.ts +114 -0
- package/dist/ui/floatingText.js +203 -0
- package/dist/ui/hudScale.d.ts +16 -1
- package/dist/ui/hudScale.js +22 -1
- package/dist/ui/modalStack.d.ts +143 -0
- package/dist/ui/modalStack.js +160 -0
- package/dist/ui/objectiveBanner.d.ts +123 -0
- package/dist/ui/objectiveBanner.js +162 -0
- package/dist/ui/photoMode.d.ts +32 -0
- package/dist/ui/photoMode.js +51 -0
- package/dist/ui/radialMenu.d.ts +47 -0
- package/dist/ui/radialMenu.js +74 -0
- package/dist/ui/screenMarkers.d.ts +128 -0
- package/dist/ui/screenMarkers.js +118 -0
- package/dist/ui.d.ts +8 -0
- package/dist/ui.js +8 -0
- package/dist/vfx/cameraShake.d.ts +97 -0
- package/dist/vfx/cameraShake.js +136 -0
- package/dist/vfx/damageDirection.d.ts +102 -0
- package/dist/vfx/damageDirection.js +188 -0
- package/dist/vfx/particles.d.ts +103 -0
- package/dist/vfx/particles.js +256 -0
- package/dist/vfx/screenEffects.d.ts +149 -0
- package/dist/vfx/screenEffects.js +191 -0
- package/dist/world/authoredObjects.d.ts +18 -0
- package/dist/world/authoredObjects.js +21 -0
- package/dist/world/authoredSpawn.js +19 -0
- package/dist/world/buildingLots.d.ts +54 -11
- package/dist/world/buildingLots.js +116 -30
- package/dist/world/cityBlocks.d.ts +45 -0
- package/dist/world/cityBlocks.js +159 -0
- package/dist/world/cityContent.d.ts +46 -2
- package/dist/world/cityContent.js +160 -2
- package/dist/world/cityGenerator.d.ts +261 -23
- package/dist/world/cityGenerator.js +643 -33
- package/dist/world/cityGeometry.d.ts +20 -0
- package/dist/world/cityGeometry.js +91 -0
- package/dist/world/cityKind.js +9 -46
- package/dist/world/fastTravel.d.ts +64 -0
- package/dist/world/fastTravel.js +108 -0
- package/dist/world/features.d.ts +16 -0
- package/dist/world/features.js +2 -0
- package/dist/world/grassKind.js +4 -1
- package/dist/world/grounded.d.ts +11 -0
- package/dist/world/grounded.js +12 -0
- package/dist/world/mapAnnotations.d.ts +75 -0
- package/dist/world/mapAnnotations.js +139 -0
- package/dist/world/markers.js +1 -0
- package/dist/world/resourceNode.d.ts +1 -1
- package/dist/world/resourceNode.js +2 -1
- package/dist/world/roads.d.ts +294 -5
- package/dist/world/roads.js +1262 -21
- package/dist/world/scatterCoverage.d.ts +98 -0
- package/dist/world/scatterCoverage.js +101 -0
- package/dist/world/scatterRegion.js +6 -0
- package/dist/world/sharedAnnotations.d.ts +84 -0
- package/dist/world/sharedAnnotations.js +76 -0
- package/dist/world/spawnOverride.d.ts +37 -0
- package/dist/world/spawnOverride.js +50 -0
- package/dist/world/streetGenerator.d.ts +60 -11
- package/dist/world/streetGenerator.js +1786 -129
- package/dist/world/terrain.d.ts +1 -0
- package/dist/world/terrain.js +2 -0
- package/dist/world/waypoints.d.ts +63 -0
- package/dist/world/waypoints.js +137 -0
- package/dist/world.d.ts +18 -2
- package/dist/world.js +18 -2
- package/package.json +2 -2
- package/skills/ce-handoff/SKILL.md +149 -0
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +7 -3
- package/skills/jgengine/api.md +17 -7
- package/skills/jgengine/capabilities.md +12 -0
- package/skills/jgengine/recipes/minimal-game.md +7 -4
- package/skills/jgengine-combat/api.md +40 -4
- package/skills/jgengine-combat/capabilities.md +12 -0
- package/skills/jgengine-combat/reference.md +2 -0
- package/skills/jgengine-gameplay/api.md +212 -11
- package/skills/jgengine-gameplay/capabilities.md +66 -1
- package/skills/jgengine-world/api.md +408 -12
- package/skills/jgengine-world/capabilities.md +127 -2
- package/skills/jgengine-world/reference.md +15 -1
|
@@ -12,7 +12,7 @@ export interface RuntimeSaveTarget {
|
|
|
12
12
|
hydrate(snapshot: WorldSnapshot): void;
|
|
13
13
|
subscribe(listener: () => void): () => void;
|
|
14
14
|
}
|
|
15
|
-
/** `"autosave"` writes a fresh capture on a
|
|
15
|
+
/** `"autosave"` writes a fresh capture on a trailing timer while the world changes (at most once per `autosaveMs`, so a never-idle world still persists); `"manual"` writes only on an explicit `save()`/`checkpoint()` — the save-point / quest-trigger model. */
|
|
16
16
|
export type RuntimeSaveMode = "autosave" | "manual";
|
|
17
17
|
/** How {@link createRuntimeSave} is wired — the live world `target`, the `backend` it persists through, and optional mode/slot/versioning/cadence knobs. */
|
|
18
18
|
export interface RuntimeSaveConfig {
|
|
@@ -29,7 +29,7 @@ export interface RuntimeSaveConfig {
|
|
|
29
29
|
/** Save-format version; bump + pass {@link migrate} on a breaking world-shape change. */
|
|
30
30
|
version?: number;
|
|
31
31
|
migrate?: (data: unknown, fromVersion: number) => WorldSnapshot;
|
|
32
|
-
/**
|
|
32
|
+
/** Trailing autosave interval (ms) for `autosave` mode — the world persists at most once per interval and always within one interval of its first unsaved change. Default `3000`. */
|
|
33
33
|
autosaveMs?: number;
|
|
34
34
|
now?: () => number;
|
|
35
35
|
timers?: SaveTimers;
|
|
@@ -41,7 +41,7 @@ export type RuntimeSaveOptions = Omit<RuntimeSaveConfig, "target">;
|
|
|
41
41
|
* Whole-world save/load bound to a live world and a pluggable backend. `save()`
|
|
42
42
|
* captures `target.snapshot()` and writes it; `load()` reads it back and
|
|
43
43
|
* `target.hydrate()`s the whole world. In `autosave` mode it also writes on a
|
|
44
|
-
*
|
|
44
|
+
* trailing timer while the world keeps changing. Named slots, versioned migration, and offline↔cloud
|
|
45
45
|
* (backend swap) all come for free from the underlying save store.
|
|
46
46
|
*/
|
|
47
47
|
export interface RuntimeSave {
|
|
@@ -51,10 +51,19 @@ export function createRuntimeSave(config) {
|
|
|
51
51
|
store.set(config.target.snapshot());
|
|
52
52
|
return store.save();
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Arm a trailing autosave. A living world (AI, physics, the day/night clock)
|
|
56
|
+
* changes every frame, so a debounce that *reset* its timer on every change
|
|
57
|
+
* would never elapse and the game would never persist — the timer would be
|
|
58
|
+
* pushed forward faster than it could fire. Instead the first change since the
|
|
59
|
+
* last write arms a single timer and later changes ride that same timer; when
|
|
60
|
+
* it fires it captures the latest snapshot. The result is at most one write per
|
|
61
|
+
* `autosaveMs`, and always within `autosaveMs` of the first unsaved change,
|
|
62
|
+
* even while the world never stops moving.
|
|
63
|
+
*/
|
|
54
64
|
function scheduleAutosave() {
|
|
55
|
-
if (disposed || restoring)
|
|
65
|
+
if (disposed || restoring || pendingTimer !== null)
|
|
56
66
|
return;
|
|
57
|
-
clearTimer();
|
|
58
67
|
pendingTimer = timers.set(() => {
|
|
59
68
|
pendingTimer = null;
|
|
60
69
|
void persist();
|
|
@@ -92,11 +92,11 @@ export function classifyAssetResponse(probe) {
|
|
|
92
92
|
});
|
|
93
93
|
if (status !== undefined && (status === 0 || status >= 400)) {
|
|
94
94
|
const suffix = statusText === undefined || statusText.length === 0 ? "" : ` ${statusText}`;
|
|
95
|
-
return finish("missing", `Asset ${who} did not load: HTTP ${status}${suffix}. The
|
|
95
|
+
return finish("missing", `Asset ${who} did not load: HTTP ${status}${suffix}. The bytes are not being served at that path — run \`assets pull <source>\`, which provisions the pack into the dev server's served models dir (\`apps/dev/public/models\` in the jgengine monorepo) so no manual copy is needed; or check the model path.`);
|
|
96
96
|
}
|
|
97
97
|
const htmlContentType = contentType !== undefined && contentType.toLowerCase().includes("text/html");
|
|
98
98
|
if (signature === "html" || (htmlContentType && signature !== "glb" && signature !== "gltf-json")) {
|
|
99
|
-
return finish("html", `Asset ${who} resolved to an HTML page, not a model — the dev server returned its fallback document because the file is missing.
|
|
99
|
+
return finish("html", `Asset ${who} resolved to an HTML page, not a model — the dev server returned its fallback document because the file is missing. Run \`assets pull <source>\` to provision the pack into the served models dir (\`apps/dev/public/models\` in the jgengine monorepo); the bytes must exist there, no manual copy needed.`);
|
|
100
100
|
}
|
|
101
101
|
if (signature === "glb" || signature === "gltf-json")
|
|
102
102
|
return finish("ok", `Asset ${who} loaded (${signature}).`);
|
package/dist/scene/autoTarget.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveRng } from "../random/resolveRng.js";
|
|
1
2
|
function metric(policy, fromId, toId, deps) {
|
|
2
3
|
switch (policy) {
|
|
3
4
|
case "nearest":
|
|
@@ -21,7 +22,7 @@ export function selectAutoTarget(policy, fromId, deps) {
|
|
|
21
22
|
if (candidates.length === 0)
|
|
22
23
|
return null;
|
|
23
24
|
if (policy === "random") {
|
|
24
|
-
const rng = deps.rng
|
|
25
|
+
const rng = resolveRng(deps.rng, "autoTarget");
|
|
25
26
|
return candidates[Math.min(candidates.length - 1, Math.floor(rng() * candidates.length))];
|
|
26
27
|
}
|
|
27
28
|
const higher = prefersHigher(policy);
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { type ParamSchema } from "./sceneKinds.js";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* The scatter/foliage region schema. `palette` (what fills the area) leads the schema body and
|
|
4
|
+
* `density` is the single coverage slider hoisted into the shared `CoverageSection` (see the kind's
|
|
5
|
+
* `coverage` descriptor), so a scatter region reads as Area → Assets → Density like every other
|
|
6
|
+
* scatterable kind; the rest is kind-specific tuning.
|
|
7
|
+
*/
|
|
3
8
|
export declare const SCATTER_SCHEMA: ParamSchema;
|
|
4
9
|
/** Idempotently registers the engine's built-in environment scene kinds + generators. @internal */
|
|
5
10
|
export declare function registerBuiltinSceneKinds(): void;
|
|
@@ -15,10 +15,17 @@ import { registerSoilKind } from "../world/soilKind.js";
|
|
|
15
15
|
import { registerPoleLineKind } from "../world/poleLineKind.js";
|
|
16
16
|
import { registerBuildingGenerator } from "../world/buildingGenerator.js";
|
|
17
17
|
import { registerCityKind } from "../world/cityKind.js";
|
|
18
|
+
import { describeScatterCoverage, densityCoverage } from "../world/scatterCoverage.js";
|
|
18
19
|
import { registerSceneKind } from "./sceneKinds.js";
|
|
19
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* The scatter/foliage region schema. `palette` (what fills the area) leads the schema body and
|
|
22
|
+
* `density` is the single coverage slider hoisted into the shared `CoverageSection` (see the kind's
|
|
23
|
+
* `coverage` descriptor), so a scatter region reads as Area → Assets → Density like every other
|
|
24
|
+
* scatterable kind; the rest is kind-specific tuning.
|
|
25
|
+
*/
|
|
20
26
|
export const SCATTER_SCHEMA = {
|
|
21
27
|
fields: [
|
|
28
|
+
{ type: "weightedList", key: "palette", label: "species (weighted)", itemLabel: "grass / tree id", default: [{ item: "grass", weight: 1 }] },
|
|
22
29
|
{ type: "range", key: "density", label: "density", min: 0, max: 2, step: 0.01, default: 0.15, unit: "/m²" },
|
|
23
30
|
{ type: "number", key: "minSpacing", label: "spacing", step: 0.25, default: 1.5, min: 0 },
|
|
24
31
|
{ type: "number", key: "minScale", label: "min scale", step: 0.05, default: 0.8, min: 0 },
|
|
@@ -27,7 +34,6 @@ export const SCATTER_SCHEMA = {
|
|
|
27
34
|
{ type: "number", key: "edgeFalloff", label: "edge fade", step: 0.5, default: 0, min: 0 },
|
|
28
35
|
{ type: "bool", key: "alignToNormal", label: "align to slope", default: false },
|
|
29
36
|
{ type: "bool", key: "autoAvoid", label: "auto-avoid gameplay spots", default: true },
|
|
30
|
-
{ type: "weightedList", key: "palette", label: "species (weighted)", itemLabel: "grass / tree id", default: [{ item: "grass", weight: 1 }] },
|
|
31
37
|
{ type: "seed", key: "seed", label: "seed", default: "" },
|
|
32
38
|
],
|
|
33
39
|
};
|
|
@@ -45,6 +51,7 @@ export function registerBuiltinSceneKinds() {
|
|
|
45
51
|
addCategory: "Studios",
|
|
46
52
|
accent: "#34d399",
|
|
47
53
|
schema: SCATTER_SCHEMA,
|
|
54
|
+
coverage: { spec: "scatter", densityKey: "density", assetsKey: "palette" },
|
|
48
55
|
note: (object, params) => {
|
|
49
56
|
const points = object.points ?? [];
|
|
50
57
|
if (points.length < 3)
|
|
@@ -55,7 +62,7 @@ export function registerBuiltinSceneKinds() {
|
|
|
55
62
|
}
|
|
56
63
|
area = Math.abs(area) / 2;
|
|
57
64
|
const density = typeof params["density"] === "number" ? params["density"] : 0;
|
|
58
|
-
return
|
|
65
|
+
return describeScatterCoverage(densityCoverage("scatter", area, density));
|
|
59
66
|
},
|
|
60
67
|
});
|
|
61
68
|
registerWaterKind();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveRng } from "../random/resolveRng.js";
|
|
1
2
|
function clamp01(value) {
|
|
2
3
|
return Math.min(1, Math.max(0, value));
|
|
3
4
|
}
|
|
@@ -6,6 +7,6 @@ export function captureChance(input) {
|
|
|
6
7
|
const hpFactor = 1 - clamp01(input.hpFraction) * 0.6;
|
|
7
8
|
return clamp01((input.catchPower * hpFactor) / Math.max(difficulty, 0.0001));
|
|
8
9
|
}
|
|
9
|
-
export function rollCapture(input, rng
|
|
10
|
-
return rng() < captureChance(input);
|
|
10
|
+
export function rollCapture(input, rng) {
|
|
11
|
+
return resolveRng(rng, "rollCapture")() < captureChance(input);
|
|
11
12
|
}
|
|
@@ -128,6 +128,26 @@ export declare function measuredEntityColliders(bounds: MeasuredBounds): EntityC
|
|
|
128
128
|
* @internal
|
|
129
129
|
*/
|
|
130
130
|
export declare function measuredObjectColliders(bounds: MeasuredBounds): EntityColliderSet | null;
|
|
131
|
+
/** Damage hitbox raycasting the actual triangles the renderer mounted for an entity kind — what
|
|
132
|
+
* `reportCollisionMesh` stores. Triangles are entity-local (scale/placement already applied), so the
|
|
133
|
+
* shape composes with identity `meshScale`/`meshTranslate`; bounds/broadphase read the clamped AABB.
|
|
134
|
+
* `null` when the mesh is degenerate, keeping the caller's existing fallback.
|
|
135
|
+
* @capability conforming-hitboxes shots aimed between a character's feet or past its shoulder MISS — the damage hitbox is the rendered model's own triangles, not a bounding box.
|
|
136
|
+
* @internal
|
|
137
|
+
*/
|
|
138
|
+
export declare function meshEntityColliders(mesh: PreparedCollisionMesh): EntityColliderSet | null;
|
|
139
|
+
/** Blocking physical body raycasting the actual triangles the renderer mounted for an object catalog
|
|
140
|
+
* id — the mesh-accurate counterpart to `measuredObjectColliders`. Movement obstruction stays on the
|
|
141
|
+
* conservative AABB (no `boxes` decomposition at runtime); rays hit the real surface. `null` when
|
|
142
|
+
* degenerate.
|
|
143
|
+
* @internal
|
|
144
|
+
*/
|
|
145
|
+
export declare function meshObjectColliders(mesh: PreparedCollisionMesh): EntityColliderSet | null;
|
|
146
|
+
/** True when any collider in the set already raycasts real triangles (index-derived mesh shapes win
|
|
147
|
+
* over runtime-measured ones — they are identical on a headless host).
|
|
148
|
+
* @internal
|
|
149
|
+
*/
|
|
150
|
+
export declare function hasMeshColliderShape(set: EntityColliderSet | null | undefined): boolean;
|
|
131
151
|
/** @internal */
|
|
132
152
|
export declare function resolveColliders(set: EntityColliderSet | null | undefined): ResolvedCollider[];
|
|
133
153
|
/** @internal */
|
package/dist/scene/colliders.js
CHANGED
|
@@ -250,6 +250,84 @@ export function measuredObjectColliders(bounds) {
|
|
|
250
250
|
},
|
|
251
251
|
};
|
|
252
252
|
}
|
|
253
|
+
/** Root-node AABB of a prepared mesh (the BVH root bounds the whole soup) as a clamped
|
|
254
|
+
* entity-local box — `null` when degenerate. */
|
|
255
|
+
function preparedMeshShape(mesh) {
|
|
256
|
+
const bounds = mesh.nodeBounds;
|
|
257
|
+
if (bounds.length < 6)
|
|
258
|
+
return null;
|
|
259
|
+
const min = [bounds[0], bounds[1], bounds[2]];
|
|
260
|
+
const max = [bounds[3], bounds[4], bounds[5]];
|
|
261
|
+
const box = measuredBox({ min, max });
|
|
262
|
+
if (box === null)
|
|
263
|
+
return null;
|
|
264
|
+
return {
|
|
265
|
+
kind: "mesh",
|
|
266
|
+
mesh,
|
|
267
|
+
meshScale: 1,
|
|
268
|
+
meshTranslate: [0, 0, 0],
|
|
269
|
+
halfExtents: box.halfExtents,
|
|
270
|
+
offset: box.offset,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/** Damage hitbox raycasting the actual triangles the renderer mounted for an entity kind — what
|
|
274
|
+
* `reportCollisionMesh` stores. Triangles are entity-local (scale/placement already applied), so the
|
|
275
|
+
* shape composes with identity `meshScale`/`meshTranslate`; bounds/broadphase read the clamped AABB.
|
|
276
|
+
* `null` when the mesh is degenerate, keeping the caller's existing fallback.
|
|
277
|
+
* @capability conforming-hitboxes shots aimed between a character's feet or past its shoulder MISS — the damage hitbox is the rendered model's own triangles, not a bounding box.
|
|
278
|
+
* @internal
|
|
279
|
+
*/
|
|
280
|
+
export function meshEntityColliders(mesh) {
|
|
281
|
+
const shape = preparedMeshShape(mesh);
|
|
282
|
+
if (shape === null)
|
|
283
|
+
return null;
|
|
284
|
+
return {
|
|
285
|
+
hitboxes: [
|
|
286
|
+
{
|
|
287
|
+
name: "body",
|
|
288
|
+
purpose: "damage",
|
|
289
|
+
shape,
|
|
290
|
+
damageEligible: true,
|
|
291
|
+
blocks: false,
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/** Blocking physical body raycasting the actual triangles the renderer mounted for an object catalog
|
|
297
|
+
* id — the mesh-accurate counterpart to `measuredObjectColliders`. Movement obstruction stays on the
|
|
298
|
+
* conservative AABB (no `boxes` decomposition at runtime); rays hit the real surface. `null` when
|
|
299
|
+
* degenerate.
|
|
300
|
+
* @internal
|
|
301
|
+
*/
|
|
302
|
+
export function meshObjectColliders(mesh) {
|
|
303
|
+
const shape = preparedMeshShape(mesh);
|
|
304
|
+
if (shape === null)
|
|
305
|
+
return null;
|
|
306
|
+
return {
|
|
307
|
+
body: {
|
|
308
|
+
name: "body",
|
|
309
|
+
purpose: "physical",
|
|
310
|
+
shape,
|
|
311
|
+
damageEligible: false,
|
|
312
|
+
blocks: true,
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/** True when any collider in the set already raycasts real triangles (index-derived mesh shapes win
|
|
317
|
+
* over runtime-measured ones — they are identical on a headless host).
|
|
318
|
+
* @internal
|
|
319
|
+
*/
|
|
320
|
+
export function hasMeshColliderShape(set) {
|
|
321
|
+
if (set === null || set === undefined)
|
|
322
|
+
return false;
|
|
323
|
+
if (set.body?.shape.kind === "mesh")
|
|
324
|
+
return true;
|
|
325
|
+
for (const hitbox of set.hitboxes ?? []) {
|
|
326
|
+
if (hitbox.shape.kind === "mesh")
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
253
331
|
/** @internal */
|
|
254
332
|
export function resolveColliders(set) {
|
|
255
333
|
if (set === null || set === undefined)
|
|
@@ -77,6 +77,14 @@ export declare function voxelizeToBoxes(source: CollisionMeshSource, cellSize?:
|
|
|
77
77
|
min: [number, number, number];
|
|
78
78
|
max: [number, number, number];
|
|
79
79
|
}[];
|
|
80
|
+
/**
|
|
81
|
+
* Build a prepared BVH mesh straight from a runtime triangle soup (float positions + indices) —
|
|
82
|
+
* the renderer-fed counterpart to {@link prepareCollisionMesh}, feeding
|
|
83
|
+
* `ctx.scene.entity.reportCollisionMesh` / `ctx.scene.object.reportCollisionMesh` without a
|
|
84
|
+
* quantize/encode round-trip. Copies and sanitizes the input: triangles referencing out-of-range,
|
|
85
|
+
* non-finite, or repeated vertices are dropped. Returns `null` when no triangle survives.
|
|
86
|
+
*/
|
|
87
|
+
export declare function prepareCollisionMeshSource(source: CollisionMeshSource): PreparedCollisionMesh | null;
|
|
80
88
|
/**
|
|
81
89
|
* Decode a stored collision mesh and index it with a BVH. Memoized per data object, so the shell and
|
|
82
90
|
* a headless host preparing the same catalog entry share one build. Returns `null` (also memoized)
|
|
@@ -500,6 +500,54 @@ function buildBvh(positions, indices) {
|
|
|
500
500
|
triOrder,
|
|
501
501
|
};
|
|
502
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* Build a prepared BVH mesh straight from a runtime triangle soup (float positions + indices) —
|
|
505
|
+
* the renderer-fed counterpart to {@link prepareCollisionMesh}, feeding
|
|
506
|
+
* `ctx.scene.entity.reportCollisionMesh` / `ctx.scene.object.reportCollisionMesh` without a
|
|
507
|
+
* quantize/encode round-trip. Copies and sanitizes the input: triangles referencing out-of-range,
|
|
508
|
+
* non-finite, or repeated vertices are dropped. Returns `null` when no triangle survives.
|
|
509
|
+
*/
|
|
510
|
+
export function prepareCollisionMeshSource(source) {
|
|
511
|
+
const raw = source.positions;
|
|
512
|
+
const rawIndices = source.indices;
|
|
513
|
+
if (raw.length < 9 || raw.length % 3 !== 0)
|
|
514
|
+
return null;
|
|
515
|
+
if (rawIndices.length < 3 || rawIndices.length % 3 !== 0)
|
|
516
|
+
return null;
|
|
517
|
+
const vertexCount = raw.length / 3;
|
|
518
|
+
const positions = new Float32Array(raw.length);
|
|
519
|
+
const finite = new Uint8Array(vertexCount);
|
|
520
|
+
for (let v = 0; v < vertexCount; v += 1) {
|
|
521
|
+
const x = raw[v * 3];
|
|
522
|
+
const y = raw[v * 3 + 1];
|
|
523
|
+
const z = raw[v * 3 + 2];
|
|
524
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z))
|
|
525
|
+
continue;
|
|
526
|
+
positions[v * 3] = x;
|
|
527
|
+
positions[v * 3 + 1] = y;
|
|
528
|
+
positions[v * 3 + 2] = z;
|
|
529
|
+
finite[v] = 1;
|
|
530
|
+
}
|
|
531
|
+
const triangles = [];
|
|
532
|
+
for (let t = 0; t + 2 < rawIndices.length; t += 3) {
|
|
533
|
+
const a = rawIndices[t];
|
|
534
|
+
const b = rawIndices[t + 1];
|
|
535
|
+
const c = rawIndices[t + 2];
|
|
536
|
+
if (!Number.isInteger(a) || a < 0 || a >= vertexCount || finite[a] === 0)
|
|
537
|
+
continue;
|
|
538
|
+
if (!Number.isInteger(b) || b < 0 || b >= vertexCount || finite[b] === 0)
|
|
539
|
+
continue;
|
|
540
|
+
if (!Number.isInteger(c) || c < 0 || c >= vertexCount || finite[c] === 0)
|
|
541
|
+
continue;
|
|
542
|
+
if (a === b || b === c || a === c)
|
|
543
|
+
continue;
|
|
544
|
+
triangles.push(a, b, c);
|
|
545
|
+
}
|
|
546
|
+
if (triangles.length === 0)
|
|
547
|
+
return null;
|
|
548
|
+
const indices = new Uint32Array(triangles);
|
|
549
|
+
return { positions, indices, ...buildBvh(positions, indices) };
|
|
550
|
+
}
|
|
503
551
|
const preparedCache = new WeakMap();
|
|
504
552
|
/**
|
|
505
553
|
* Decode a stored collision mesh and index it with a BVH. Memoized per data object, so the shell and
|
|
@@ -22,6 +22,8 @@ export interface SceneEntity<TMeta = unknown> {
|
|
|
22
22
|
role: EntityRole;
|
|
23
23
|
movement: EntityMovement;
|
|
24
24
|
behaviors: readonly BehaviorDescriptor[];
|
|
25
|
+
/** Render-only visibility: `true` skips the entity's visual entirely (seated vehicle riders, cutscene actors, stealth cloaks) while the entity keeps simulating — position, stats, AI targeting, and queries are untouched. Absent/`false` renders normally. */
|
|
26
|
+
hidden?: boolean;
|
|
25
27
|
/** Game-defined per-instance data set at `spawn`/`update` and carried to `renderEntity`/queries — narrow with {@link entityMetaOf} (or a type guard) on read (#286.1); never encode state in the id/name string. */
|
|
26
28
|
meta: TMeta;
|
|
27
29
|
}
|
|
@@ -44,6 +46,7 @@ export interface SpawnOptions<TMeta = unknown> {
|
|
|
44
46
|
role?: EntityRole;
|
|
45
47
|
movement?: EntityMovement;
|
|
46
48
|
behaviors?: readonly BehaviorDescriptor[];
|
|
49
|
+
hidden?: boolean;
|
|
47
50
|
meta?: TMeta;
|
|
48
51
|
/** When `id` is already spawned: `"throw"` (default), `"replace"` (fresh spawn over it — remount-safe world setup, #284.10), or `"keep"` (leave it untouched and return the id). */
|
|
49
52
|
onExisting?: "throw" | "replace" | "keep";
|
|
@@ -68,7 +71,7 @@ export interface PoseConstraintFrame {
|
|
|
68
71
|
}
|
|
69
72
|
/** Return a replacement position to constrain the step, or nothing to accept it. */
|
|
70
73
|
export type PoseConstraint = (frame: PoseConstraintFrame) => readonly [number, number, number] | undefined | void;
|
|
71
|
-
export type EntityUpdatePatch<TMeta = unknown> = Partial<Pick<SceneEntity<TMeta>, "name" | "rotationY" | "rotationX" | "rotationZ" | "role" | "movement" | "behaviors" | "meta">> & {
|
|
74
|
+
export type EntityUpdatePatch<TMeta = unknown> = Partial<Pick<SceneEntity<TMeta>, "name" | "rotationY" | "rotationX" | "rotationZ" | "role" | "movement" | "behaviors" | "hidden" | "meta">> & {
|
|
72
75
|
/** Accepts the same friendly shapes as `spawn`/`setPose` (#286.13). Raw patch semantics — velocity is not derived; use `setPose` with `dt` for that. */
|
|
73
76
|
position?: SpawnPositionInput;
|
|
74
77
|
};
|
|
@@ -91,6 +91,7 @@ export function createEntityStore() {
|
|
|
91
91
|
role: options.role ?? "prop",
|
|
92
92
|
movement: options.movement ?? {},
|
|
93
93
|
behaviors: options.behaviors ?? [],
|
|
94
|
+
...(options.hidden === undefined ? {} : { hidden: options.hidden }),
|
|
94
95
|
meta: options.meta,
|
|
95
96
|
});
|
|
96
97
|
spawnPoses.set(id, { position, rotationY });
|
|
@@ -124,6 +125,8 @@ export function createEntityStore() {
|
|
|
124
125
|
current.movement = patch.movement;
|
|
125
126
|
if (patch.behaviors !== undefined)
|
|
126
127
|
current.behaviors = patch.behaviors;
|
|
128
|
+
if (patch.hidden !== undefined)
|
|
129
|
+
current.hidden = patch.hidden;
|
|
127
130
|
if (patch.meta !== undefined)
|
|
128
131
|
current.meta = patch.meta;
|
|
129
132
|
store.set(id, current);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ModelAnimationConfig } from "../game/playableGame.js";
|
|
1
2
|
import type { EntityPosition } from "./entityStore.js";
|
|
2
3
|
export interface ObjectVisual {
|
|
3
4
|
scale?: number | readonly [number, number, number];
|
|
@@ -11,12 +12,21 @@ export interface SceneObject {
|
|
|
11
12
|
rotationY: number;
|
|
12
13
|
parentSpace?: string;
|
|
13
14
|
visual?: ObjectVisual;
|
|
15
|
+
/**
|
|
16
|
+
* Per-placement rig animation override applied on top of catalog resolution — the same shape as
|
|
17
|
+
* `ModelConfig.animation` (role→clip states, `"auto"`/`"none"`, speeds, fade, one-shot bindings).
|
|
18
|
+
* Authored in the editor as `marker.meta.animation` (#1274) and threaded here by
|
|
19
|
+
* `resolveAuthoredObjects` → `placeAuthoredObjects`; `undefined` keeps the catalog-resolved default.
|
|
20
|
+
*/
|
|
21
|
+
animation?: ModelAnimationConfig | "auto" | "none";
|
|
14
22
|
}
|
|
15
23
|
export interface PlaceOptions {
|
|
16
24
|
instanceId?: string;
|
|
17
25
|
parentSpace?: string;
|
|
18
26
|
rotation?: number;
|
|
19
27
|
visual?: ObjectVisual;
|
|
28
|
+
/** Per-placement rig animation override stored on the placed {@link SceneObject}; same shape as `ModelConfig.animation`. Omit to keep the catalog-resolved default. */
|
|
29
|
+
animation?: ModelAnimationConfig | "auto" | "none";
|
|
20
30
|
/** When `instanceId` is already placed: `"throw"` (default), `"replace"` (fresh placement over it — remount-safe world setup, #284.10), or `"keep"` (leave it untouched and return the id). */
|
|
21
31
|
onExisting?: "throw" | "replace" | "keep";
|
|
22
32
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
|
|
2
2
|
const CELL_SIZE = 1;
|
|
3
|
+
/**
|
|
4
|
+
* When an `inBox` query spans more than this many 1 m cells, walk the object list instead of the
|
|
5
|
+
* cell grid. Tall/wide colliders (city buildings) can inflate movement broadphase to hundreds of
|
|
6
|
+
* metres on each axis — at 1 m cells that is O(10⁶) empty `Map.get`s per frame and freezes `pose`.
|
|
7
|
+
* Linear scan over a few hundred objects is cheaper the moment the cell volume exceeds the scene.
|
|
8
|
+
*/
|
|
9
|
+
const IN_BOX_CELL_VOLUME_LIMIT = 4096;
|
|
3
10
|
function cellIndexOf(value) {
|
|
4
11
|
return Math.floor(value / CELL_SIZE);
|
|
5
12
|
}
|
|
@@ -9,6 +16,10 @@ function cellKey(cx, cy, cz) {
|
|
|
9
16
|
function cellKeyOf(position) {
|
|
10
17
|
return cellKey(cellIndexOf(position[0]), cellIndexOf(position[1]), cellIndexOf(position[2]));
|
|
11
18
|
}
|
|
19
|
+
function objectInAabb(object, min, max) {
|
|
20
|
+
const [x, y, z] = object.position;
|
|
21
|
+
return x >= min[0] && x <= max[0] && y >= min[1] && y <= max[1] && z >= min[2] && z <= max[2];
|
|
22
|
+
}
|
|
12
23
|
export function createObjectStore() {
|
|
13
24
|
const store = createObservableKeyedStore();
|
|
14
25
|
const cellIndex = new Map();
|
|
@@ -60,6 +71,7 @@ export function createObjectStore() {
|
|
|
60
71
|
rotationY: options.rotation ?? 0,
|
|
61
72
|
...(options.parentSpace !== undefined ? { parentSpace: options.parentSpace } : {}),
|
|
62
73
|
...(options.visual !== undefined ? { visual: options.visual } : {}),
|
|
74
|
+
...(options.animation !== undefined ? { animation: options.animation } : {}),
|
|
63
75
|
});
|
|
64
76
|
indexAdd(instanceId, position);
|
|
65
77
|
return instanceId;
|
|
@@ -99,6 +111,7 @@ export function createObjectStore() {
|
|
|
99
111
|
position: current.position,
|
|
100
112
|
rotationY: current.rotationY,
|
|
101
113
|
...(current.parentSpace !== undefined ? { parentSpace: current.parentSpace } : {}),
|
|
114
|
+
...(current.animation !== undefined ? { animation: current.animation } : {}),
|
|
102
115
|
...(visual !== undefined ? { visual } : {}),
|
|
103
116
|
});
|
|
104
117
|
return true;
|
|
@@ -145,13 +158,27 @@ export function createObjectStore() {
|
|
|
145
158
|
return results;
|
|
146
159
|
},
|
|
147
160
|
inBox(min, max) {
|
|
148
|
-
const hits = [];
|
|
149
161
|
const minCx = cellIndexOf(min[0]);
|
|
150
162
|
const maxCx = cellIndexOf(max[0]);
|
|
151
163
|
const minCy = cellIndexOf(min[1]);
|
|
152
164
|
const maxCy = cellIndexOf(max[1]);
|
|
153
165
|
const minCz = cellIndexOf(min[2]);
|
|
154
166
|
const maxCz = cellIndexOf(max[2]);
|
|
167
|
+
const cellsX = Math.max(0, maxCx - minCx + 1);
|
|
168
|
+
const cellsY = Math.max(0, maxCy - minCy + 1);
|
|
169
|
+
const cellsZ = Math.max(0, maxCz - minCz + 1);
|
|
170
|
+
const cellVolume = cellsX * cellsY * cellsZ;
|
|
171
|
+
// Huge queries (city-scale movement broadphase inflated by tall building colliders): scanning
|
|
172
|
+
// every empty 1 m cell is far more expensive than testing the few hundred placed objects.
|
|
173
|
+
if (cellVolume > IN_BOX_CELL_VOLUME_LIMIT) {
|
|
174
|
+
const hits = [];
|
|
175
|
+
for (const object of store.arraySnapshot()) {
|
|
176
|
+
if (objectInAabb(object, min, max))
|
|
177
|
+
hits.push(object);
|
|
178
|
+
}
|
|
179
|
+
return hits;
|
|
180
|
+
}
|
|
181
|
+
const hits = [];
|
|
155
182
|
for (let cx = minCx; cx <= maxCx; cx += 1) {
|
|
156
183
|
for (let cy = minCy; cy <= maxCy; cy += 1) {
|
|
157
184
|
for (let cz = minCz; cz <= maxCz; cz += 1) {
|
|
@@ -162,12 +189,8 @@ export function createObjectStore() {
|
|
|
162
189
|
const object = store.get(instanceId);
|
|
163
190
|
if (object === undefined)
|
|
164
191
|
continue;
|
|
165
|
-
|
|
166
|
-
if (x >= min[0] && x <= max[0] &&
|
|
167
|
-
y >= min[1] && y <= max[1] &&
|
|
168
|
-
z >= min[2] && z <= max[2]) {
|
|
192
|
+
if (objectInAabb(object, min, max))
|
|
169
193
|
hits.push(object);
|
|
170
|
-
}
|
|
171
194
|
}
|
|
172
195
|
}
|
|
173
196
|
}
|
|
@@ -145,6 +145,19 @@ export interface ParamSchema {
|
|
|
145
145
|
export declare function findSchemaPreset(schema: ParamSchema, id: string): ParamPreset | undefined;
|
|
146
146
|
/** Parsed params after `parseParams`: every schema field present with a validated, defaulted value. */
|
|
147
147
|
export type ParsedParams = Record<string, number | boolean | string | WeightedParamEntry[]>;
|
|
148
|
+
/**
|
|
149
|
+
* Marks a kind as *scatterable* — it fills an authored area with instances at one density under a
|
|
150
|
+
* bounded budget — so the editor renders the shared `CoverageSection` (Area → Assets → Density slider
|
|
151
|
+
* + budget note) that gives grass/scatter/city one authoring gesture. `spec` selects the unit + budget
|
|
152
|
+
* from `@jgengine/core/world/scatterCoverage`; `densityKey` is the single density slider hoisted into
|
|
153
|
+
* the coverage header (and hidden from the schema body below); `assetsKey` is the optional weighted
|
|
154
|
+
* "what fills it" palette summarized in the header.
|
|
155
|
+
*/
|
|
156
|
+
export interface SceneKindCoverage {
|
|
157
|
+
spec: import("../world/scatterCoverage.js").ScatterCoverageKind;
|
|
158
|
+
densityKey: string;
|
|
159
|
+
assetsKey?: string;
|
|
160
|
+
}
|
|
148
161
|
/**
|
|
149
162
|
* Parse a raw `meta` bag against a schema into typed params — every field present, invalid/missing
|
|
150
163
|
* values replaced by the field default, numbers clamped to their range. The single parser every
|
|
@@ -201,6 +214,12 @@ export interface SceneKindDefinition<TResolved = unknown> {
|
|
|
201
214
|
resolve?: (object: SceneKindObject, params: ParsedParams, context: SceneKindResolveContext) => TResolved;
|
|
202
215
|
/** Optional one-line readout under the inspector fields (estimate, dimensions) — owned by the studio. */
|
|
203
216
|
note?: (object: SceneKindObject, params: ParsedParams) => string;
|
|
217
|
+
/**
|
|
218
|
+
* Optional scatterable-coverage descriptor. When set, the editor leads this kind's inspector with
|
|
219
|
+
* the shared Area / Assets / Density `CoverageSection` and surfaces the budget clamp-and-warn note,
|
|
220
|
+
* giving every scatterable kind (grass/scatter/city) one authoring pattern.
|
|
221
|
+
*/
|
|
222
|
+
coverage?: SceneKindCoverage;
|
|
204
223
|
}
|
|
205
224
|
/** The raw document object a resolver receives — shape shared by markers, volumes, and paths. */
|
|
206
225
|
export interface SceneKindObject {
|
|
@@ -246,6 +265,24 @@ export interface SceneKindResolveContext {
|
|
|
246
265
|
* inspector, `+ Add` menu, and `AuthoredScene` renderer lookup all read this registry.
|
|
247
266
|
*/
|
|
248
267
|
export declare function registerSceneKind<TResolved>(definition: SceneKindDefinition<TResolved>): void;
|
|
268
|
+
/**
|
|
269
|
+
* Declare a game's own **click-to-place** marker kind in one call — the light path for logical
|
|
270
|
+
* markers a game reads off the document by `kind` (a stash, checkpoint, spawn point, bounty…), with
|
|
271
|
+
* none of a full studio's schema/resolver boilerplate. Registering it makes the kind a click-to-place
|
|
272
|
+
* tool in the editor's `+ Add` menu (grouped under `category`) and gives its markers an inspector for
|
|
273
|
+
* any `fields` you pass. Call at module load from a module the editor loads (a game's `editorLayers`
|
|
274
|
+
* import graph). Rendering is unaffected — the game still reads `kind` off `editorLayers.markers`.
|
|
275
|
+
*/
|
|
276
|
+
export declare function definePlaceableMarkerKind(options: {
|
|
277
|
+
kind: string;
|
|
278
|
+
label: string;
|
|
279
|
+
/** `+ Add` menu section header this kind groups under. Defaults to `"Custom"`. */
|
|
280
|
+
category?: string;
|
|
281
|
+
/** Accent hex for the menu entry + inspector header. */
|
|
282
|
+
accent?: string;
|
|
283
|
+
/** Optional inspector fields written into each marker's `meta` (e.g. a payout value). */
|
|
284
|
+
fields?: readonly ParamField[];
|
|
285
|
+
}): string;
|
|
249
286
|
/** The registered definition for a kind, or undefined if the kind is a plain (non-studio) kind. @internal */
|
|
250
287
|
export declare function getSceneKind(kind: string): SceneKindDefinition | undefined;
|
|
251
288
|
/** True when a kind has a registered studio definition (schema/inspector/renderer). @internal */
|
package/dist/scene/sceneKinds.js
CHANGED
|
@@ -198,6 +198,25 @@ const registry = new Map();
|
|
|
198
198
|
export function registerSceneKind(definition) {
|
|
199
199
|
registry.set(definition.kind, definition);
|
|
200
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Declare a game's own **click-to-place** marker kind in one call — the light path for logical
|
|
203
|
+
* markers a game reads off the document by `kind` (a stash, checkpoint, spawn point, bounty…), with
|
|
204
|
+
* none of a full studio's schema/resolver boilerplate. Registering it makes the kind a click-to-place
|
|
205
|
+
* tool in the editor's `+ Add` menu (grouped under `category`) and gives its markers an inspector for
|
|
206
|
+
* any `fields` you pass. Call at module load from a module the editor loads (a game's `editorLayers`
|
|
207
|
+
* import graph). Rendering is unaffected — the game still reads `kind` off `editorLayers.markers`.
|
|
208
|
+
*/
|
|
209
|
+
export function definePlaceableMarkerKind(options) {
|
|
210
|
+
registerSceneKind({
|
|
211
|
+
kind: options.kind,
|
|
212
|
+
target: "marker",
|
|
213
|
+
label: options.label,
|
|
214
|
+
addCategory: options.category ?? "Custom",
|
|
215
|
+
...(options.accent === undefined ? {} : { accent: options.accent }),
|
|
216
|
+
schema: { fields: options.fields ?? [] },
|
|
217
|
+
});
|
|
218
|
+
return options.kind;
|
|
219
|
+
}
|
|
201
220
|
/** The registered definition for a kind, or undefined if the kind is a plain (non-studio) kind. @internal */
|
|
202
221
|
export function getSceneKind(kind) {
|
|
203
222
|
return registry.get(kind);
|