@jgengine/core 0.12.0 → 0.13.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 +26 -0
- 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 +25 -1
- package/dist/runtime/context/combat.js +2 -1
- package/dist/runtime/context/combatFx.js +15 -5
- package/dist/runtime/context/scene.d.ts +2 -1
- package/dist/runtime/context/scene.js +39 -1
- package/dist/runtime/gameContext.js +50 -4
- package/dist/runtime/gameContextTypes.d.ts +37 -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/colliders.d.ts +21 -0
- package/dist/scene/colliders.js +62 -0
- package/dist/scene/entityStats.js +9 -1
- 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/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 +4 -4
- 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/roads.d.ts +19 -2
- package/dist/world/roads.js +60 -3
- 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/package.json +1 -1
- package/skills/jgengine/SKILL.md +1 -0
- 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 +52 -24
- package/skills/jgengine-world/capabilities.md +8 -4
- package/skills/jgengine-world/reference.md +1 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Procedural animation for part-composed (rig-less) models — characters kit-bashed from
|
|
3
|
+
* primitives/`ModelPart`s with no skeleton or clips. Pure, deterministic curve sampling:
|
|
4
|
+
* the same (time, speed, phase, flinch, death) input always yields the same pose, so
|
|
5
|
+
* replays and multi-client renders stay in lockstep and the hot path allocates nothing
|
|
6
|
+
* when an `out` pose is reused.
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_STRIDE_HZ = 1.6;
|
|
9
|
+
const DEFAULT_SWING_RAD = 0.7;
|
|
10
|
+
const DEFAULT_BOB_AMP = 0.04;
|
|
11
|
+
const DEFAULT_SWAY_RAD = 0.05;
|
|
12
|
+
const DEFAULT_WALK_SPEED = 2;
|
|
13
|
+
const DEFAULT_FLINCH_RAD = 0.35;
|
|
14
|
+
const DEFAULT_TOPPLE_RAD = Math.PI / 2;
|
|
15
|
+
const IDLE_BREATHE_HZ = 0.45;
|
|
16
|
+
const TWO_PI = Math.PI * 2;
|
|
17
|
+
function clamp01(value) {
|
|
18
|
+
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
19
|
+
}
|
|
20
|
+
function zeroPose(out) {
|
|
21
|
+
out.position[0] = 0;
|
|
22
|
+
out.position[1] = 0;
|
|
23
|
+
out.position[2] = 0;
|
|
24
|
+
out.rotation[0] = 0;
|
|
25
|
+
out.rotation[1] = 0;
|
|
26
|
+
out.rotation[2] = 0;
|
|
27
|
+
out.scale[0] = 1;
|
|
28
|
+
out.scale[1] = 1;
|
|
29
|
+
out.scale[2] = 1;
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
/** Volume-conserving vertical squash: compress y by `amount`, bulge x/z by half of it. */
|
|
33
|
+
function applySquash(out, amount) {
|
|
34
|
+
out.scale[1] *= 1 - amount;
|
|
35
|
+
const bulge = 1 + amount * 0.5;
|
|
36
|
+
out.scale[0] *= bulge;
|
|
37
|
+
out.scale[2] *= bulge;
|
|
38
|
+
}
|
|
39
|
+
/** A reusable identity pose (zero offsets, unit scale). */
|
|
40
|
+
export function createPartPose() {
|
|
41
|
+
return { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Deterministic per-character stride phase in [0, 1) from an id (e.g. the entity instance id),
|
|
45
|
+
* so a crowd of identical part-composed characters doesn't animate in lockstep.
|
|
46
|
+
*/
|
|
47
|
+
export function partMotionPhase(id) {
|
|
48
|
+
let hash = 0;
|
|
49
|
+
for (let index = 0; index < id.length; index += 1)
|
|
50
|
+
hash = (hash * 31 + id.charCodeAt(index)) >>> 0;
|
|
51
|
+
return (hash % 1000) / 1000;
|
|
52
|
+
}
|
|
53
|
+
const SPLAT_FLATTEN = 0.7;
|
|
54
|
+
/**
|
|
55
|
+
* Sample the character-root pose: walk bob, idle breathe/sway, backward hit flinch, and death —
|
|
56
|
+
* a sideways topple by default, or a flatten-out splat with `deathStyle: "splat"`. With
|
|
57
|
+
* `squashAmp > 0` the body also squashes and stretches (footfall squash, jelly idle breathe,
|
|
58
|
+
* flinch squash pulse), volume-conserving. A driver applies this to the group wrapping the
|
|
59
|
+
* whole composition (base model plus parts) — limbs ride along, as they would on a real torso.
|
|
60
|
+
*
|
|
61
|
+
* @capability part-motion sample the root bob/sway/squash/flinch/death pose for a rig-less part-composed character
|
|
62
|
+
*/
|
|
63
|
+
export function sampleBodyPose(input, params, out = createPartPose()) {
|
|
64
|
+
zeroPose(out);
|
|
65
|
+
const walkSpeed = params?.walkSpeed ?? DEFAULT_WALK_SPEED;
|
|
66
|
+
const strideHz = params?.strideHz ?? DEFAULT_STRIDE_HZ;
|
|
67
|
+
const bobAmp = params?.bobAmp ?? DEFAULT_BOB_AMP;
|
|
68
|
+
const swayRad = params?.swayRad ?? DEFAULT_SWAY_RAD;
|
|
69
|
+
const squashAmp = params?.squashAmp ?? 0;
|
|
70
|
+
const death = clamp01(input.death);
|
|
71
|
+
const alive = 1 - death;
|
|
72
|
+
const walk = clamp01(input.speed / walkSpeed) * alive;
|
|
73
|
+
const idle = (1 - walk) * alive;
|
|
74
|
+
const stride = TWO_PI * (input.timeSec * strideHz + input.phase);
|
|
75
|
+
const breathe = TWO_PI * (input.timeSec * IDLE_BREATHE_HZ + input.phase);
|
|
76
|
+
const step = Math.abs(Math.sin(stride));
|
|
77
|
+
// Double-bounce bob: the body rises on each step, twice per stride cycle.
|
|
78
|
+
out.position[1] = step * bobAmp * walk + Math.sin(breathe) * bobAmp * 0.5 * idle;
|
|
79
|
+
out.rotation[2] = Math.sin(breathe) * swayRad * idle;
|
|
80
|
+
out.rotation[0] = -clamp01(input.flinch) * (params?.flinchRad ?? DEFAULT_FLINCH_RAD) * alive;
|
|
81
|
+
if (squashAmp > 0) {
|
|
82
|
+
// Footfall squash: deepest where the bob is lowest (step ≈ 0), released at the top of the step.
|
|
83
|
+
applySquash(out, (1 - step) * squashAmp * walk);
|
|
84
|
+
// Jelly breathe: idle scale oscillation, gentler than a footfall.
|
|
85
|
+
applySquash(out, Math.sin(breathe) * squashAmp * 0.4 * idle);
|
|
86
|
+
// Flinch reads as a squash pulse on a soft body.
|
|
87
|
+
applySquash(out, clamp01(input.flinch) * squashAmp * alive);
|
|
88
|
+
}
|
|
89
|
+
if ((params?.deathStyle ?? "topple") === "splat") {
|
|
90
|
+
applySquash(out, death * SPLAT_FLATTEN);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
out.rotation[2] += death * (params?.toppleRad ?? DEFAULT_TOPPLE_RAD);
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Sample one part's pose offset by role: counter-phase leg/arm swing driven by speed, a head
|
|
99
|
+
* counter-sway, tail wag, and wing flap. Offsets are added onto the part's authored
|
|
100
|
+
* position/rotation. Motion fades to zero as `death` ramps so the topple reads as one piece.
|
|
101
|
+
*
|
|
102
|
+
* @capability part-motion sample a limb/head/tail/wing pose offset for a role-tagged ModelPart
|
|
103
|
+
*/
|
|
104
|
+
export function samplePartPose(role, input, params, out = createPartPose()) {
|
|
105
|
+
zeroPose(out);
|
|
106
|
+
const walkSpeed = params?.walkSpeed ?? DEFAULT_WALK_SPEED;
|
|
107
|
+
const strideHz = params?.strideHz ?? DEFAULT_STRIDE_HZ;
|
|
108
|
+
const swingRad = params?.swingRad ?? DEFAULT_SWING_RAD;
|
|
109
|
+
const swayRad = params?.swayRad ?? DEFAULT_SWAY_RAD;
|
|
110
|
+
const alive = 1 - clamp01(input.death);
|
|
111
|
+
const walk = clamp01(input.speed / walkSpeed) * alive;
|
|
112
|
+
const idle = (1 - walk) * alive;
|
|
113
|
+
const stride = TWO_PI * (input.timeSec * strideHz + input.phase);
|
|
114
|
+
const breathe = TWO_PI * (input.timeSec * IDLE_BREATHE_HZ + input.phase);
|
|
115
|
+
switch (role) {
|
|
116
|
+
case "leg.l":
|
|
117
|
+
out.rotation[0] = Math.sin(stride) * swingRad * walk;
|
|
118
|
+
break;
|
|
119
|
+
case "leg.r":
|
|
120
|
+
out.rotation[0] = Math.sin(stride + Math.PI) * swingRad * walk;
|
|
121
|
+
break;
|
|
122
|
+
case "arm.l":
|
|
123
|
+
// Arms counter their same-side leg, at slightly lower amplitude.
|
|
124
|
+
out.rotation[0] = Math.sin(stride + Math.PI) * swingRad * 0.8 * walk;
|
|
125
|
+
out.rotation[2] = Math.sin(breathe) * swayRad * idle;
|
|
126
|
+
break;
|
|
127
|
+
case "arm.r":
|
|
128
|
+
out.rotation[0] = Math.sin(stride) * swingRad * 0.8 * walk;
|
|
129
|
+
out.rotation[2] = -Math.sin(breathe) * swayRad * idle;
|
|
130
|
+
break;
|
|
131
|
+
case "head":
|
|
132
|
+
out.rotation[2] = -Math.sin(breathe) * swayRad * idle;
|
|
133
|
+
out.rotation[1] = Math.sin(stride) * swayRad * 0.5 * walk;
|
|
134
|
+
out.rotation[0] = -clamp01(input.flinch) * 0.5 * (params?.flinchRad ?? DEFAULT_FLINCH_RAD) * alive;
|
|
135
|
+
break;
|
|
136
|
+
case "tail":
|
|
137
|
+
out.rotation[1] = Math.sin(stride) * swingRad * 0.5 * walk + Math.sin(breathe) * swayRad * 2 * idle;
|
|
138
|
+
break;
|
|
139
|
+
case "wing.l":
|
|
140
|
+
out.rotation[2] = Math.abs(Math.sin(stride)) * swingRad * walk + Math.sin(breathe) * swayRad * idle;
|
|
141
|
+
break;
|
|
142
|
+
case "wing.r":
|
|
143
|
+
out.rotation[2] = -Math.abs(Math.sin(stride)) * swingRad * walk - Math.sin(breathe) * swayRad * idle;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
@@ -10,6 +10,7 @@ import type { HudPlatform, HudViewportConfig } from "../ui/hudScale.js";
|
|
|
10
10
|
import type { PositionedPrompt } from "../interaction/proximityPrompt.js";
|
|
11
11
|
import type { CatalogEntityRole, GameContext, GameContextContent } from "../runtime/gameContext.js";
|
|
12
12
|
import type { ModelDims } from "../scene/assetCatalog.js";
|
|
13
|
+
import type { PartMotionParams, PartRole } from "./partAnimation.js";
|
|
13
14
|
import type { CollisionMeshData } from "../scene/collisionMesh.js";
|
|
14
15
|
import type { SkyEnvironmentConfig } from "../world/features.js";
|
|
15
16
|
import type { VisibilityConfig } from "../visibility/config.js";
|
|
@@ -163,6 +164,14 @@ export interface ModelPart {
|
|
|
163
164
|
rotation?: [number, number, number];
|
|
164
165
|
/** Uniform scale of the part under the parent's transform. Default 1. */
|
|
165
166
|
scale?: number;
|
|
167
|
+
/**
|
|
168
|
+
* Semantic motion role for a rig-less part-composed character (see `game/partAnimation`).
|
|
169
|
+
* Tagging any part enables the shell's procedural driver on an entity model: legs and arms swing
|
|
170
|
+
* counter-phase with movement speed, the head counter-sways, tails wag, wings flap, and the whole
|
|
171
|
+
* composition gains walk bob, idle breathe, hit flinch, and a death topple. Untagged parts stay
|
|
172
|
+
* static kit pieces.
|
|
173
|
+
*/
|
|
174
|
+
role?: PartRole;
|
|
166
175
|
}
|
|
167
176
|
export interface ModelConfig {
|
|
168
177
|
url: string;
|
|
@@ -178,12 +187,21 @@ export interface ModelConfig {
|
|
|
178
187
|
collisionMesh?: CollisionMeshData;
|
|
179
188
|
/** Per-entity PBR tint/finish override (#151.3); cloned onto each `MeshStandardMaterial` in the model so shared GLTF caches stay untouched. */
|
|
180
189
|
material?: ModelMaterialOverride;
|
|
181
|
-
/**
|
|
182
|
-
|
|
190
|
+
/**
|
|
191
|
+
* Plays a GLTF animation clip on the model when the source has any (skinned or not).
|
|
192
|
+
* A model resolved from a catalog id whose index carries `clips` animates automatically —
|
|
193
|
+
* semantic clip roles build the idle/walk/run states and hit/death/attack one-shots (see
|
|
194
|
+
* `defaultAnimationForClips`). `"auto"` forces that derivation, `"none"` opts out (bind pose),
|
|
195
|
+
* and an explicit config always wins. Omit on an inline (non-catalog) config to render the
|
|
196
|
+
* rig's bind pose.
|
|
197
|
+
*/
|
|
198
|
+
animation?: ModelAnimationConfig | "auto" | "none";
|
|
183
199
|
/** Props/weapons parented to named bones on this model's rig; each follows its bone through animation. */
|
|
184
200
|
attachments?: readonly ModelAttachment[];
|
|
185
|
-
/** Static kit-of-parts pieces stacked at fixed local offsets — no bone/rig involved. Use this for a compound entity assembled from several modular meshes (a castle keep from base + mid + roof pieces); use `attachments` for props parented to an animated rig's bones. */
|
|
201
|
+
/** Static kit-of-parts pieces stacked at fixed local offsets — no bone/rig involved. Use this for a compound entity assembled from several modular meshes (a castle keep from base + mid + roof pieces); use `attachments` for props parented to an animated rig's bones. Tag parts with a `role` to procedurally animate a rig-less character composition. */
|
|
186
202
|
parts?: readonly ModelPart[];
|
|
203
|
+
/** Tuning for the procedural part-motion driver when any part carries a `role`; omit for defaults. */
|
|
204
|
+
partMotion?: PartMotionParams;
|
|
187
205
|
}
|
|
188
206
|
export interface ObjectStyle {
|
|
189
207
|
color?: string;
|
|
@@ -286,7 +304,19 @@ export interface PresentationEffectsConfig {
|
|
|
286
304
|
}
|
|
287
305
|
/** How a screenshot host reaches live gameplay in this game — the data behind `shoot --mode play`. */
|
|
288
306
|
export interface GameCaptureConfig {
|
|
289
|
-
/**
|
|
307
|
+
/**
|
|
308
|
+
* Commands run (in order, via `ctx.game.commands.run`) right after boot when a capture host requests the
|
|
309
|
+
* play screen — the same commands the start-menu buttons dispatch. A bare string runs with a default input;
|
|
310
|
+
* the object form carries the input a command needs (e.g. `[{ name: "class.select", input: { classId: "siren" } }, "startRun"]`).
|
|
311
|
+
* A name the game never registers fails the capture loudly instead of shipping a menu screenshot.
|
|
312
|
+
*
|
|
313
|
+
* Timing contract: these dispatch **once**, synchronously, at `onContextReady` — after `onNewPlayer` but
|
|
314
|
+
* before any async boot work it kicked off has settled. If the game restores state asynchronously (a
|
|
315
|
+
* whole-world save load, network hydration) and that restore resets the same "started" gate these commands
|
|
316
|
+
* flip, the restore lands *after* the play commands and re-shows the menu — the capture then fails with a
|
|
317
|
+
* start menu still on screen even though `play` is declared. Make such a restore preserve an already-live
|
|
318
|
+
* session (skip the reset when the start gate is already set) so it does not bounce capture back to the title.
|
|
319
|
+
*/
|
|
290
320
|
play?: readonly (string | {
|
|
291
321
|
name: string;
|
|
292
322
|
input?: unknown;
|
|
@@ -362,12 +392,15 @@ export interface PlayableGame<TUi = unknown, TWorldOverlay = unknown, TRenderEnt
|
|
|
362
392
|
* declares one of the given roles (default: all); `maxDistance` hides nameplates beyond this many
|
|
363
393
|
* world units from the player (default 40). Headless: skin every part via `className`/`data-*` hooks
|
|
364
394
|
* on `WorldNameplates` (`@jgengine/shell/world/WorldHud`) — this flag only turns the readout on and
|
|
365
|
-
* scopes which entities it covers.
|
|
395
|
+
* scopes which entities it covers. Set `showHealth: false` for a name-only plate when a game already
|
|
396
|
+
* draws the HP bar itself (its own HUD, or `worldHealthBars`) — otherwise the nameplate's built-in bar
|
|
397
|
+
* stacks a second health bar over every entity.
|
|
366
398
|
*/
|
|
367
399
|
nameplates?: boolean | {
|
|
368
400
|
statId?: string;
|
|
369
401
|
roles?: readonly CatalogEntityRole[];
|
|
370
402
|
maxDistance?: number;
|
|
403
|
+
showHealth?: boolean;
|
|
371
404
|
};
|
|
372
405
|
/**
|
|
373
406
|
* Combat presentation stack mounted inside the 3D canvas (telegraphs, spell VFX, retained VFX,
|
|
@@ -76,7 +76,7 @@ export interface LevelingStatAccess {
|
|
|
76
76
|
max?: number;
|
|
77
77
|
}): unknown;
|
|
78
78
|
}
|
|
79
|
-
/** A resolved leveling track: the immutable `maxLevel`/`startLevel`, the `xpForLevel` threshold lookup, a pure `resolve`, and `grantXp` which writes back through a {@link LevelingStatAccess} and fires
|
|
79
|
+
/** A resolved leveling track: the immutable `maxLevel`/`startLevel`, the `xpForLevel` threshold lookup, a pure `resolve`, and `grantXp` which writes back through a {@link LevelingStatAccess} and fires `onLevelUp` once for every reached level in ascending order. */
|
|
80
80
|
export interface LevelingTrack {
|
|
81
81
|
readonly maxLevel: number;
|
|
82
82
|
readonly startLevel: number;
|
|
@@ -89,7 +89,7 @@ export interface LevelingTrack {
|
|
|
89
89
|
* returned {@link LevelingTrack} resolves how many levels a given xp total earns
|
|
90
90
|
* (rolling over surplus xp, capping at `maxLevel`) and, via `grantXp`, writes the
|
|
91
91
|
* new level/xp back through a {@link LevelingStatAccess} and fires `onLevelUp` for
|
|
92
|
-
* each level gained. Reach for this instead of hand-tracking xp thresholds; pair
|
|
92
|
+
* each level gained in ascending order. Reach for this instead of hand-tracking xp thresholds; pair
|
|
93
93
|
* it with `resource-pool` (mana/stamina) and `ability-bar` (cooldowns) for a hero.
|
|
94
94
|
*
|
|
95
95
|
* @capability leveling-track grant XP, resolve level-ups against a curve, and write level+xp stats back through a stat adapter
|
package/dist/game/progression.js
CHANGED
|
@@ -69,7 +69,7 @@ export function curve(spec) {
|
|
|
69
69
|
* returned {@link LevelingTrack} resolves how many levels a given xp total earns
|
|
70
70
|
* (rolling over surplus xp, capping at `maxLevel`) and, via `grantXp`, writes the
|
|
71
71
|
* new level/xp back through a {@link LevelingStatAccess} and fires `onLevelUp` for
|
|
72
|
-
* each level gained. Reach for this instead of hand-tracking xp thresholds; pair
|
|
72
|
+
* each level gained in ascending order. Reach for this instead of hand-tracking xp thresholds; pair
|
|
73
73
|
* it with `resource-pool` (mana/stamina) and `ability-bar` (cooldowns) for a hero.
|
|
74
74
|
*
|
|
75
75
|
* @capability leveling-track grant XP, resolve level-ups against a curve, and write level+xp stats back through a stat adapter
|
|
@@ -128,7 +128,9 @@ export function leveling(config) {
|
|
|
128
128
|
access.set(userId, xpStat, { current: progress.xp, max: progress.xpMax });
|
|
129
129
|
if (progress.levelsGained > 0) {
|
|
130
130
|
access.set(userId, levelStat, { current: progress.level });
|
|
131
|
-
|
|
131
|
+
for (let reached = level.current + 1; reached <= progress.level; reached += 1) {
|
|
132
|
+
onLevelUp?.(reached);
|
|
133
|
+
}
|
|
132
134
|
}
|
|
133
135
|
return progress.levelsGained;
|
|
134
136
|
};
|
|
@@ -35,25 +35,41 @@ export type MoveResult = {
|
|
|
35
35
|
status: "rejected";
|
|
36
36
|
reason: "invalid-slot" | "empty-slot" | "wrong-kind" | "no-space";
|
|
37
37
|
};
|
|
38
|
+
/** Outcome of {@link splitStack}: the new state, or a rejection reason. */
|
|
39
|
+
export type SplitResult = {
|
|
40
|
+
status: "ok";
|
|
41
|
+
state: InventoryState;
|
|
42
|
+
} | {
|
|
43
|
+
status: "rejected";
|
|
44
|
+
reason: "invalid-slot" | "empty-slot" | "invalid-amount" | "no-space" | "slot-occupied";
|
|
45
|
+
};
|
|
38
46
|
/**
|
|
39
47
|
* A bag of stackable items supporting add, remove, count, and move — the base inventory model.
|
|
40
48
|
*
|
|
41
49
|
* @capability inventory-grid a bag of stackable items with add, remove, and move
|
|
42
|
-
|
|
43
|
-
*/
|
|
50
|
+
*/
|
|
44
51
|
export declare function createEmptyInventory(layout: InventoryLayout): InventoryState;
|
|
45
|
-
/**
|
|
52
|
+
/** Add `count` of `itemId` — into a specific `options.slot`, or auto-stacked across free/partial slots. Rejects on no space or wrong kind. */
|
|
46
53
|
export declare function putItem(state: InventoryState, layout: InventoryLayout, traits: ItemTraits, itemId: string, count: number, options?: {
|
|
47
54
|
slot?: number;
|
|
48
55
|
}): PutResult;
|
|
49
|
-
/**
|
|
56
|
+
/** Remove `count` of `itemId`, draining from the last matching stacks first. Rejects if fewer than `count` are held. */
|
|
50
57
|
export declare function takeItem(state: InventoryState, itemId: string, count: number): TakeResult;
|
|
51
|
-
/**
|
|
58
|
+
/** Total quantity of `itemId` summed across every stack in the inventory. */
|
|
52
59
|
export declare function countItem(state: InventoryState, itemId: string): number;
|
|
53
|
-
/**
|
|
60
|
+
/** True when the inventory holds at least `count` of `itemId`. */
|
|
54
61
|
export declare function hasItem(state: InventoryState, itemId: string, count: number): boolean;
|
|
55
|
-
/**
|
|
62
|
+
/** Move the stack at `fromSlot` to `toSlot` (or auto-stack when omitted) — swapping, merging with remainder, or relocating across two inventories. Rejects on empty source, wrong kind, or no space. */
|
|
56
63
|
export declare function moveItem(from: InventoryState, fromSlot: number, to: InventoryState, toLayout: InventoryLayout, traits: ItemTraits, toSlot?: number): MoveResult;
|
|
64
|
+
/**
|
|
65
|
+
* Splits `amount` off the stack in `slot` into `toSlot` (or the first empty slot when `toSlot` is
|
|
66
|
+
* omitted). Limit-agnostic: it does not consult {@link ItemTraits}, so a same-item target merges by
|
|
67
|
+
* simple addition. Rejects an empty source, `amount <= 0`, `amount >= stack count` (a split must
|
|
68
|
+
* leave both sides non-empty), no free slot, or a target holding a different item.
|
|
69
|
+
*
|
|
70
|
+
* @capability inventory-split split a stack into a target or first-empty slot
|
|
71
|
+
*/
|
|
72
|
+
export declare function splitStack(state: InventoryState, slot: number, amount: number, toSlot?: number): SplitResult;
|
|
57
73
|
export interface InventorySet<TId extends string> {
|
|
58
74
|
put(id: TId, itemId: string, count: number, options?: {
|
|
59
75
|
slot?: number;
|
|
@@ -65,5 +81,5 @@ export interface InventorySet<TId extends string> {
|
|
|
65
81
|
state(id: TId): InventoryState;
|
|
66
82
|
replaceState(id: TId, state: InventoryState): void;
|
|
67
83
|
}
|
|
68
|
-
/**
|
|
84
|
+
/** Build a keyed set of named inventories from their `layouts`, sharing one `traits` table — the stateful façade over put/take/move/count. */
|
|
69
85
|
export declare function createInventorySet<TId extends string>(layouts: Record<TId, InventoryLayout>, traits: ItemTraits): InventorySet<TId>;
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* A bag of stackable items supporting add, remove, count, and move — the base inventory model.
|
|
3
3
|
*
|
|
4
4
|
* @capability inventory-grid a bag of stackable items with add, remove, and move
|
|
5
|
-
|
|
6
|
-
*/
|
|
5
|
+
*/
|
|
7
6
|
export function createEmptyInventory(layout) {
|
|
8
7
|
return { slots: new Array(layout.slots).fill(null) };
|
|
9
8
|
}
|
|
@@ -82,13 +81,13 @@ function explicitPut(state, layout, traits, itemId, count, slot) {
|
|
|
82
81
|
slots[slot] = { itemId, count: newCount };
|
|
83
82
|
return { status: "ok", state: { slots } };
|
|
84
83
|
}
|
|
85
|
-
/**
|
|
84
|
+
/** Add `count` of `itemId` — into a specific `options.slot`, or auto-stacked across free/partial slots. Rejects on no space or wrong kind. */
|
|
86
85
|
export function putItem(state, layout, traits, itemId, count, options) {
|
|
87
86
|
if (options?.slot !== undefined)
|
|
88
87
|
return explicitPut(state, layout, traits, itemId, count, options.slot);
|
|
89
88
|
return autoPut(state, layout, traits, itemId, count);
|
|
90
89
|
}
|
|
91
|
-
/**
|
|
90
|
+
/** Remove `count` of `itemId`, draining from the last matching stacks first. Rejects if fewer than `count` are held. */
|
|
92
91
|
export function takeItem(state, itemId, count) {
|
|
93
92
|
if (count <= 0)
|
|
94
93
|
return { status: "ok", state };
|
|
@@ -107,7 +106,7 @@ export function takeItem(state, itemId, count) {
|
|
|
107
106
|
}
|
|
108
107
|
return { status: "ok", state: { slots } };
|
|
109
108
|
}
|
|
110
|
-
/**
|
|
109
|
+
/** Total quantity of `itemId` summed across every stack in the inventory. */
|
|
111
110
|
export function countItem(state, itemId) {
|
|
112
111
|
let total = 0;
|
|
113
112
|
for (const slot of state.slots)
|
|
@@ -115,11 +114,11 @@ export function countItem(state, itemId) {
|
|
|
115
114
|
total += slot.count;
|
|
116
115
|
return total;
|
|
117
116
|
}
|
|
118
|
-
/**
|
|
117
|
+
/** True when the inventory holds at least `count` of `itemId`. */
|
|
119
118
|
export function hasItem(state, itemId, count) {
|
|
120
119
|
return countItem(state, itemId) >= count;
|
|
121
120
|
}
|
|
122
|
-
/**
|
|
121
|
+
/** Move the stack at `fromSlot` to `toSlot` (or auto-stack when omitted) — swapping, merging with remainder, or relocating across two inventories. Rejects on empty source, wrong kind, or no space. */
|
|
123
122
|
export function moveItem(from, fromSlot, to, toLayout, traits, toSlot) {
|
|
124
123
|
if (fromSlot < 0 || fromSlot >= from.slots.length)
|
|
125
124
|
return { status: "rejected", reason: "invalid-slot" };
|
|
@@ -171,7 +170,47 @@ export function moveItem(from, fromSlot, to, toLayout, traits, toSlot) {
|
|
|
171
170
|
to: sameInventory ? { slots: fromSlots } : { slots: toSlots },
|
|
172
171
|
};
|
|
173
172
|
}
|
|
174
|
-
/**
|
|
173
|
+
/**
|
|
174
|
+
* Splits `amount` off the stack in `slot` into `toSlot` (or the first empty slot when `toSlot` is
|
|
175
|
+
* omitted). Limit-agnostic: it does not consult {@link ItemTraits}, so a same-item target merges by
|
|
176
|
+
* simple addition. Rejects an empty source, `amount <= 0`, `amount >= stack count` (a split must
|
|
177
|
+
* leave both sides non-empty), no free slot, or a target holding a different item.
|
|
178
|
+
*
|
|
179
|
+
* @capability inventory-split split a stack into a target or first-empty slot
|
|
180
|
+
*/
|
|
181
|
+
export function splitStack(state, slot, amount, toSlot) {
|
|
182
|
+
if (slot < 0 || slot >= state.slots.length)
|
|
183
|
+
return { status: "rejected", reason: "invalid-slot" };
|
|
184
|
+
const source = state.slots[slot];
|
|
185
|
+
if (source === null)
|
|
186
|
+
return { status: "rejected", reason: "empty-slot" };
|
|
187
|
+
if (amount <= 0 || amount >= source.count)
|
|
188
|
+
return { status: "rejected", reason: "invalid-amount" };
|
|
189
|
+
let target = toSlot;
|
|
190
|
+
if (target === undefined) {
|
|
191
|
+
target = state.slots.findIndex((s) => s === null);
|
|
192
|
+
if (target === -1)
|
|
193
|
+
return { status: "rejected", reason: "no-space" };
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
if (target < 0 || target >= state.slots.length)
|
|
197
|
+
return { status: "rejected", reason: "invalid-slot" };
|
|
198
|
+
if (target === slot)
|
|
199
|
+
return { status: "rejected", reason: "slot-occupied" };
|
|
200
|
+
const dest = state.slots[target];
|
|
201
|
+
if (dest !== null && dest.itemId !== source.itemId)
|
|
202
|
+
return { status: "rejected", reason: "slot-occupied" };
|
|
203
|
+
}
|
|
204
|
+
const slots = state.slots.slice();
|
|
205
|
+
const dest = slots[target];
|
|
206
|
+
slots[slot] = { itemId: source.itemId, count: source.count - amount };
|
|
207
|
+
slots[target] =
|
|
208
|
+
dest === null || dest === undefined
|
|
209
|
+
? { itemId: source.itemId, count: amount }
|
|
210
|
+
: { itemId: source.itemId, count: dest.count + amount };
|
|
211
|
+
return { status: "ok", state: { slots } };
|
|
212
|
+
}
|
|
213
|
+
/** Build a keyed set of named inventories from their `layouts`, sharing one `traits` table — the stateful façade over put/take/move/count. */
|
|
175
214
|
export function createInventorySet(layouts, traits) {
|
|
176
215
|
const states = new Map();
|
|
177
216
|
for (const id of Object.keys(layouts)) {
|
package/dist/meta/changelog.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.13.0";
|
|
3
3
|
/** One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md). */
|
|
4
4
|
export interface ChangelogEntry {
|
|
5
5
|
migrate: readonly string[];
|
package/dist/meta/changelog.js
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
/** Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating. */
|
|
2
|
-
export const VERSION = "0.
|
|
2
|
+
export const VERSION = "0.13.0";
|
|
3
3
|
/** Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`). */
|
|
4
4
|
export const CHANGELOG = {
|
|
5
|
+
"0.13.0": {
|
|
6
|
+
migrate: [
|
|
7
|
+
"Bump lockstep SDK packages to ^0.13.0: @jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}. CLI jgengine is 0.10.0; @jgengine/github is unchanged.",
|
|
8
|
+
"Nothing required — additive release. Two overridable defaults changed in the player's favor: offline games persist to localStorage automatically (#1207) and rigged catalog assets animate by default via clip metadata + semantic clip roles (#1209); opt out explicitly if a game relied on the old behavior.",
|
|
9
|
+
],
|
|
10
|
+
added: [
|
|
11
|
+
"Drop-in inventory grid, window manager, and character sheet (#1220) — @jgengine/react InventoryGrid (drag/stack/split), toggleable windows backed by @jgengine/core/ui/panelModel, CharacterSheet paperdoll; all HudTheme-skinnable.",
|
|
12
|
+
"Serializable runtime snapshots (#1179) — capture/restore live runtime state including magazine state (@jgengine/core/combat/magazine) and stat modifiers.",
|
|
13
|
+
"Portable weapon plumbing (#1211) — @jgengine/core/combat/weaponFire composes trigger → magazine → spread → projectile/hitscan as data.",
|
|
14
|
+
"Portable leveling (#1206) and portable damage-pool access (#1208) as genre-agnostic game-context seams.",
|
|
15
|
+
"Named combat VFX presets (#1204) — @jgengine/core/combat/vfxPresets; a visible attack is one line.",
|
|
16
|
+
"Runtime-measured render-bounds hitboxes (#1227) — colliders wrap what the shell actually mounts.",
|
|
17
|
+
"Procedural part motion for rig-less part-composed characters (#1222); squash & stretch and death splat (#1229).",
|
|
18
|
+
"City fabric (#1191) — streetGenerator + cityGenerator, editor bake verb, street-aware lots, road junctions, grounding.",
|
|
19
|
+
"jgengine recipe CLI (#1214, #1231) — vetted wired compositions, compile-pinned against the SDK.",
|
|
20
|
+
"Editor/debug modes mirrored to the URL with an editor exit (#1203); dependency-free WebGL screenshot script in the game template (#1202).",
|
|
21
|
+
],
|
|
22
|
+
changed: [
|
|
23
|
+
"Offline games persist to localStorage by default (#1207); rigged assets animate by default (#1209).",
|
|
24
|
+
"RTS camera pan inversion fixed (#1228); sprite raycast warning silenced and duplicate nameplate health bars removed (#1194); scaffolded F2+E editor Tailwind @source renders its theme (#1195).",
|
|
25
|
+
"Outside-game DX: stat-pool no-op stability, camera-transparent decor, cleaner install/version output, headless UI-intent seam (#1223).",
|
|
26
|
+
],
|
|
27
|
+
removed: [],
|
|
28
|
+
},
|
|
5
29
|
"0.12.0": {
|
|
6
30
|
migrate: [
|
|
7
31
|
"Bump lockstep SDK packages to ^0.12.0: @jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}. CLI jgengine is 0.9.0; @jgengine/github is unchanged.",
|
|
@@ -2,6 +2,7 @@ import { createDeathSystem, deathReasonFromEffect } from "../../combat/death.js"
|
|
|
2
2
|
import { createEffectSystem, } from "../../combat/effects.js";
|
|
3
3
|
import { createProjectileSystem } from "../../combat/projectiles.js";
|
|
4
4
|
import { createVfxInstanceStore } from "../../game/vfxInstance.js";
|
|
5
|
+
import { createEntityStatsApi } from "../../scene/entityStats.js";
|
|
5
6
|
import { notifyAfter } from "../../store/changeSignal.js";
|
|
6
7
|
import { createCombatFx } from "./combatFx.js";
|
|
7
8
|
import { applyLethalLoot, isLocalPlayerKill } from "./deathLoot.js";
|
|
@@ -30,7 +31,7 @@ export function createCombatSubsystem(d) {
|
|
|
30
31
|
});
|
|
31
32
|
const effects = notifyAfter(createEffectSystem({
|
|
32
33
|
resolveReceive: (instanceId) => catalogEntry(instanceId)?.receive,
|
|
33
|
-
|
|
34
|
+
statPools: createEntityStatsApi((instanceId) => statsByInstance.get(instanceId)),
|
|
34
35
|
getStat: weapon.getStat,
|
|
35
36
|
spatial: combatSpatial,
|
|
36
37
|
onLethal(instanceId, lethalCtx) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveHitReaction } from "../../combat/hitReaction.js";
|
|
2
2
|
import { pointInTelegraph } from "../../combat/telegraph.js";
|
|
3
|
+
import { resolveVfxPreset } from "../../combat/vfxPresets.js";
|
|
3
4
|
/** @internal */
|
|
4
5
|
export function createCombatFx(d) {
|
|
5
6
|
const { entities, events, time } = d;
|
|
@@ -52,17 +53,26 @@ export function createCombatFx(d) {
|
|
|
52
53
|
const from = resolveVfxPoint(input.from) ?? to;
|
|
53
54
|
if (from === undefined)
|
|
54
55
|
return;
|
|
56
|
+
// A named preset fills in kind/color/defaults; any field on the call still wins. With no
|
|
57
|
+
// preset and no explicit kind/color the resolver falls back to a visible spark rather than
|
|
58
|
+
// emitting an invalid (blank) burst — so a bare `vfx({ from, to })` still shows something.
|
|
59
|
+
const resolved = resolveVfxPreset(input.preset, {
|
|
60
|
+
...(input.kind === undefined ? {} : { kind: input.kind }),
|
|
61
|
+
...(input.color === undefined ? {} : { color: input.color }),
|
|
62
|
+
...(input.durationMs === undefined ? {} : { durationMs: input.durationMs }),
|
|
63
|
+
...(input.radius === undefined ? {} : { radius: input.radius }),
|
|
64
|
+
});
|
|
55
65
|
const event = {
|
|
56
66
|
id: vfxSeq++,
|
|
57
|
-
kind:
|
|
58
|
-
color:
|
|
67
|
+
kind: resolved.kind,
|
|
68
|
+
color: resolved.color,
|
|
59
69
|
from,
|
|
60
|
-
durationMs:
|
|
70
|
+
durationMs: resolved.durationMs ?? vfxDefaultDurationMs[resolved.kind],
|
|
61
71
|
};
|
|
62
72
|
if (to !== undefined)
|
|
63
73
|
event.to = to;
|
|
64
|
-
if (
|
|
65
|
-
event.radius =
|
|
74
|
+
if (resolved.radius !== undefined)
|
|
75
|
+
event.radius = resolved.radius;
|
|
66
76
|
events.emit("combat.vfx", event);
|
|
67
77
|
}
|
|
68
78
|
let telegraphSeq = 0;
|
|
@@ -4,7 +4,7 @@ import { type BodyBind } from "../../scene/bodyBind.js";
|
|
|
4
4
|
import { type EntityStatsApi, type StatValueMap } from "../../scene/entityStats.js";
|
|
5
5
|
import { type EntityPosition, type EntityStore, type SceneEntity, type SpawnOptions } from "../../scene/entityStore.js";
|
|
6
6
|
import { type Forms } from "../../scene/form.js";
|
|
7
|
-
import { type EntityColliderSet } from "../../scene/colliders.js";
|
|
7
|
+
import { type EntityColliderSet, type MeasuredBounds } from "../../scene/colliders.js";
|
|
8
8
|
import { type ObjectStore } from "../../scene/objectStore.js";
|
|
9
9
|
import { type PaintLayer } from "../../scene/paintLayer.js";
|
|
10
10
|
import { type SceneRaycastApi } from "../../scene/sceneRaycast.js";
|
|
@@ -41,6 +41,7 @@ export interface SceneSubsystem {
|
|
|
41
41
|
objectColliders: Map<string, EntityColliderSet>;
|
|
42
42
|
entityCollidersOf: (instanceId: string) => EntityColliderSet | null;
|
|
43
43
|
objectCollidersOf: (instanceId: string) => EntityColliderSet | null;
|
|
44
|
+
reportEntityBounds: (kind: string, bounds: MeasuredBounds | null) => boolean;
|
|
44
45
|
entityVisualScaleOf: (instanceId: string) => number;
|
|
45
46
|
forms: Forms;
|
|
46
47
|
paintLayer: PaintLayer;
|
|
@@ -2,7 +2,7 @@ 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, measuredEntityColliders, measuredObjectColliders, scaledEntityColliders, scaledObjectColliders, } from "../../scene/colliders.js";
|
|
6
6
|
import { raycastObjects, raycastObjectsAll } from "../../scene/objectQuery.js";
|
|
7
7
|
import { createObjectStore, objectVisualScale } from "../../scene/objectStore.js";
|
|
8
8
|
import { createPaintLayer } from "../../scene/paintLayer.js";
|
|
@@ -82,6 +82,36 @@ export function createSceneSubsystem(d) {
|
|
|
82
82
|
fittedObjectByCatalogId.set(catalogId, fitted);
|
|
83
83
|
return fitted;
|
|
84
84
|
}
|
|
85
|
+
const measuredEntityByKind = new Map();
|
|
86
|
+
const measuredObjectByCatalogId = new Map();
|
|
87
|
+
function reportEntityBounds(kind, bounds) {
|
|
88
|
+
if (bounds === null) {
|
|
89
|
+
const existed = measuredEntityByKind.delete(kind);
|
|
90
|
+
if (existed)
|
|
91
|
+
signalNotify();
|
|
92
|
+
return existed;
|
|
93
|
+
}
|
|
94
|
+
const measured = measuredEntityColliders(bounds);
|
|
95
|
+
if (measured === null)
|
|
96
|
+
return false;
|
|
97
|
+
measuredEntityByKind.set(kind, measured);
|
|
98
|
+
signalNotify();
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
function reportObjectBounds(catalogId, bounds) {
|
|
102
|
+
if (bounds === null) {
|
|
103
|
+
const existed = measuredObjectByCatalogId.delete(catalogId);
|
|
104
|
+
if (existed)
|
|
105
|
+
signalNotify();
|
|
106
|
+
return existed;
|
|
107
|
+
}
|
|
108
|
+
const measured = measuredObjectColliders(bounds);
|
|
109
|
+
if (measured === null)
|
|
110
|
+
return false;
|
|
111
|
+
measuredObjectByCatalogId.set(catalogId, measured);
|
|
112
|
+
signalNotify();
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
85
115
|
function entityCollidersOf(instanceId) {
|
|
86
116
|
const override = entityColliders.get(instanceId);
|
|
87
117
|
if (override !== undefined)
|
|
@@ -94,6 +124,9 @@ export function createSceneSubsystem(d) {
|
|
|
94
124
|
const fitted = fittedEntitySetFor(kind);
|
|
95
125
|
if (fitted !== null)
|
|
96
126
|
return fitted;
|
|
127
|
+
const measured = measuredEntityByKind.get(kind);
|
|
128
|
+
if (measured !== undefined)
|
|
129
|
+
return measured;
|
|
97
130
|
}
|
|
98
131
|
const scale = entry?.scale;
|
|
99
132
|
if (scale !== undefined && scale !== 1)
|
|
@@ -118,6 +151,9 @@ export function createSceneSubsystem(d) {
|
|
|
118
151
|
const fitted = fittedObjectSetFor(object.catalogId);
|
|
119
152
|
if (fitted !== null)
|
|
120
153
|
return fitted;
|
|
154
|
+
const measured = measuredObjectByCatalogId.get(object.catalogId);
|
|
155
|
+
if (measured !== undefined)
|
|
156
|
+
return measured;
|
|
121
157
|
if (object.visual?.scale === undefined)
|
|
122
158
|
return null;
|
|
123
159
|
return scaledObjectColliders(objectVisualScale(object.visual));
|
|
@@ -254,6 +290,7 @@ export function createSceneSubsystem(d) {
|
|
|
254
290
|
signalNotify();
|
|
255
291
|
},
|
|
256
292
|
collidersOf: objectCollidersOf,
|
|
293
|
+
reportBounds: reportObjectBounds,
|
|
257
294
|
selection: objectSelection,
|
|
258
295
|
};
|
|
259
296
|
return {
|
|
@@ -272,6 +309,7 @@ export function createSceneSubsystem(d) {
|
|
|
272
309
|
objectColliders,
|
|
273
310
|
entityCollidersOf,
|
|
274
311
|
objectCollidersOf,
|
|
312
|
+
reportEntityBounds,
|
|
275
313
|
entityVisualScaleOf,
|
|
276
314
|
forms,
|
|
277
315
|
paintLayer,
|