@jgengine/core 0.38.0 → 0.38.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/dist/area/areaEffectField.d.ts +7 -0
- package/dist/area/areaEffectField.js +20 -3
- package/dist/combat/castRunner.d.ts +13 -0
- package/dist/combat/castRunner.js +17 -0
- package/dist/combat/downed.d.ts +3 -0
- package/dist/combat/downed.js +11 -0
- package/dist/game/playableGame.d.ts +3 -3
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +41 -1
- package/dist/scene/selectionBookmarks.d.ts +2 -0
- package/dist/scene/selectionBookmarks.js +6 -2
- package/dist/survival/decayMeter.d.ts +23 -4
- package/dist/survival/decayMeter.js +23 -0
- package/dist/world/buildPermissions.d.ts +2 -0
- package/dist/world/buildPermissions.js +10 -0
- package/dist/world/buildSockets.js +1 -0
- package/dist/world/modelLod.d.ts +9 -1
- package/package.json +1 -1
- package/skills/jgengine/api.md +2 -2
- package/skills/jgengine-combat/api.md +1 -1
- package/skills/jgengine-gameplay/api.md +5 -3
- package/skills/jgengine-world/api.md +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -26,6 +26,53 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
26
26
|
`packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
|
|
27
27
|
-->
|
|
28
28
|
|
|
29
|
+
## 0.38.2
|
|
30
|
+
|
|
31
|
+
### Migrate
|
|
32
|
+
|
|
33
|
+
- **Bump lockstep SDK packages to `^0.38.2`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.35.2`; `@jgengine/github` is `0.25.2`.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `requestSceneCapture` and `requestSceneThumbnail` (`@jgengine/shell/render/sceneCapture`) ask the live scene for a frame or a save-card thumbnail. A request waits for `SceneCaptureBinding` to mount, waits for the canvas to paint, retries a blank read and resolves with the image or a stated failure (`no-binding`, `no-frame`, `undecodable`), so a game stops polling for the binding and waiting frames before a save screenshot.
|
|
38
|
+
- `registerSceneCapture` (`@jgengine/shell/render/sceneCapture`) lets a host that owns its own canvas hand the capture path a reader.
|
|
39
|
+
- `createDownedState` (`@jgengine/core/combat/downed`) exposes `restore(next)`, the counterpart to `snapshot()`, so a saved bleed-out resumes with the same remaining seconds, revive progress, phase and banner timer instead of being rebuilt around a guessed countdown.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- Pin Games to the 0.38.1 adoption
|
|
44
|
+
- `GameMenu`'s `style` and `detailStyle` accept `--jg-*` custom properties (`GameMenuStyle`), so a game passes `{ "--jg-menu-surface-radius": "12px" }` without a cast.
|
|
45
|
+
- `GameMenu` sets `pointer-events: auto` on its own root while its painted backdrop stays `none`, so a menu inside a HUD wrapper that passes clicks through to the scene no longer has dead buttons.
|
|
46
|
+
- `check-stateful-ratchet` skips a factory carrying `// stateless: <reason>`, the named case for a catalog whose only closure state is an index of its own config. `createBuildSocketCatalog` declares it instead of being baselined.
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- `bun run shoot` refuses a scene shot whose page has no 3D canvas, and scores rendered geometry inside the canvas region before writing the file. `bun run shoot <game> --view <name>` defaults to `--mode ui`, which renders the game's HUD preview over a CSS backdrop and creates no canvas; the whole-frame flat test was skipped without a region and the backdrop's full-screen paint withheld the viewport verdict, so a HUD-only frame was written with exit 0.
|
|
51
|
+
- `countStaticBatchDraws` (`@jgengine/shell/world/staticObjectBatches`) counts the draws the static batches submit, so a variant level with no instance in a cell and a cell the camera culled are no longer reported as draws. `submitInstanceCount` (`@jgengine/shell/render/submittedDraws`) is the one place the batch, scatter, proxy and authored-primitive paths set an instance count and its visibility, and three issues a draw only when both say there is something to draw.
|
|
52
|
+
- `createCastRunner.state()` carries the cast's `moveTolerance`, `moveDeadzone` and `movedTotal`, and `restore(next)` resumes it. A cast saved mid-fight used to come back with a fresh movement budget, so the caster could walk further than the ability allowed.
|
|
53
|
+
- `createDecayMeterSet.serialize()` / `restore(next)` carry each meter's value, base rate and `setRateModifier` multiplier. A save used to drop a tuned rate, so a cold biome's faster warmth drain reset to the config rate on load.
|
|
54
|
+
- `createAreaEffectField.restore(state)`, `createSelectionBookmarks.restore(snapshot)` and `createPlotPermissions.restore(snapshot)` are the state-in counterparts to their existing `serialize()` / `snapshot()`; restoring into a live field releases the members the saved state does not carry.
|
|
55
|
+
- `check-stateful-ratchet` names the round-trip gap separately and caps it: a `snapshot()` with no `restore()`/`reset()` fails as `round-trip` debt, `--write` refuses to raise that ceiling, and a group paid below its ceiling must lower it. `createDownedState` was baselined when the ratchet shipped and never revisited; the restores in this release take the baseline from 113 entries to 93 and the `round-trip` ceiling from 8 to 2.
|
|
56
|
+
|
|
57
|
+
## 0.38.1
|
|
58
|
+
|
|
59
|
+
### Migrate
|
|
60
|
+
|
|
61
|
+
- **Bump lockstep SDK packages to `^0.38.1`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.35.1`; `@jgengine/github` is `0.25.1`.
|
|
62
|
+
- Nothing to migrate. `variantLevels()` was renamed to `ModelVariantConfig.shadows` before any release shipped it, so no published version ever exported it and no consumer can be holding one. A level that wants to stop paying the near level's shadow cost writes `{ url, shadows: "none" }`; leaving `shadows` out keeps the model's own answer.
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
|
|
66
|
+
- `ModelVariantConfig.shadows` (`@jgengine/core/world/modelLod`): a model variant level declares its own shadow participation as a plain field, `"inherit" | "cast" | "receive" | "both" | "none"`. `"inherit"`, the default, keeps the model's own `shadows`, so every existing model draws as before; a far level set to `"none"` leaves the shadow pass while still drawing its mesh.
|
|
67
|
+
- `ModelShadowMode` (`@jgengine/core/world/modelLod`) names the four-value shadow vocabulary, and `ModelConfig.shadows` uses it.
|
|
68
|
+
- `variantLevelModel` (`@jgengine/shell/render/singleVariantLod`) is the one place that resolves what a level draws and how it shadows; the single prop, batched prop and scatter paths all ask it, so a level's asset and its shadow answer cannot disagree.
|
|
69
|
+
|
|
70
|
+
### Fixed
|
|
71
|
+
|
|
72
|
+
- `SaveSlotsPane` cards are one box each: equal tracks, `grid-auto-rows: 1fr`, a card edge that reads over a bright scene, and a name that ellipsises instead of widening the card. A save with a long name and one with none produce the same card box.
|
|
73
|
+
- `GameMenu` puts the primary verbs on their own scrim, so their labels stay legible over whatever the live scene is showing instead of relying on a text shadow.
|
|
74
|
+
- The theme emits three menu tokens the slot card and the verb surface read: `--jg-menu-card-height` (`clamp(244px, 27vh, 300px)`), `--jg-menu-card-thumb` (`clamp(88px, 14vh, 160px)`) and `--jg-menu-surface-radius` (`8px`). A game overrides them in its own `HudTheme` instead of patching the menu components.
|
|
75
|
+
|
|
29
76
|
## 0.38.0
|
|
30
77
|
|
|
31
78
|
### Migrate
|
|
@@ -115,6 +115,13 @@ export interface AreaEffectField<P> {
|
|
|
115
115
|
clear(): AreaEffectEvent<P>[];
|
|
116
116
|
/** Snapshot the persistent membership lifecycle for save/load; shapes are transient and re-supplied via `setSource`. */
|
|
117
117
|
serialize(): AreaFieldState<P>;
|
|
118
|
+
/**
|
|
119
|
+
* Replace sources and membership with a {@link serialize} result — the save/load counterpart.
|
|
120
|
+
* Sources come back on a sentinel shape, so re-`setSource` live shapes before the next `step`.
|
|
121
|
+
* Members the restored state does not carry leave with reason `"cleared"`; survivors keep their
|
|
122
|
+
* refresh phase without an enter/leave churn.
|
|
123
|
+
*/
|
|
124
|
+
restore(state: AreaFieldState<P>): AreaEffectEvent<P>[];
|
|
118
125
|
}
|
|
119
126
|
/**
|
|
120
127
|
* Build a continuous area-effect field. Drive it with `setSource` (once per live source per tick, so
|
|
@@ -30,8 +30,9 @@ function shapeContains(shape, point) {
|
|
|
30
30
|
*/
|
|
31
31
|
export function createAreaEffectField(state) {
|
|
32
32
|
const sources = new Map();
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
function applyState(next) {
|
|
34
|
+
sources.clear();
|
|
35
|
+
for (const source of next.sources) {
|
|
35
36
|
sources.set(source.id, {
|
|
36
37
|
shape: SENTINEL_SHAPE,
|
|
37
38
|
payload: source.payload,
|
|
@@ -41,12 +42,14 @@ export function createAreaEffectField(state) {
|
|
|
41
42
|
members: new Map(),
|
|
42
43
|
});
|
|
43
44
|
}
|
|
44
|
-
for (const membership of
|
|
45
|
+
for (const membership of next.memberships) {
|
|
45
46
|
sources.get(membership.sourceId)?.members.set(membership.receiverId, {
|
|
46
47
|
sinceRefreshMs: membership.sinceRefreshMs,
|
|
47
48
|
});
|
|
48
49
|
}
|
|
49
50
|
}
|
|
51
|
+
if (state !== undefined)
|
|
52
|
+
applyState(state);
|
|
50
53
|
function leaveAll(rec, sourceId, reason) {
|
|
51
54
|
const events = [];
|
|
52
55
|
for (const receiverId of rec.members.keys()) {
|
|
@@ -171,5 +174,19 @@ export function createAreaEffectField(state) {
|
|
|
171
174
|
}
|
|
172
175
|
return out;
|
|
173
176
|
},
|
|
177
|
+
restore(next) {
|
|
178
|
+
const kept = new Set(next.memberships.map((membership) => `${membership.sourceId}\u0000${membership.receiverId}`));
|
|
179
|
+
const events = [];
|
|
180
|
+
for (const [sourceId, rec] of sources) {
|
|
181
|
+
for (const receiverId of [...rec.members.keys()]) {
|
|
182
|
+
if (kept.has(`${sourceId}\u0000${receiverId}`))
|
|
183
|
+
continue;
|
|
184
|
+
rec.members.delete(receiverId);
|
|
185
|
+
events.push({ kind: "leave", sourceId, receiverId, stackKey: rec.stackKey, payload: rec.payload, reason: "cleared" });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
applyState(next);
|
|
189
|
+
return events;
|
|
190
|
+
},
|
|
174
191
|
};
|
|
175
192
|
}
|
|
@@ -20,6 +20,12 @@ export interface CastBarSnapshot {
|
|
|
20
20
|
remainingMs: number;
|
|
21
21
|
/** Fill fraction in `[0, 1]` for the cast bar. */
|
|
22
22
|
fraction: number;
|
|
23
|
+
/** Movement budget this cast was begun with; `Infinity` for a cast that tolerates travel. */
|
|
24
|
+
moveTolerance: number;
|
|
25
|
+
/** The caster's noise floor this cast was begun with. */
|
|
26
|
+
moveDeadzone: number;
|
|
27
|
+
/** Travel already spent against `moveTolerance` (sub-deadzone jitter excluded). */
|
|
28
|
+
movedTotal: number;
|
|
23
29
|
}
|
|
24
30
|
export type CastInterruptReason = "moved" | "cancelled" | "replaced";
|
|
25
31
|
export type CastEvent = {
|
|
@@ -40,7 +46,14 @@ export interface CastRunner {
|
|
|
40
46
|
/** Start a cast; `false` while one is already in flight (interrupt or let it finish first). */
|
|
41
47
|
begin(config: CastConfig): boolean;
|
|
42
48
|
casting(): boolean;
|
|
49
|
+
/** The in-flight cast's timing and remaining move budget, or `null` when idle. */
|
|
43
50
|
state(): CastBarSnapshot | null;
|
|
51
|
+
/**
|
|
52
|
+
* Replace the in-flight cast with a snapshot from {@link state} — resuming a saved cast with the
|
|
53
|
+
* movement already spent against its budget, or clearing it with `null`. The derived `fraction`
|
|
54
|
+
* and `remainingMs` are recomputed; everything else is kept as saved.
|
|
55
|
+
*/
|
|
56
|
+
restore(next: CastBarSnapshot | null): void;
|
|
44
57
|
/** Interrupt the in-flight cast now; returns the interruption event, or `null` when idle. */
|
|
45
58
|
interrupt(reason?: CastInterruptReason): CastEvent | null;
|
|
46
59
|
/**
|
|
@@ -16,6 +16,9 @@ export function createCastRunner() {
|
|
|
16
16
|
elapsedMs: active.elapsedMs,
|
|
17
17
|
remainingMs: Math.max(0, active.castTimeMs - active.elapsedMs),
|
|
18
18
|
fraction,
|
|
19
|
+
moveTolerance: active.moveTolerance,
|
|
20
|
+
moveDeadzone: active.moveDeadzone,
|
|
21
|
+
movedTotal: active.movedTotal,
|
|
19
22
|
};
|
|
20
23
|
}
|
|
21
24
|
return {
|
|
@@ -34,6 +37,20 @@ export function createCastRunner() {
|
|
|
34
37
|
},
|
|
35
38
|
casting: () => active !== null,
|
|
36
39
|
state: snapshot,
|
|
40
|
+
restore(next) {
|
|
41
|
+
if (next === null) {
|
|
42
|
+
active = null;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
active = {
|
|
46
|
+
abilityId: next.abilityId,
|
|
47
|
+
castTimeMs: Math.max(0, next.castTimeMs),
|
|
48
|
+
moveTolerance: next.moveTolerance,
|
|
49
|
+
moveDeadzone: Math.max(0, next.moveDeadzone),
|
|
50
|
+
elapsedMs: Math.max(0, next.elapsedMs),
|
|
51
|
+
movedTotal: Math.max(0, next.movedTotal),
|
|
52
|
+
};
|
|
53
|
+
},
|
|
37
54
|
interrupt(reason = "cancelled") {
|
|
38
55
|
if (active === null)
|
|
39
56
|
return null;
|
package/dist/combat/downed.d.ts
CHANGED
|
@@ -30,7 +30,10 @@ export interface DownedState {
|
|
|
30
30
|
phase(instanceId: string): DownedPhase;
|
|
31
31
|
get(instanceId: string): DownedEntry | null;
|
|
32
32
|
clear(instanceId: string): void;
|
|
33
|
+
/** Every tracked entity's phase, remaining bleed-out, revive progress and banner timer. */
|
|
33
34
|
snapshot(): Record<string, DownedEntry>;
|
|
35
|
+
/** Replaces the tracked entities, resuming a saved bleed-out where it left off. */
|
|
36
|
+
restore(next: Record<string, DownedEntry>): void;
|
|
34
37
|
}
|
|
35
38
|
/**
|
|
36
39
|
* A downed/bleed-out state that ticks toward death and that teammates can revive before the timer runs out.
|
package/dist/combat/downed.js
CHANGED
|
@@ -105,5 +105,16 @@ export function createDownedState(config) {
|
|
|
105
105
|
out[instanceId] = { ...entry };
|
|
106
106
|
return out;
|
|
107
107
|
},
|
|
108
|
+
restore(next) {
|
|
109
|
+
entries.clear();
|
|
110
|
+
for (const [instanceId, entry] of Object.entries(next)) {
|
|
111
|
+
entries.set(instanceId, {
|
|
112
|
+
phase: entry.phase,
|
|
113
|
+
bleedoutRemaining: entry.bleedoutRemaining,
|
|
114
|
+
reviveProgress: entry.reviveProgress,
|
|
115
|
+
bannerRemaining: entry.bannerRemaining,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
},
|
|
108
119
|
};
|
|
109
120
|
}
|
|
@@ -3,8 +3,8 @@ import type { AmbienceZoneDef } from "../audio/ambience.js";
|
|
|
3
3
|
import type { AudioBusDef, SoundDef } from "../audio/audioFalloff.js";
|
|
4
4
|
import type { AudioOcclusionConfig } from "../audio/occlusion.js";
|
|
5
5
|
import type { MusicTheme } from "../audio/music.js";
|
|
6
|
-
import type { ModelVariantLodConfig } from "../world/modelLod.js";
|
|
7
|
-
export type { ModelVariantConfig, ModelVariantLodConfig } from "../world/modelLod.js";
|
|
6
|
+
import type { ModelShadowMode, ModelVariantLodConfig } from "../world/modelLod.js";
|
|
7
|
+
export type { ModelShadowMode, ModelVariantConfig, ModelVariantLodConfig } from "../world/modelLod.js";
|
|
8
8
|
import type { UiCueOverrides } from "../audio/uiCues.js";
|
|
9
9
|
import type { FeedbackCueOverrides } from "../audio/feedbackCues.js";
|
|
10
10
|
import type { NoticeFeedConfig } from "./notices.js";
|
|
@@ -278,7 +278,7 @@ export interface ModelConfig {
|
|
|
278
278
|
/** Hide named visual subtrees on this instance before placement measurement. Exact runtime names or unique original imported names resolve without removing bones or changing the cached model; missing or ambiguous names warn and leave existing visibility unchanged. Malformed lists reject before model or texture allocation. Omit or pass [] to retain imported visibility. */
|
|
279
279
|
hiddenNodes?: readonly string[];
|
|
280
280
|
/** Shadow participation of every mesh in the model. Default `"both"` (casts and receives); `"none"` for foliage cards, decals, or viewmodels that must never shadow the world. */
|
|
281
|
-
shadows?:
|
|
281
|
+
shadows?: ModelShadowMode;
|
|
282
282
|
/**
|
|
283
283
|
* Plays a GLTF animation clip on the model when the source has any (skinned or not).
|
|
284
284
|
* A model resolved from a catalog id whose index carries `clips` animates automatically —
|
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.38.
|
|
2
|
+
export declare const VERSION = "0.38.2";
|
|
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,47 @@
|
|
|
1
1
|
/** Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating. */
|
|
2
|
-
export const VERSION = "0.38.
|
|
2
|
+
export const VERSION = "0.38.2";
|
|
3
3
|
/** Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`). */
|
|
4
4
|
export const CHANGELOG = {
|
|
5
|
+
"0.38.2": {
|
|
6
|
+
migrate: [
|
|
7
|
+
"Bump lockstep SDK packages to `^0.38.2`: `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.35.2`; `@jgengine/github` is `0.25.2`.",
|
|
8
|
+
],
|
|
9
|
+
added: [
|
|
10
|
+
"`requestSceneCapture` and `requestSceneThumbnail` (`@jgengine/shell/render/sceneCapture`) ask the live scene for a frame or a save-card thumbnail. A request waits for `SceneCaptureBinding` to mount, waits for the canvas to paint, retries a blank read and resolves with the image or a stated failure (`no-binding`, `no-frame`, `undecodable`), so a game stops polling for the binding and waiting frames before a save screenshot.",
|
|
11
|
+
"`registerSceneCapture` (`@jgengine/shell/render/sceneCapture`) lets a host that owns its own canvas hand the capture path a reader.",
|
|
12
|
+
"`createDownedState` (`@jgengine/core/combat/downed`) exposes `restore(next)`, the counterpart to `snapshot()`, so a saved bleed-out resumes with the same remaining seconds, revive progress, phase and banner timer instead of being rebuilt around a guessed countdown.",
|
|
13
|
+
],
|
|
14
|
+
changed: [
|
|
15
|
+
"Pin Games to the 0.38.1 adoption",
|
|
16
|
+
"`GameMenu`'s `style` and `detailStyle` accept `--jg-*` custom properties (`GameMenuStyle`), so a game passes `{ \"--jg-menu-surface-radius\": \"12px\" }` without a cast.",
|
|
17
|
+
"`GameMenu` sets `pointer-events: auto` on its own root while its painted backdrop stays `none`, so a menu inside a HUD wrapper that passes clicks through to the scene no longer has dead buttons.",
|
|
18
|
+
"`check-stateful-ratchet` skips a factory carrying `// stateless: <reason>`, the named case for a catalog whose only closure state is an index of its own config. `createBuildSocketCatalog` declares it instead of being baselined.",
|
|
19
|
+
"`bun run shoot` refuses a scene shot whose page has no 3D canvas, and scores rendered geometry inside the canvas region before writing the file. `bun run shoot <game> --view <name>` defaults to `--mode ui`, which renders the game's HUD preview over a CSS backdrop and creates no canvas; the whole-frame flat test was skipped without a region and the backdrop's full-screen paint withheld the viewport verdict, so a HUD-only frame was written with exit 0.",
|
|
20
|
+
"`countStaticBatchDraws` (`@jgengine/shell/world/staticObjectBatches`) counts the draws the static batches submit, so a variant level with no instance in a cell and a cell the camera culled are no longer reported as draws. `submitInstanceCount` (`@jgengine/shell/render/submittedDraws`) is the one place the batch, scatter, proxy and authored-primitive paths set an instance count and its visibility, and three issues a draw only when both say there is something to draw.",
|
|
21
|
+
"`createCastRunner.state()` carries the cast's `moveTolerance`, `moveDeadzone` and `movedTotal`, and `restore(next)` resumes it. A cast saved mid-fight used to come back with a fresh movement budget, so the caster could walk further than the ability allowed.",
|
|
22
|
+
"`createDecayMeterSet.serialize()` / `restore(next)` carry each meter's value, base rate and `setRateModifier` multiplier. A save used to drop a tuned rate, so a cold biome's faster warmth drain reset to the config rate on load.",
|
|
23
|
+
"`createAreaEffectField.restore(state)`, `createSelectionBookmarks.restore(snapshot)` and `createPlotPermissions.restore(snapshot)` are the state-in counterparts to their existing `serialize()` / `snapshot()`; restoring into a live field releases the members the saved state does not carry.",
|
|
24
|
+
"`check-stateful-ratchet` names the round-trip gap separately and caps it: a `snapshot()` with no `restore()`/`reset()` fails as `round-trip` debt, `--write` refuses to raise that ceiling, and a group paid below its ceiling must lower it. `createDownedState` was baselined when the ratchet shipped and never revisited; the restores in this release take the baseline from 113 entries to 93 and the `round-trip` ceiling from 8 to 2.",
|
|
25
|
+
],
|
|
26
|
+
removed: [],
|
|
27
|
+
},
|
|
28
|
+
"0.38.1": {
|
|
29
|
+
migrate: [
|
|
30
|
+
"Bump lockstep SDK packages to `^0.38.1`: `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.35.1`; `@jgengine/github` is `0.25.1`.",
|
|
31
|
+
"Nothing to migrate. `variantLevels()` was renamed to `ModelVariantConfig.shadows` before any release shipped it, so no published version ever exported it and no consumer can be holding one. A level that wants to stop paying the near level's shadow cost writes `{ url, shadows: \"none\" }`; leaving `shadows` out keeps the model's own answer.",
|
|
32
|
+
],
|
|
33
|
+
added: [
|
|
34
|
+
"`ModelVariantConfig.shadows` (`@jgengine/core/world/modelLod`): a model variant level declares its own shadow participation as a plain field, `\"inherit\" | \"cast\" | \"receive\" | \"both\" | \"none\"`. `\"inherit\"`, the default, keeps the model's own `shadows`, so every existing model draws as before; a far level set to `\"none\"` leaves the shadow pass while still drawing its mesh.",
|
|
35
|
+
"`ModelShadowMode` (`@jgengine/core/world/modelLod`) names the four-value shadow vocabulary, and `ModelConfig.shadows` uses it.",
|
|
36
|
+
"`variantLevelModel` (`@jgengine/shell/render/singleVariantLod`) is the one place that resolves what a level draws and how it shadows; the single prop, batched prop and scatter paths all ask it, so a level's asset and its shadow answer cannot disagree.",
|
|
37
|
+
],
|
|
38
|
+
changed: [
|
|
39
|
+
"`SaveSlotsPane` cards are one box each: equal tracks, `grid-auto-rows: 1fr`, a card edge that reads over a bright scene, and a name that ellipsises instead of widening the card. A save with a long name and one with none produce the same card box.",
|
|
40
|
+
"`GameMenu` puts the primary verbs on their own scrim, so their labels stay legible over whatever the live scene is showing instead of relying on a text shadow.",
|
|
41
|
+
"The theme emits three menu tokens the slot card and the verb surface read: `--jg-menu-card-height` (`clamp(244px, 27vh, 300px)`), `--jg-menu-card-thumb` (`clamp(88px, 14vh, 160px)`) and `--jg-menu-surface-radius` (`8px`). A game overrides them in its own `HudTheme` instead of patching the menu components.",
|
|
42
|
+
],
|
|
43
|
+
removed: [],
|
|
44
|
+
},
|
|
5
45
|
"0.38.0": {
|
|
6
46
|
migrate: [
|
|
7
47
|
"Bump lockstep SDK packages to `^0.38.0`: `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.35.0`; `@jgengine/github` is `0.25.0`.",
|
|
@@ -56,6 +56,8 @@ export interface SelectionBookmarks {
|
|
|
56
56
|
prune(isValid: (id: string) => boolean): SelectionPruneResult;
|
|
57
57
|
/** A plain-data copy of every bookmark — the exact input {@link createSelectionBookmarks} restores. */
|
|
58
58
|
serialize(): SelectionBookmarkSnapshot;
|
|
59
|
+
/** Replace every bookmark with a {@link serialize} snapshot — re-deduped, empty sets dropped. */
|
|
60
|
+
restore(snapshot: SelectionBookmarkSnapshot): void;
|
|
59
61
|
}
|
|
60
62
|
/**
|
|
61
63
|
* Create a keyed bookmark store, optionally restored from a {@link serialize}
|
|
@@ -18,13 +18,16 @@ function dedupe(ids) {
|
|
|
18
18
|
*/
|
|
19
19
|
export function createSelectionBookmarks(snapshot) {
|
|
20
20
|
const groups = new Map();
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
function load(next) {
|
|
22
|
+
groups.clear();
|
|
23
|
+
for (const [key, ids] of Object.entries(next.bookmarks)) {
|
|
23
24
|
const list = dedupe(ids);
|
|
24
25
|
if (list.length > 0)
|
|
25
26
|
groups.set(key, list);
|
|
26
27
|
}
|
|
27
28
|
}
|
|
29
|
+
if (snapshot !== undefined)
|
|
30
|
+
load(snapshot);
|
|
28
31
|
return {
|
|
29
32
|
bind(key, ids) {
|
|
30
33
|
const list = dedupe(ids);
|
|
@@ -94,6 +97,7 @@ export function createSelectionBookmarks(snapshot) {
|
|
|
94
97
|
serialize: () => ({
|
|
95
98
|
bookmarks: Object.fromEntries([...groups].map(([key, list]) => [key, [...list]])),
|
|
96
99
|
}),
|
|
100
|
+
restore: load,
|
|
97
101
|
};
|
|
98
102
|
}
|
|
99
103
|
/**
|
|
@@ -32,6 +32,18 @@ export interface DecayMeterState {
|
|
|
32
32
|
/** 0..1 fill. */
|
|
33
33
|
fraction: number;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* The whole persisted state of a {@link DecayMeterSet}: every meter's value plus the play-time rate
|
|
37
|
+
* changes `setRate` and `setRateModifier` applied. A save that carries only values resumes a tuned
|
|
38
|
+
* meter at its config rate, which is how a cold biome's faster warmth drain vanished on load.
|
|
39
|
+
*/
|
|
40
|
+
export interface DecayMeterSetState {
|
|
41
|
+
values: DecayMeterValues;
|
|
42
|
+
/** Current base rate per meter id, including any `setRate` override. */
|
|
43
|
+
rates: Record<string, number>;
|
|
44
|
+
/** Rate multiplier per meter id from `setRateModifier`; a missing id means `1`. */
|
|
45
|
+
modifiers: Record<string, number>;
|
|
46
|
+
}
|
|
35
47
|
/** Set of named survival meters (hunger/thirst/…) that drain and refill over game time. */
|
|
36
48
|
export interface DecayMeterSet {
|
|
37
49
|
/** Drain/fill every meter by `rate * rateModifier * dt`. Call once per game tick. */
|
|
@@ -46,14 +58,21 @@ export interface DecayMeterSet {
|
|
|
46
58
|
setRateModifier(id: string, multiplier: number): void;
|
|
47
59
|
/** Moodles for every crossed threshold, worst-first per meter. */
|
|
48
60
|
moodles(): Moodle[];
|
|
49
|
-
/**
|
|
61
|
+
/** Every meter's numeric state for a HUD — the display view; {@link serialize} is the save pair. */
|
|
50
62
|
snapshot(): Record<string, DecayMeterState>;
|
|
63
|
+
/** Values, base rates and rate modifiers — the full state to persist; see {@link restore}. */
|
|
64
|
+
serialize(): DecayMeterSetState;
|
|
65
|
+
/**
|
|
66
|
+
* Put a {@link serialize} result back: values, base rates and rate modifiers. Meters the state
|
|
67
|
+
* omits keep their current value and rate, and a modifier it omits is cleared.
|
|
68
|
+
*/
|
|
69
|
+
restore(next: DecayMeterSetState): void;
|
|
51
70
|
ids(): readonly string[];
|
|
52
71
|
}
|
|
53
72
|
/**
|
|
54
|
-
* Plain-data meter values: `meter id → current value`.
|
|
55
|
-
*
|
|
56
|
-
*
|
|
73
|
+
* Plain-data meter values: `meter id → current value`. The value half of a save — a tuned meter
|
|
74
|
+
* also needs its rate and rate modifiers, which {@link DecayMeterSetState} carries and
|
|
75
|
+
* {@link createDecayMeterSet}'s `serialize`/`restore` hand back and forth.
|
|
57
76
|
*/
|
|
58
77
|
export type DecayMeterValues = Record<string, number>;
|
|
59
78
|
/**
|
|
@@ -160,6 +160,29 @@ export function createDecayMeterSet(configs) {
|
|
|
160
160
|
snapshot() {
|
|
161
161
|
return decayMeterSnapshot(values, defs);
|
|
162
162
|
},
|
|
163
|
+
serialize() {
|
|
164
|
+
const rates = {};
|
|
165
|
+
for (const def of defs)
|
|
166
|
+
rates[def.id] = def.rate;
|
|
167
|
+
return { values: { ...values }, rates, modifiers: { ...modifiers } };
|
|
168
|
+
},
|
|
169
|
+
restore(next) {
|
|
170
|
+
const restored = {};
|
|
171
|
+
for (const def of defs) {
|
|
172
|
+
const rate = next.rates[def.id];
|
|
173
|
+
if (rate !== undefined)
|
|
174
|
+
def.rate = rate;
|
|
175
|
+
const modifier = next.modifiers[def.id];
|
|
176
|
+
if (modifier === undefined)
|
|
177
|
+
delete modifiers[def.id];
|
|
178
|
+
else
|
|
179
|
+
modifiers[def.id] = modifier;
|
|
180
|
+
const value = next.values[def.id];
|
|
181
|
+
if (value !== undefined)
|
|
182
|
+
restored[def.id] = value;
|
|
183
|
+
}
|
|
184
|
+
values = restored;
|
|
185
|
+
},
|
|
163
186
|
ids() {
|
|
164
187
|
return order;
|
|
165
188
|
},
|
|
@@ -26,6 +26,8 @@ export interface PlotPermissions {
|
|
|
26
26
|
setGuild(guildId: string | null, role?: BuildRole): void;
|
|
27
27
|
transferOwner(userId: string): void;
|
|
28
28
|
snapshot(): PlotPermissionSnapshot;
|
|
29
|
+
/** Replace owner, guild and grants from a {@link snapshot}; plot identity stays the config's. */
|
|
30
|
+
restore(next: PlotPermissionSnapshot): void;
|
|
29
31
|
}
|
|
30
32
|
export declare function createPlotPermissions(config: PlotPermissionConfig): PlotPermissions;
|
|
31
33
|
export type ContributionGoal = Readonly<Record<string, number>>;
|
|
@@ -52,6 +52,16 @@ export function createPlotPermissions(config) {
|
|
|
52
52
|
...(guildRole === undefined ? {} : { guildRole }),
|
|
53
53
|
};
|
|
54
54
|
},
|
|
55
|
+
restore(next) {
|
|
56
|
+
ownerId = next.ownerId;
|
|
57
|
+
guildId = next.guildId;
|
|
58
|
+
guildRole = next.guildRole;
|
|
59
|
+
roles.clear();
|
|
60
|
+
for (const [userId, role] of Object.entries(next.roles)) {
|
|
61
|
+
if (userId !== next.ownerId)
|
|
62
|
+
roles.set(userId, role);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
55
65
|
};
|
|
56
66
|
}
|
|
57
67
|
export function createContributionPool(goal) {
|
|
@@ -82,6 +82,7 @@ export function pieceSocketsFromModel(sockets, kindOf, yawOf) {
|
|
|
82
82
|
*
|
|
83
83
|
* @capability build-socket-catalog data-driven named-socket snap catalog for modular base building
|
|
84
84
|
*/
|
|
85
|
+
// stateless: pieces, rules and their lookup indexes are built once from `config` and never written after the factory returns, so the constructor is the whole state
|
|
85
86
|
export function createBuildSocketCatalog(config) {
|
|
86
87
|
const cellSize = config.cellSize ?? 1;
|
|
87
88
|
const pieces = new Map();
|
package/dist/world/modelLod.d.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import type { LodBand } from "./lod.js";
|
|
2
|
-
/**
|
|
2
|
+
/** How a model, or one of its variant levels, takes part in the shadow pass. */
|
|
3
|
+
export type ModelShadowMode = "cast" | "receive" | "both" | "none";
|
|
4
|
+
/** One mesh level of a {@link ModelVariantLodConfig}: a full or decimated GLB, how far it draws and how it shadows. */
|
|
3
5
|
export interface ModelVariantConfig {
|
|
4
6
|
/** GLB url for this level — the source asset for the near level, a decimated asset for the far ones. */
|
|
5
7
|
url: string;
|
|
6
8
|
/** Furthest distance in metres from the camera this level draws. The last level omits it and covers everything beyond the level before it. */
|
|
7
9
|
maxDistance?: number;
|
|
10
|
+
/**
|
|
11
|
+
* This level's shadow participation. `"inherit"` (the default) keeps the model's own `shadows`, so a
|
|
12
|
+
* level that says nothing draws exactly as it did before levels could choose. A far level set to
|
|
13
|
+
* `"none"` leaves the shadow pass entirely while still drawing its mesh.
|
|
14
|
+
*/
|
|
15
|
+
shadows?: ModelShadowMode | "inherit";
|
|
8
16
|
}
|
|
9
17
|
/**
|
|
10
18
|
* Ordered mesh detail levels for one model, switched per instance by camera distance. The first level
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/core",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.2",
|
|
4
4
|
"description": "jgengine core — pure-TypeScript game framework SDK (npm @jgengine/core). Entity stores, commands, combat, inventory, multiplayer seams. Zero dependencies. Not an ECS. Agent skills ship in the tarball. https://jgengine.com",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jgengine",
|
package/skills/jgengine/api.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
8
8
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
9
|
-
- `VERSION` (const): const VERSION: "0.38.
|
|
9
|
+
- `VERSION` (const): const VERSION: "0.38.2" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
10
10
|
|
|
11
11
|
## @jgengine/core/authoring
|
|
12
12
|
|
|
@@ -285,7 +285,7 @@
|
|
|
285
285
|
|
|
286
286
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
287
287
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
288
|
-
- `VERSION` (const): const VERSION: "0.38.
|
|
288
|
+
- `VERSION` (const): const VERSION: "0.38.2" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
289
289
|
|
|
290
290
|
## @jgengine/core/runtime/adapter
|
|
291
291
|
|
|
@@ -389,7 +389,7 @@
|
|
|
389
389
|
|
|
390
390
|
## @jgengine/core/combat/castRunner
|
|
391
391
|
|
|
392
|
-
- `CastBarSnapshot` (interface): interface CastBarSnapshot { abilityId: string; castTimeMs: number; elapsedMs: number; remainingMs: number; fraction: number } — ⚠ undocumented
|
|
392
|
+
- `CastBarSnapshot` (interface): interface CastBarSnapshot { abilityId: string; castTimeMs: number; elapsedMs: number; remainingMs: number; fraction: number; moveTolerance: number; moveDeadzone: number; movedTotal: number } — ⚠ undocumented
|
|
393
393
|
- `CastConfig` (interface): interface CastConfig { abilityId: string; castTimeMs: number; moveTolerance?: number; moveDeadzone?: number } — ⚠ undocumented
|
|
394
394
|
- `CastEvent` (type): type CastEvent = | { kind: "completed"; abilityId: string } | { kind: "interrupted"; abilityId: string; reason: CastInterruptReason } — ⚠ undocumented
|
|
395
395
|
- `CastInterruptReason` (type): type CastInterruptReason = "moved" | "cancelled" | "replaced" — ⚠ undocumented
|
|
@@ -922,7 +922,8 @@
|
|
|
922
922
|
- `ModelMaterialOverride` (interface): interface ModelMaterialOverride extends MaterialSurfaceParameters — Per-entity PBR material override (#151.3) applied to every standard or physical material in the model's cloned scene graph.
|
|
923
923
|
- `ModelPart` (interface): interface ModelPart — Static child model stacked at a fixed local offset under its parent's transform — no bone/rig resolution, unlike `ModelAttachment`. Assembles a compound entity (e.g. a modular castle wall + tower + roof) from several single-mesh kit pieces.
|
|
924
924
|
- `ModelRimLight` (interface): interface ModelRimLight — Fresnel rim-light term added on top of a model's lighting — see {@link ModelMaterialOverride.rim}.
|
|
925
|
-
- `
|
|
925
|
+
- `ModelShadowMode` (type): type ModelShadowMode = "cast" | "receive" | "both" | "none" — How a model, or one of its variant levels, takes part in the shadow pass.
|
|
926
|
+
- `ModelVariantConfig` (interface): interface ModelVariantConfig — One mesh level of a {@link ModelVariantLodConfig}: a full or decimated GLB, how far it draws and how it shadows.
|
|
926
927
|
- `ModelVariantLodConfig` (interface): interface ModelVariantLodConfig — Ordered mesh detail levels for one model, switched per instance by camera distance. The first level is the full mesh the model's own `url` names; every later level is a cheaper asset. A tree line uses this so near trees keep their full mesh while far ones draw a decimated copy of the same species.
|
|
927
928
|
- `ModelWindConfig` (interface): interface ModelWindConfig — Wind sway for a foliage model; see {@link ModelConfig.wind}.
|
|
928
929
|
- `MovementCommitFrame` (interface): interface MovementCommitFrame — One frame's movement resolution handed to `PlayerMovementConfig.beforeCommit`.
|
|
@@ -1341,7 +1342,7 @@
|
|
|
1341
1342
|
- `DEFAULT_RIDE_RATING_RANGE` (const): const DEFAULT_RIDE_RATING_RANGE: RideRatingRange — A 0–10 rating scale, the usual shape of a ride catalog. Override it when a park rates differently.
|
|
1342
1343
|
- `DEFAULT_TOUCH_STYLE` (const): const DEFAULT_TOUCH_STYLE: TouchStyle — Skin used when neither the game nor the player picks one.
|
|
1343
1344
|
- `DecayMeterSet` (interface): interface DecayMeterSet — Set of named survival meters (hunger/thirst/…) that drain and refill over game time.
|
|
1344
|
-
- `DecayMeterValues` (type): type DecayMeterValues = Record<string, number> — Plain-data meter values: `meter id → current value`.
|
|
1345
|
+
- `DecayMeterValues` (type): type DecayMeterValues = Record<string, number> — Plain-data meter values: `meter id → current value`. The value half of a save — a tuned meter also needs its rate and rate modifiers, which {@link DecayMeterSetState} carries and {@link createDecayMeterSet}'s `serialize`/`restore` hand back and forth.
|
|
1345
1346
|
- `DecayModifier` (type): type DecayModifier = number | Record<string, number> — Rate multiplier for {@link decayMeters}: one scalar applied to every meter (a member's metabolism, a game-mode harshness dial) or a per-meter record (cold biome → warmth only). `1` / omitted leaves the base rates unscaled.
|
|
1346
1347
|
- `DefendMissionOptions` (interface): interface DefendMissionOptions extends ComposedMissionOptions — Options for {@link defendMission}: hold a position, scored by what is left standing.
|
|
1347
1348
|
- `DeliveryEntry` (interface): interface DeliveryEntry { id: string; userId: string; inventoryId: string; items: readonly ItemStack[]; deliverAt: number } — ⚠ undocumented
|
|
@@ -2877,8 +2878,9 @@
|
|
|
2877
2878
|
|
|
2878
2879
|
- `DecayMeterConfig` (interface): interface DecayMeterConfig { id: string; max: number; min?: number; start?: number; rate: number; thresholds?: readonly MeterThreshold[] } — ⚠ undocumented · used by `createDecayMeterSet`: Named decay meters — hunger, thirst, oxygen, sanity, warmth, stamina.
|
|
2879
2880
|
- `DecayMeterSet` (interface): interface DecayMeterSet — Set of named survival meters (hunger/thirst/…) that drain and refill over game time.
|
|
2881
|
+
- `DecayMeterSetState` (interface): interface DecayMeterSetState — The whole persisted state of a {@link DecayMeterSet}: every meter's value plus the play-time rate changes `setRate` and `setRateModifier` applied. A save that carries only values resumes a tuned meter at its config rate, which is how a cold biome's faster warmth drain vanished on load.
|
|
2880
2882
|
- `DecayMeterState` (interface): interface DecayMeterState { id: string; value: number; max: number; min: number; fraction: number } — ⚠ undocumented · used by `decayMeterSnapshot`: Numeric state for every meter, keyed by id — the pure counterpart to {@link DecayMeterSet.snapshot}.
|
|
2881
|
-
- `DecayMeterValues` (type): type DecayMeterValues = Record<string, number> — Plain-data meter values: `meter id → current value`.
|
|
2883
|
+
- `DecayMeterValues` (type): type DecayMeterValues = Record<string, number> — Plain-data meter values: `meter id → current value`. The value half of a save — a tuned meter also needs its rate and rate modifiers, which {@link DecayMeterSetState} carries and {@link createDecayMeterSet}'s `serialize`/`restore` hand back and forth.
|
|
2882
2884
|
- `DecayModifier` (type): type DecayModifier = number | Record<string, number> — Rate multiplier for {@link decayMeters}: one scalar applied to every meter (a member's metabolism, a game-mode harshness dial) or a per-meter record (cold biome → warmth only). `1` / omitted leaves the base rates unscaled.
|
|
2883
2885
|
- `MeterThreshold` (interface): interface MeterThreshold { id: string; label: string; at: number; when: "below" | "above"; severity?: MoodleSeverity; icon?: string } — ⚠ undocumented
|
|
2884
2886
|
- `createDecayMeterSet` (function): function createDecayMeterSet(configs: readonly DecayMeterConfig[]): DecayMeterSet — Named decay meters — hunger, thirst, oxygen, sanity, warmth, stamina. Each drains (or recovers) on game-time `dt` at a configurable rate, refills from consumables or actions, and raises moodle statuses at thresholds. Rate modifiers let the environment drive them (colder → faster warmth loss; toxic biome → oxygen drops), so a game reads an environment field then calls `setRateModifier`.
|
|
@@ -2655,7 +2655,7 @@
|
|
|
2655
2655
|
- `ModelAssetRef` (interface): interface ModelAssetRef { url: string; anchor?: "center" | "origin"; dims?: ModelDims; collisionMesh?: CollisionMeshData; space?: AssetSpace; clips?: readonly string[] } — ⚠ undocumented · used by `createHostedWorldSessionAsync` (@jgengine/core/runtime/hostedWorldSession): Build a hosted session from an asynchronous persistence backend.
|
|
2656
2656
|
- `ModelDims` (interface): interface ModelDims — Measured horizontal footprint, footprint center, and lowest Y of a model in model space.
|
|
2657
2657
|
- `ModelNode` (interface): interface ModelNode — Generic named-socket reader for loaded 3D models. Walks a node tree (any object with `.name`, `.position`, and `.children` — structurally satisfied by `THREE.Object3D`) and collects the local offsets of nodes whose name marks an attachment point. Genre-agnostic: wire anchors on a pylon, muzzle/hand mounts on a character, hardpoints on a ship, seat/decal slots on furniture — anything an artist tags with an empty in the GLB. Pure data (no three.js import), so it lives in core.
|
|
2658
|
-
- `ModelVariantConfig` (interface): interface ModelVariantConfig — One mesh level of a {@link ModelVariantLodConfig}: a full or decimated GLB
|
|
2658
|
+
- `ModelVariantConfig` (interface): interface ModelVariantConfig — One mesh level of a {@link ModelVariantLodConfig}: a full or decimated GLB, how far it draws and how it shadows.
|
|
2659
2659
|
- `ModelVariantLodConfig` (interface): interface ModelVariantLodConfig — Ordered mesh detail levels for one model, switched per instance by camera distance. The first level is the full mesh the model's own `url` names; every later level is a cheaper asset. A tree line uses this so near trees keep their full mesh while far ones draw a decimated copy of the same species.
|
|
2660
2660
|
- `ModelVariantSelector` (interface): interface ModelVariantSelector — Per-instance distance switch over a model's detail levels: one answer per id, held across a boundary until the instance clears it.
|
|
2661
2661
|
- `ModelVariantSelectorState` (interface): interface ModelVariantSelectorState — Plain JSON state of a {@link ModelVariantSelector}: each id's current level index.
|
|
@@ -4041,7 +4041,8 @@
|
|
|
4041
4041
|
## @jgengine/core/world/modelLod
|
|
4042
4042
|
|
|
4043
4043
|
- `DEFAULT_VARIANT_HYSTERESIS` (const): const DEFAULT_VARIANT_HYSTERESIS: 6 — Metres an instance must travel past a switch boundary before it changes level unless the model declares its own.
|
|
4044
|
-
- `
|
|
4044
|
+
- `ModelShadowMode` (type): type ModelShadowMode = "cast" | "receive" | "both" | "none" — How a model, or one of its variant levels, takes part in the shadow pass.
|
|
4045
|
+
- `ModelVariantConfig` (interface): interface ModelVariantConfig — One mesh level of a {@link ModelVariantLodConfig}: a full or decimated GLB, how far it draws and how it shadows.
|
|
4045
4046
|
- `ModelVariantLodConfig` (interface): interface ModelVariantLodConfig — Ordered mesh detail levels for one model, switched per instance by camera distance. The first level is the full mesh the model's own `url` names; every later level is a cheaper asset. A tree line uses this so near trees keep their full mesh while far ones draw a decimated copy of the same species.
|
|
4046
4047
|
- `ModelVariantSelector` (interface): interface ModelVariantSelector — Per-instance distance switch over a model's detail levels: one answer per id, held across a boundary until the instance clears it.
|
|
4047
4048
|
- `ModelVariantSelectorState` (interface): interface ModelVariantSelectorState — Plain JSON state of a {@link ModelVariantSelector}: each id's current level index.
|