@jgengine/core 0.12.0 → 0.14.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 +73 -1
- package/dist/combat/effects.d.ts +11 -4
- package/dist/combat/effects.js +24 -17
- package/dist/combat/magazine.d.ts +13 -0
- package/dist/combat/magazine.js +27 -0
- package/dist/combat/vfxPresets.d.ts +176 -0
- package/dist/combat/vfxPresets.js +74 -0
- package/dist/combat/weaponFire.d.ts +114 -0
- package/dist/combat/weaponFire.js +96 -0
- package/dist/combat.d.ts +4 -2
- package/dist/combat.js +3 -1
- package/dist/devtools/urlFlags.d.ts +22 -0
- package/dist/devtools/urlFlags.js +62 -0
- package/dist/game/clipRoles.d.ts +51 -0
- package/dist/game/clipRoles.js +142 -0
- package/dist/game/defineGame.d.ts +2 -2
- package/dist/game/partAnimation.d.ts +78 -0
- package/dist/game/partAnimation.js +147 -0
- package/dist/game/playableGame.d.ts +38 -5
- package/dist/game/progression.d.ts +2 -2
- package/dist/game/progression.js +4 -2
- package/dist/inventory/inventoryModel.d.ts +24 -8
- package/dist/inventory/inventoryModel.js +47 -8
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +45 -1
- package/dist/runtime/context/combat.js +2 -1
- package/dist/runtime/context/combatFx.js +15 -5
- package/dist/runtime/context/scene.d.ts +4 -1
- package/dist/runtime/context/scene.js +86 -1
- package/dist/runtime/gameContext.js +51 -4
- package/dist/runtime/gameContextTypes.d.ts +58 -4
- package/dist/runtime/headlessRunner.d.ts +13 -0
- package/dist/runtime/headlessRunner.js +14 -0
- package/dist/scene/assetCatalog.d.ts +2 -0
- package/dist/scene/builtinSceneKinds.d.ts +6 -1
- package/dist/scene/builtinSceneKinds.js +10 -3
- package/dist/scene/colliders.d.ts +41 -0
- package/dist/scene/colliders.js +140 -0
- package/dist/scene/collisionMesh.d.ts +8 -0
- package/dist/scene/collisionMesh.js +48 -0
- package/dist/scene/entityStats.js +9 -1
- package/dist/scene/sceneKinds.d.ts +19 -0
- package/dist/stats/statModifiers.d.ts +16 -0
- package/dist/stats/statModifiers.js +30 -1
- package/dist/stats/statPool.js +5 -2
- package/dist/ui/hudScale.d.ts +16 -1
- package/dist/ui/hudScale.js +22 -1
- package/dist/ui/panelModel.d.ts +120 -0
- package/dist/ui/panelModel.js +168 -0
- package/dist/ui.d.ts +1 -0
- package/dist/ui.js +1 -0
- package/dist/world/buildingLots.d.ts +84 -0
- package/dist/world/buildingLots.js +119 -0
- package/dist/world/buildings.d.ts +6 -0
- package/dist/world/cityGenerator.d.ts +41 -0
- package/dist/world/cityGenerator.js +58 -0
- package/dist/world/cityKind.d.ts +2 -2
- package/dist/world/cityKind.js +12 -6
- package/dist/world/environmentSummary.js +57 -1
- package/dist/world/features.d.ts +35 -2
- package/dist/world/features.js +5 -2
- package/dist/world/grassKind.js +4 -1
- package/dist/world/roads.d.ts +19 -2
- package/dist/world/roads.js +60 -3
- 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/{pathNetwork.d.ts → streetGenerator.d.ts} +37 -37
- package/dist/world/{pathNetwork.js → streetGenerator.js} +9 -9
- package/dist/world/streets.d.ts +74 -1
- package/dist/world/streets.js +218 -0
- package/dist/world/terrain.d.ts +20 -1
- package/dist/world/terrain.js +41 -2
- package/dist/world.d.ts +1 -0
- package/dist/world.js +1 -0
- package/package.json +2 -2
- package/skills/jgengine/SKILL.md +5 -2
- package/skills/jgengine/api.md +13 -5
- package/skills/jgengine-combat/SKILL.md +15 -1
- package/skills/jgengine-combat/api.md +39 -2
- package/skills/jgengine-combat/capabilities.md +17 -1
- package/skills/jgengine-combat/recipes/portable-damage-effects.md +197 -0
- package/skills/jgengine-combat/recipes/portable-runtime-state.md +154 -0
- package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +156 -0
- package/skills/jgengine-combat/recipes/visible-attacks.md +81 -0
- package/skills/jgengine-combat/reference.md +22 -3
- package/skills/jgengine-gameplay/SKILL.md +8 -0
- package/skills/jgengine-gameplay/api.md +35 -5
- package/skills/jgengine-gameplay/capabilities.md +18 -0
- package/skills/jgengine-gameplay/recipes/portable-xp-leveling.md +168 -0
- package/skills/jgengine-gameplay/reference-systems.md +9 -0
- package/skills/jgengine-world/api.md +82 -25
- package/skills/jgengine-world/capabilities.md +12 -4
- package/skills/jgengine-world/reference.md +1 -1
|
@@ -2,7 +2,8 @@ import { createBodyBind } from "../../scene/bodyBind.js";
|
|
|
2
2
|
import { createEntityStatsApi, seedStatValues, } from "../../scene/entityStats.js";
|
|
3
3
|
import { createEntityStore, } from "../../scene/entityStore.js";
|
|
4
4
|
import { createForms } from "../../scene/form.js";
|
|
5
|
-
import { fittedEntityColliders, fittedObjectColliders, scaledEntityColliders, scaledObjectColliders, } from "../../scene/colliders.js";
|
|
5
|
+
import { fittedEntityColliders, fittedObjectColliders, hasMeshColliderShape, measuredEntityColliders, measuredObjectColliders, meshEntityColliders, meshObjectColliders, scaledEntityColliders, scaledObjectColliders, } from "../../scene/colliders.js";
|
|
6
|
+
import { prepareCollisionMeshSource } from "../../scene/collisionMesh.js";
|
|
6
7
|
import { raycastObjects, raycastObjectsAll } from "../../scene/objectQuery.js";
|
|
7
8
|
import { createObjectStore, objectVisualScale } from "../../scene/objectStore.js";
|
|
8
9
|
import { createPaintLayer } from "../../scene/paintLayer.js";
|
|
@@ -82,6 +83,68 @@ export function createSceneSubsystem(d) {
|
|
|
82
83
|
fittedObjectByCatalogId.set(catalogId, fitted);
|
|
83
84
|
return fitted;
|
|
84
85
|
}
|
|
86
|
+
const measuredEntityByKind = new Map();
|
|
87
|
+
const measuredObjectByCatalogId = new Map();
|
|
88
|
+
function reportEntityBounds(kind, bounds) {
|
|
89
|
+
if (bounds === null) {
|
|
90
|
+
const existed = measuredEntityByKind.delete(kind);
|
|
91
|
+
if (existed)
|
|
92
|
+
signalNotify();
|
|
93
|
+
return existed;
|
|
94
|
+
}
|
|
95
|
+
const measured = measuredEntityColliders(bounds);
|
|
96
|
+
if (measured === null)
|
|
97
|
+
return false;
|
|
98
|
+
measuredEntityByKind.set(kind, measured);
|
|
99
|
+
signalNotify();
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
const meshEntityByKind = new Map();
|
|
103
|
+
const meshObjectByCatalogId = new Map();
|
|
104
|
+
function reportEntityCollisionMesh(kind, mesh) {
|
|
105
|
+
if (mesh === null) {
|
|
106
|
+
const existed = meshEntityByKind.delete(kind);
|
|
107
|
+
if (existed)
|
|
108
|
+
signalNotify();
|
|
109
|
+
return existed;
|
|
110
|
+
}
|
|
111
|
+
const prepared = prepareCollisionMeshSource(mesh);
|
|
112
|
+
const set = prepared === null ? null : meshEntityColliders(prepared);
|
|
113
|
+
if (set === null)
|
|
114
|
+
return false;
|
|
115
|
+
meshEntityByKind.set(kind, set);
|
|
116
|
+
signalNotify();
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
function reportObjectCollisionMesh(catalogId, mesh) {
|
|
120
|
+
if (mesh === null) {
|
|
121
|
+
const existed = meshObjectByCatalogId.delete(catalogId);
|
|
122
|
+
if (existed)
|
|
123
|
+
signalNotify();
|
|
124
|
+
return existed;
|
|
125
|
+
}
|
|
126
|
+
const prepared = prepareCollisionMeshSource(mesh);
|
|
127
|
+
const set = prepared === null ? null : meshObjectColliders(prepared);
|
|
128
|
+
if (set === null)
|
|
129
|
+
return false;
|
|
130
|
+
meshObjectByCatalogId.set(catalogId, set);
|
|
131
|
+
signalNotify();
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
function reportObjectBounds(catalogId, bounds) {
|
|
135
|
+
if (bounds === null) {
|
|
136
|
+
const existed = measuredObjectByCatalogId.delete(catalogId);
|
|
137
|
+
if (existed)
|
|
138
|
+
signalNotify();
|
|
139
|
+
return existed;
|
|
140
|
+
}
|
|
141
|
+
const measured = measuredObjectColliders(bounds);
|
|
142
|
+
if (measured === null)
|
|
143
|
+
return false;
|
|
144
|
+
measuredObjectByCatalogId.set(catalogId, measured);
|
|
145
|
+
signalNotify();
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
85
148
|
function entityCollidersOf(instanceId) {
|
|
86
149
|
const override = entityColliders.get(instanceId);
|
|
87
150
|
if (override !== undefined)
|
|
@@ -92,8 +155,18 @@ export function createSceneSubsystem(d) {
|
|
|
92
155
|
const kind = entities.get(instanceId)?.name;
|
|
93
156
|
if (kind !== undefined) {
|
|
94
157
|
const fitted = fittedEntitySetFor(kind);
|
|
158
|
+
// Index-derived mesh shapes win (identical on a headless host); otherwise a runtime-reported
|
|
159
|
+
// collision mesh upgrades the fitted/measured bounding box to the rendered triangles.
|
|
160
|
+
if (fitted !== null && hasMeshColliderShape(fitted))
|
|
161
|
+
return fitted;
|
|
162
|
+
const runtimeMesh = meshEntityByKind.get(kind);
|
|
163
|
+
if (runtimeMesh !== undefined)
|
|
164
|
+
return runtimeMesh;
|
|
95
165
|
if (fitted !== null)
|
|
96
166
|
return fitted;
|
|
167
|
+
const measured = measuredEntityByKind.get(kind);
|
|
168
|
+
if (measured !== undefined)
|
|
169
|
+
return measured;
|
|
97
170
|
}
|
|
98
171
|
const scale = entry?.scale;
|
|
99
172
|
if (scale !== undefined && scale !== 1)
|
|
@@ -116,8 +189,16 @@ export function createSceneSubsystem(d) {
|
|
|
116
189
|
if (object === null)
|
|
117
190
|
return null;
|
|
118
191
|
const fitted = fittedObjectSetFor(object.catalogId);
|
|
192
|
+
if (fitted !== null && hasMeshColliderShape(fitted))
|
|
193
|
+
return fitted;
|
|
194
|
+
const runtimeMesh = meshObjectByCatalogId.get(object.catalogId);
|
|
195
|
+
if (runtimeMesh !== undefined)
|
|
196
|
+
return runtimeMesh;
|
|
119
197
|
if (fitted !== null)
|
|
120
198
|
return fitted;
|
|
199
|
+
const measured = measuredObjectByCatalogId.get(object.catalogId);
|
|
200
|
+
if (measured !== undefined)
|
|
201
|
+
return measured;
|
|
121
202
|
if (object.visual?.scale === undefined)
|
|
122
203
|
return null;
|
|
123
204
|
return scaledObjectColliders(objectVisualScale(object.visual));
|
|
@@ -254,6 +335,8 @@ export function createSceneSubsystem(d) {
|
|
|
254
335
|
signalNotify();
|
|
255
336
|
},
|
|
256
337
|
collidersOf: objectCollidersOf,
|
|
338
|
+
reportBounds: reportObjectBounds,
|
|
339
|
+
reportCollisionMesh: reportObjectCollisionMesh,
|
|
257
340
|
selection: objectSelection,
|
|
258
341
|
};
|
|
259
342
|
return {
|
|
@@ -272,6 +355,8 @@ export function createSceneSubsystem(d) {
|
|
|
272
355
|
objectColliders,
|
|
273
356
|
entityCollidersOf,
|
|
274
357
|
objectCollidersOf,
|
|
358
|
+
reportEntityBounds,
|
|
359
|
+
reportEntityCollisionMesh,
|
|
275
360
|
entityVisualScaleOf,
|
|
276
361
|
forms,
|
|
277
362
|
paintLayer,
|
|
@@ -6,6 +6,7 @@ import { createGameFeed } from "../game/feed.js";
|
|
|
6
6
|
import { setGamePhase } from "../game/gamePhase.js";
|
|
7
7
|
import { createLootRegistry, grantDrops } from "../game/lootTable.js";
|
|
8
8
|
import { createSocial } from "../game/social.js";
|
|
9
|
+
import { splitStack } from "../inventory/inventoryModel.js";
|
|
9
10
|
import { createItemUse } from "../item/use.js";
|
|
10
11
|
import { createWeaponStats } from "../item/weapon.js";
|
|
11
12
|
import {} from "../scene/entityStats.js";
|
|
@@ -55,7 +56,7 @@ export function createGameContext(options) {
|
|
|
55
56
|
...(occluder !== undefined ? { occluder } : {}),
|
|
56
57
|
...(options.models !== undefined ? { models: options.models } : {}),
|
|
57
58
|
});
|
|
58
|
-
const { entities, objects, statsByInstance, entityStats, ensureInstanceStats, catalogEntry, spatial, combatSpatial, sceneRaycast, targeting, entityColliders, entityCollidersOf, objectCollidersOf, entityVisualScaleOf, forms, paintLayer, spawnEntity, despawnEntity, moveEntityTowardCommit, resetAllToSpawn, bind, sceneObjects, } = scene;
|
|
59
|
+
const { entities, objects, statsByInstance, entityStats, ensureInstanceStats, catalogEntry, spatial, combatSpatial, sceneRaycast, targeting, entityColliders, entityCollidersOf, objectCollidersOf, reportEntityBounds, reportEntityCollisionMesh, entityVisualScaleOf, forms, paintLayer, spawnEntity, despawnEntity, moveEntityTowardCommit, resetAllToSpawn, bind, sceneObjects, } = scene;
|
|
59
60
|
// --- Shared game services (weapon, feed, features bag, commands, item use) ---
|
|
60
61
|
const weapon = createWeaponStats((itemId) => content.itemById?.(itemId));
|
|
61
62
|
const feed = createGameFeed(definition.feed);
|
|
@@ -114,6 +115,45 @@ export function createGameContext(options) {
|
|
|
114
115
|
});
|
|
115
116
|
const { inventoryIds, inventoryByUser, inventoryFor, layouts, wallets, walletOf, economy, putIntoAnyInventory, playerStatsFor, pose, possession, motionByUser, motionFor, loadouts, seedUserPool, } = playerSys;
|
|
116
117
|
scene.setOnAfterDespawn((instanceId) => pose.clear(instanceId));
|
|
118
|
+
// Built-in inventory mutation commands so a HUD grid drives moves/splits through the notifying
|
|
119
|
+
// command path (a raw InventorySet mutation would not re-render React). Each resolves the acting
|
|
120
|
+
// user's own inventory, so it respects `privatePerUser` replication.
|
|
121
|
+
const knownInventory = (inventoryId) => layouts[inventoryId] !== undefined;
|
|
122
|
+
const slotInRange = (inventoryId, slot) => typeof slot === "number" && Number.isInteger(slot) && slot >= 0 && slot < layouts[inventoryId].slots;
|
|
123
|
+
commandRegistry.define("inventory.move", {
|
|
124
|
+
validate(_state, input) {
|
|
125
|
+
if (!knownInventory(input.inventoryId))
|
|
126
|
+
return { reason: `unknown inventory "${input.inventoryId}"` };
|
|
127
|
+
if (!slotInRange(input.inventoryId, input.from) || !slotInRange(input.inventoryId, input.to)) {
|
|
128
|
+
return { reason: "invalid-slot" };
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
},
|
|
132
|
+
apply(_state, input) {
|
|
133
|
+
const inventory = inventoryFor(activeUserId());
|
|
134
|
+
// A rejected move (empty source, wrong-kind, no-space) is a harmless no-op: the item bounces back.
|
|
135
|
+
inventory.move(input.inventoryId, input.from, input.inventoryId, input.to);
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
commandRegistry.define("inventory.split", {
|
|
139
|
+
validate(_state, input) {
|
|
140
|
+
if (!knownInventory(input.inventoryId))
|
|
141
|
+
return { reason: `unknown inventory "${input.inventoryId}"` };
|
|
142
|
+
if (!slotInRange(input.inventoryId, input.slot))
|
|
143
|
+
return { reason: "invalid-slot" };
|
|
144
|
+
if (input.toSlot !== undefined && !slotInRange(input.inventoryId, input.toSlot)) {
|
|
145
|
+
return { reason: "invalid-slot" };
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
},
|
|
149
|
+
apply(_state, input) {
|
|
150
|
+
const inventory = inventoryFor(activeUserId());
|
|
151
|
+
const result = splitStack(inventory.state(input.inventoryId), input.slot, input.amount, input.toSlot);
|
|
152
|
+
if (result.status === "rejected")
|
|
153
|
+
return;
|
|
154
|
+
inventory.replaceState(input.inventoryId, result.state);
|
|
155
|
+
},
|
|
156
|
+
});
|
|
117
157
|
const loot = {
|
|
118
158
|
register: lootRegistry.register,
|
|
119
159
|
has: lootRegistry.has,
|
|
@@ -328,6 +368,8 @@ export function createGameContext(options) {
|
|
|
328
368
|
signal.notify();
|
|
329
369
|
},
|
|
330
370
|
collidersOf: entityCollidersOf,
|
|
371
|
+
reportBounds: reportEntityBounds,
|
|
372
|
+
reportCollisionMesh: reportEntityCollisionMesh,
|
|
331
373
|
visualScaleOf: entityVisualScaleOf,
|
|
332
374
|
form: forms,
|
|
333
375
|
paint: paintLayer,
|
|
@@ -472,16 +514,21 @@ function saveKey(name) {
|
|
|
472
514
|
const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
473
515
|
return `jgengine:save:${slug === "" ? "game" : slug}`;
|
|
474
516
|
}
|
|
475
|
-
/**
|
|
517
|
+
/**
|
|
518
|
+
* Explicit `options.save` wins (the shell can inject any backend, e.g. cloud); otherwise an offline /
|
|
519
|
+
* single-player world auto-wires a `localStorage` whole-world save **by default** — a game gets `ctx.game.save`
|
|
520
|
+
* and autosave without touching `persist`. Only an explicit `persist: false` opts out; a host-authoritative
|
|
521
|
+
* multiplayer world never persists to `localStorage` (its host owns persistence) regardless of `persist`.
|
|
522
|
+
*/
|
|
476
523
|
function resolveSaveOptions(definition, options) {
|
|
477
524
|
if (options.save !== undefined)
|
|
478
525
|
return options.save;
|
|
479
526
|
const persist = definition.persist;
|
|
480
|
-
if (persist ===
|
|
527
|
+
if (persist === false)
|
|
481
528
|
return undefined;
|
|
482
529
|
if (!isOffline(definition.multiplayer))
|
|
483
530
|
return undefined;
|
|
484
|
-
const config = persist === true ? {} : persist;
|
|
531
|
+
const config = persist === undefined || persist === true ? {} : persist;
|
|
485
532
|
return {
|
|
486
533
|
backend: config.storage === "memory" ? memorySaveBackend() : localSaveBackend(),
|
|
487
534
|
mode: config.mode,
|
|
@@ -2,6 +2,7 @@ import type { CardPile, CardPileConfig } from "../cards/cardPile.js";
|
|
|
2
2
|
import type { EffectInput, EffectResult, EffectVia, ReceiveMap, SingleTargetEffectInput } from "../combat/effects.js";
|
|
3
3
|
import type { ProjectileSystem } from "../combat/projectiles.js";
|
|
4
4
|
import type { HitReaction, HitReactionConfig, ImpactPresetName } from "../combat/hitReaction.js";
|
|
5
|
+
import type { VfxPresetName } from "../combat/vfxPresets.js";
|
|
5
6
|
import type { TelegraphShape } from "../combat/telegraph.js";
|
|
6
7
|
import type { CommandDefinition, CommandResult } from "../commands/commandRegistry.js";
|
|
7
8
|
import type { ChargeOptions as WalletChargeOptions } from "../economy/wallet.js";
|
|
@@ -33,7 +34,8 @@ import type { PaintLayer } from "../scene/paintLayer.js";
|
|
|
33
34
|
import type { EntityStatsApi, StatCatalog } from "../scene/entityStats.js";
|
|
34
35
|
import type { EntityBlackboard, EntityPose, EntityPosition, EntityStore, SceneEntity, SpawnOptions, SpawnPose } from "../scene/entityStore.js";
|
|
35
36
|
import type { Forms } from "../scene/form.js";
|
|
36
|
-
import type { EntityColliderSet, ModelBodySource } from "../scene/colliders.js";
|
|
37
|
+
import type { EntityColliderSet, MeasuredBounds, ModelBodySource } from "../scene/colliders.js";
|
|
38
|
+
import type { CollisionMeshSource } from "../scene/collisionMesh.js";
|
|
37
39
|
import type { ObjectRaycastHit, ObjectRaycastInput } from "../scene/objectQuery.js";
|
|
38
40
|
import type { ObjectStore } from "../scene/objectStore.js";
|
|
39
41
|
import type { Roster } from "../scene/roster.js";
|
|
@@ -135,6 +137,24 @@ export interface SceneObjectContext extends ObjectStore {
|
|
|
135
137
|
raycastAll(input: ObjectRaycastInput): readonly ObjectRaycastHit[];
|
|
136
138
|
setColliders(instanceId: string, colliders: EntityColliderSet | null): void;
|
|
137
139
|
collidersOf(instanceId: string): EntityColliderSet | null;
|
|
140
|
+
/**
|
|
141
|
+
* Report the entity-local AABB the renderer actually mounted for `catalogId` — stored per catalog
|
|
142
|
+
* id as a blocking physical body and consulted after authored `colliders` and index-`dims` fitted
|
|
143
|
+
* models, before the visual-scale fallback. The shell reports custom `renderObject` content and
|
|
144
|
+
* dims-less models automatically, so a placed prop's body wraps its real rendered shape instead of
|
|
145
|
+
* a unit-cube guess. Pass `null` to clear. Returns `false` when the bounds are degenerate.
|
|
146
|
+
* @capability measured-colliders hitboxes and blocking bodies wrap what the renderer actually mounted — custom render props and dims-less models stop falling back to fixed-size boxes.
|
|
147
|
+
*/
|
|
148
|
+
reportBounds(catalogId: string, bounds: MeasuredBounds | null): boolean;
|
|
149
|
+
/**
|
|
150
|
+
* Report the entity-local triangle soup the renderer actually mounted for `catalogId` — stored per
|
|
151
|
+
* catalog id as a blocking physical body whose raycasts hit the real surface (a BVH over the
|
|
152
|
+
* triangles) while movement obstruction keeps the conservative AABB. Consulted right after
|
|
153
|
+
* authored `colliders` / index collision meshes and before the fitted-box, measured-bounds, and
|
|
154
|
+
* visual-scale fallbacks. The shell reports rendered models automatically. Pass `null` to clear.
|
|
155
|
+
* Returns `false` when no valid triangle survives.
|
|
156
|
+
*/
|
|
157
|
+
reportCollisionMesh(catalogId: string, mesh: CollisionMeshSource | null): boolean;
|
|
138
158
|
/**
|
|
139
159
|
* Per-instance selection/highlight state for placed objects — the reactive counterpart to
|
|
140
160
|
* `worldHealthBars`/`nameplates` (entities), so a build-mode or RTS selection ring reads
|
|
@@ -156,10 +176,22 @@ export interface FloatTextInput {
|
|
|
156
176
|
crit?: boolean;
|
|
157
177
|
scale?: number;
|
|
158
178
|
}
|
|
159
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* Request a transient spell/ability VFX burst. The easy path is a named `preset` —
|
|
181
|
+
* `vfx({ preset: "arrow", from: caster, to: enemy })` renders a visible bolt with no color
|
|
182
|
+
* or archetype tuning; `"lightning"`, `"web"`, `"slash"`, `"shield"`, `"heal"`, `"explosion"`
|
|
183
|
+
* and the rest of {@link vfxPresets} likewise just work. `from`/`to` accept an instance id
|
|
184
|
+
* (the shell follows its live pose) or a fixed world point. Anything you also pass — `kind`,
|
|
185
|
+
* `color` (`0xRRGGBB`), `radius`, `durationMs` — overrides the preset; supply `kind` + `color`
|
|
186
|
+
* yourself for a fully custom burst with no preset. `durationMs` defaults per `kind`.
|
|
187
|
+
*/
|
|
160
188
|
export interface VfxInput {
|
|
161
|
-
kind
|
|
162
|
-
|
|
189
|
+
/** A named visual flavor from {@link vfxPresets}. Fills in `kind`/`color`/defaults; explicit fields below still win. */
|
|
190
|
+
preset?: VfxPresetName | (string & {});
|
|
191
|
+
/** Visual archetype. Optional when `preset` is given; required otherwise. */
|
|
192
|
+
kind?: VfxKind;
|
|
193
|
+
/** `0xRRGGBB` tint. Optional when `preset` is given; required otherwise. */
|
|
194
|
+
color?: number;
|
|
163
195
|
from?: string | readonly [number, number, number];
|
|
164
196
|
to?: string | readonly [number, number, number];
|
|
165
197
|
radius?: number;
|
|
@@ -242,6 +274,28 @@ export interface SceneEntityContext {
|
|
|
242
274
|
invalidateSpatial: SpatialApi["invalidate"];
|
|
243
275
|
setColliders(instanceId: string, colliders: EntityColliderSet | null): void;
|
|
244
276
|
collidersOf(instanceId: string): EntityColliderSet | null;
|
|
277
|
+
/**
|
|
278
|
+
* Report the entity-local AABB the renderer actually mounted for `kind` — stored per kind as a
|
|
279
|
+
* tight damage hitbox and consulted after authored `colliders` and index-`dims` fitted models,
|
|
280
|
+
* before the scale/humanoid-default fallbacks. The shell reports custom `renderEntity` content and
|
|
281
|
+
* dims-less models automatically, so hitboxes wrap the rendered mesh instead of the fixed
|
|
282
|
+
* 0.7×1.8×0.7 box. Runtime-measured on the client; authoritative hosts that never render should
|
|
283
|
+
* author `colliders` or model `dims` instead. Pass `null` to clear. Returns `false` when the
|
|
284
|
+
* bounds are degenerate.
|
|
285
|
+
* @capability measured-hitboxes an entity rendered with a custom mesh takes hits in a box matching its rendered bounds, not the fixed humanoid rectangle.
|
|
286
|
+
*/
|
|
287
|
+
reportBounds(kind: string, bounds: MeasuredBounds | null): boolean;
|
|
288
|
+
/**
|
|
289
|
+
* Report the entity-local triangle soup the renderer actually mounted for `kind` — stored per kind
|
|
290
|
+
* as a damage hitbox that raycasts the model's own triangles (BVH), so shots aimed between a
|
|
291
|
+
* character's feet or past its shoulder miss instead of hitting a bounding box. Consulted right
|
|
292
|
+
* after authored `colliders` / index collision meshes and before the fitted-box and
|
|
293
|
+
* measured-bounds fallbacks. The shell reports rendered models automatically; authoritative hosts
|
|
294
|
+
* that never render should author `colliders` or index collision meshes for identical results.
|
|
295
|
+
* Pass `null` to clear. Returns `false` when no valid triangle survives.
|
|
296
|
+
* @capability conforming-hitboxes shots aimed between a character's feet or past its shoulder MISS — damage raycasts hit the rendered model's triangles, not its bounding box.
|
|
297
|
+
*/
|
|
298
|
+
reportCollisionMesh(kind: string, mesh: CollisionMeshSource | null): boolean;
|
|
245
299
|
/** Uniform visual scale from the entity's catalog entry (1 when unscaled). */
|
|
246
300
|
visualScaleOf(instanceId: string): number;
|
|
247
301
|
form: Forms;
|
|
@@ -36,6 +36,17 @@ export interface HeadlessRunnerOptions<TAssetRef extends ModelAssetRef, TMultipl
|
|
|
36
36
|
/** Render-model lookup for collider auto-fit — pass what the shell would derive from `entityModels`/`objectModels` so headless raycasts hit the same boxes as play. */
|
|
37
37
|
models?: GameContextModels;
|
|
38
38
|
}
|
|
39
|
+
/** Canvas-free command/intent surface for {@link HeadlessRunner} — invoke a game's registered UI commands and read the resulting reactive state off `ctx`. */
|
|
40
|
+
export interface HeadlessCommandInterface {
|
|
41
|
+
/** Invoke a registered command/intent by name against the live context. Throws on `unknown-command` or a validation rejection so a headless UI-flow test fails loudly instead of silently no-op'ing. */
|
|
42
|
+
invoke(name: string, input?: unknown): void;
|
|
43
|
+
/** Non-throwing {@link invoke} returning the raw command result (inspect `status`). */
|
|
44
|
+
tryInvoke(name: string, input?: unknown): ReturnType<GameContext["game"]["commands"]["run"]>;
|
|
45
|
+
/** Registered command names. */
|
|
46
|
+
names(): string[];
|
|
47
|
+
/** Is a command registered? */
|
|
48
|
+
has(name: string): boolean;
|
|
49
|
+
}
|
|
39
50
|
/**
|
|
40
51
|
* A renderer-free driver for a game loop: builds a {@link GameContext} from a {@link GameDefinition}, runs the init
|
|
41
52
|
* hooks, then advances the simulation one step at a time from injected input. No React, R3F, or three.js — the whole
|
|
@@ -58,5 +69,7 @@ export interface HeadlessRunner {
|
|
|
58
69
|
step(dtSeconds: number, input?: HeadlessInput): number;
|
|
59
70
|
/** Publish input without advancing — for pre-seeding the held set before the first {@link step}. */
|
|
60
71
|
publishInput(input: HeadlessInput): void;
|
|
72
|
+
/** Drive the game's own UI commands / intents headlessly — the same registry `ctx.game.commands` and the shell's command sink dispatch into. No renderer, no pointer simulation: dispatch an intent, then read reactive state off `ctx`. */
|
|
73
|
+
readonly ui: HeadlessCommandInterface;
|
|
61
74
|
}
|
|
62
75
|
export declare function createHeadlessRunner<TAssetRef extends ModelAssetRef, TMultiplayer>(options: HeadlessRunnerOptions<TAssetRef, TMultiplayer>): HeadlessRunner;
|
|
@@ -29,6 +29,20 @@ export function createHeadlessRunner(options) {
|
|
|
29
29
|
input: ctx.input,
|
|
30
30
|
userId: player.userId,
|
|
31
31
|
publishInput,
|
|
32
|
+
ui: {
|
|
33
|
+
invoke(name, input) {
|
|
34
|
+
const result = ctx.game.commands.run(name, input);
|
|
35
|
+
if (result.status === "unknown-command") {
|
|
36
|
+
throw new Error(`headless ui.invoke: unknown command "${name}" — define it via ctx.game.commands.define`);
|
|
37
|
+
}
|
|
38
|
+
if (result.status === "rejected") {
|
|
39
|
+
throw new Error(`headless ui.invoke: command "${name}" rejected — ${result.reason}`);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
tryInvoke: (name, input) => ctx.game.commands.run(name, input),
|
|
43
|
+
names: () => ctx.game.commands.names(),
|
|
44
|
+
has: (name) => ctx.game.commands.has(name),
|
|
45
|
+
},
|
|
32
46
|
step(dtSeconds, input) {
|
|
33
47
|
if (input !== undefined)
|
|
34
48
|
publishInput(input);
|
|
@@ -22,6 +22,8 @@ export interface ModelAssetRef {
|
|
|
22
22
|
collisionMesh?: CollisionMeshData;
|
|
23
23
|
/** Authored asset-space metadata — canonical facing (degrees), source-unit scale, footprint, anchor, bounds, and rotation policy. The upstream owner of placement corrections; see {@link "scene/assetSpace".AssetSpace}. */
|
|
24
24
|
space?: AssetSpace;
|
|
25
|
+
/** Animation clip names read from the GLB at asset reindex. Lets the shell auto-animate rigged catalog assets (semantic clip roles → default idle/walk/run + one-shots) with no game-side clip strings. */
|
|
26
|
+
clips?: readonly string[];
|
|
25
27
|
}
|
|
26
28
|
/**
|
|
27
29
|
* A catalog entry backed by a registered {@link registerAssetGenerator} instead of a GLB URL — a
|
|
@@ -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();
|
|
@@ -107,6 +107,47 @@ export declare function fittedEntityColliders(model: ModelBodySource): EntityCol
|
|
|
107
107
|
* @internal
|
|
108
108
|
*/
|
|
109
109
|
export declare function fittedObjectColliders(model: ModelBodySource): EntityColliderSet | null;
|
|
110
|
+
/**
|
|
111
|
+
* Entity-local AABB of what the renderer actually mounted for a kind — the runtime counterpart to
|
|
112
|
+
* index-measured `dims`. Reported through `ctx.scene.entity.reportBounds` /
|
|
113
|
+
* `ctx.scene.object.reportBounds` (the shell does this automatically for custom
|
|
114
|
+
* `renderEntity`/`renderObject` content and models without index dims) so collider resolution can
|
|
115
|
+
* wrap the real rendered shape instead of a fixed-size guess.
|
|
116
|
+
*/
|
|
117
|
+
export interface MeasuredBounds {
|
|
118
|
+
min: EntityPosition;
|
|
119
|
+
max: EntityPosition;
|
|
120
|
+
}
|
|
121
|
+
/** Damage hitbox exactly wrapping runtime-measured render bounds — what `reportBounds` stores for an
|
|
122
|
+
* entity kind. `null` when the bounds are degenerate, keeping the caller's existing fallback.
|
|
123
|
+
* @internal
|
|
124
|
+
*/
|
|
125
|
+
export declare function measuredEntityColliders(bounds: MeasuredBounds): EntityColliderSet | null;
|
|
126
|
+
/** Blocking physical body exactly wrapping runtime-measured render bounds — what `reportBounds`
|
|
127
|
+
* stores for an object catalog id. `null` when the bounds are degenerate.
|
|
128
|
+
* @internal
|
|
129
|
+
*/
|
|
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;
|
|
110
151
|
/** @internal */
|
|
111
152
|
export declare function resolveColliders(set: EntityColliderSet | null | undefined): ResolvedCollider[];
|
|
112
153
|
/** @internal */
|
package/dist/scene/colliders.js
CHANGED
|
@@ -188,6 +188,146 @@ export function fittedObjectColliders(model) {
|
|
|
188
188
|
},
|
|
189
189
|
};
|
|
190
190
|
}
|
|
191
|
+
/** Thin measured axes (decals, blades, flat panels) keep at least this half-extent so the box stays hittable. */
|
|
192
|
+
const MIN_MEASURED_HALF_EXTENT = 0.01;
|
|
193
|
+
/** Tight AABB from measured bounds — `null` when inverted, non-finite, or a degenerate point. */
|
|
194
|
+
function measuredBox(bounds) {
|
|
195
|
+
const { min, max } = bounds;
|
|
196
|
+
let volume = 0;
|
|
197
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
198
|
+
const lo = min[axis];
|
|
199
|
+
const hi = max[axis];
|
|
200
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi) || hi < lo)
|
|
201
|
+
return null;
|
|
202
|
+
volume = Math.max(volume, hi - lo);
|
|
203
|
+
}
|
|
204
|
+
if (!(volume > 0))
|
|
205
|
+
return null;
|
|
206
|
+
return {
|
|
207
|
+
halfExtents: [
|
|
208
|
+
Math.max((max[0] - min[0]) / 2, MIN_MEASURED_HALF_EXTENT),
|
|
209
|
+
Math.max((max[1] - min[1]) / 2, MIN_MEASURED_HALF_EXTENT),
|
|
210
|
+
Math.max((max[2] - min[2]) / 2, MIN_MEASURED_HALF_EXTENT),
|
|
211
|
+
],
|
|
212
|
+
offset: [(min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/** Damage hitbox exactly wrapping runtime-measured render bounds — what `reportBounds` stores for an
|
|
216
|
+
* entity kind. `null` when the bounds are degenerate, keeping the caller's existing fallback.
|
|
217
|
+
* @internal
|
|
218
|
+
*/
|
|
219
|
+
export function measuredEntityColliders(bounds) {
|
|
220
|
+
const box = measuredBox(bounds);
|
|
221
|
+
if (box === null)
|
|
222
|
+
return null;
|
|
223
|
+
return {
|
|
224
|
+
hitboxes: [
|
|
225
|
+
{
|
|
226
|
+
name: "body",
|
|
227
|
+
purpose: "damage",
|
|
228
|
+
shape: { kind: "aabb", halfExtents: box.halfExtents, offset: box.offset },
|
|
229
|
+
damageEligible: true,
|
|
230
|
+
blocks: false,
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/** Blocking physical body exactly wrapping runtime-measured render bounds — what `reportBounds`
|
|
236
|
+
* stores for an object catalog id. `null` when the bounds are degenerate.
|
|
237
|
+
* @internal
|
|
238
|
+
*/
|
|
239
|
+
export function measuredObjectColliders(bounds) {
|
|
240
|
+
const box = measuredBox(bounds);
|
|
241
|
+
if (box === null)
|
|
242
|
+
return null;
|
|
243
|
+
return {
|
|
244
|
+
body: {
|
|
245
|
+
name: "body",
|
|
246
|
+
purpose: "physical",
|
|
247
|
+
shape: { kind: "aabb", halfExtents: box.halfExtents, offset: box.offset },
|
|
248
|
+
damageEligible: false,
|
|
249
|
+
blocks: true,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
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
|
+
}
|
|
191
331
|
/** @internal */
|
|
192
332
|
export function resolveColliders(set) {
|
|
193
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)
|