@jgengine/core 0.38.1 → 0.38.3
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 +39 -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/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +35 -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/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 +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -26,6 +26,45 @@ 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.3
|
|
30
|
+
|
|
31
|
+
### Migrate
|
|
32
|
+
|
|
33
|
+
- **Bump lockstep SDK packages to `^0.38.3`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.35.3`; `@jgengine/github` is `0.25.3`.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- `GameMenu`'s `style` and `detailStyle` accept a plain `CSSProperties` again (`CSSProperties | GameMenuStyle`). 0.38.2 typed them as `CSSProperties & Record<`--${string}`, ...>`, which a `CSSProperties` value cannot satisfy, so Harbor Heat and Beacon Bastion stopped typechecking on a patch release while they passed the same objects to `style` before it.
|
|
38
|
+
- `SceneCaptureBinding`'s props are a required object with an optional `bind` again. The 0.38.2 `{ bind } = {}` optional parameter made React's `createElement` overload infer `Attributes & undefined` for the props, so a game that builds its overlay as `createElement(SceneCaptureBinding, { bind })` failed with `'bind' does not exist in type 'Attributes'`; `<SceneCaptureBinding />` and `<SceneCaptureBinding bind={…} />` both still typecheck.
|
|
39
|
+
|
|
40
|
+
## 0.38.2
|
|
41
|
+
|
|
42
|
+
### Migrate
|
|
43
|
+
|
|
44
|
+
- **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`.
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- `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.
|
|
49
|
+
- `registerSceneCapture` (`@jgengine/shell/render/sceneCapture`) lets a host that owns its own canvas hand the capture path a reader.
|
|
50
|
+
- `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.
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
|
|
54
|
+
- Pin Games to the 0.38.1 adoption
|
|
55
|
+
- `GameMenu`'s `style` and `detailStyle` accept `--jg-*` custom properties (`GameMenuStyle`), so a game passes `{ "--jg-menu-surface-radius": "12px" }` without a cast.
|
|
56
|
+
- `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.
|
|
57
|
+
- `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.
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- `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.
|
|
62
|
+
- `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.
|
|
63
|
+
- `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.
|
|
64
|
+
- `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.
|
|
65
|
+
- `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.
|
|
66
|
+
- `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.
|
|
67
|
+
|
|
29
68
|
## 0.38.1
|
|
30
69
|
|
|
31
70
|
### 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
|
}
|
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.3";
|
|
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,41 @@
|
|
|
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.3";
|
|
3
3
|
/** Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`). */
|
|
4
4
|
export const CHANGELOG = {
|
|
5
|
+
"0.38.3": {
|
|
6
|
+
migrate: [
|
|
7
|
+
"Bump lockstep SDK packages to `^0.38.3`: `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.35.3`; `@jgengine/github` is `0.25.3`.",
|
|
8
|
+
],
|
|
9
|
+
added: [],
|
|
10
|
+
changed: [
|
|
11
|
+
"`GameMenu`'s `style` and `detailStyle` accept a plain `CSSProperties` again (`CSSProperties | GameMenuStyle`). 0.38.2 typed them as `CSSProperties & Record<`--${string}`, ...>`, which a `CSSProperties` value cannot satisfy, so Harbor Heat and Beacon Bastion stopped typechecking on a patch release while they passed the same objects to `style` before it.",
|
|
12
|
+
"`SceneCaptureBinding`'s props are a required object with an optional `bind` again. The 0.38.2 `{ bind } = {}` optional parameter made React's `createElement` overload infer `Attributes & undefined` for the props, so a game that builds its overlay as `createElement(SceneCaptureBinding, { bind })` failed with `'bind' does not exist in type 'Attributes'`; `<SceneCaptureBinding />` and `<SceneCaptureBinding bind={…} />` both still typecheck.",
|
|
13
|
+
],
|
|
14
|
+
removed: [],
|
|
15
|
+
},
|
|
16
|
+
"0.38.2": {
|
|
17
|
+
migrate: [
|
|
18
|
+
"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`.",
|
|
19
|
+
],
|
|
20
|
+
added: [
|
|
21
|
+
"`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.",
|
|
22
|
+
"`registerSceneCapture` (`@jgengine/shell/render/sceneCapture`) lets a host that owns its own canvas hand the capture path a reader.",
|
|
23
|
+
"`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.",
|
|
24
|
+
],
|
|
25
|
+
changed: [
|
|
26
|
+
"Pin Games to the 0.38.1 adoption",
|
|
27
|
+
"`GameMenu`'s `style` and `detailStyle` accept `--jg-*` custom properties (`GameMenuStyle`), so a game passes `{ \"--jg-menu-surface-radius\": \"12px\" }` without a cast.",
|
|
28
|
+
"`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.",
|
|
29
|
+
"`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.",
|
|
30
|
+
"`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.",
|
|
31
|
+
"`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.",
|
|
32
|
+
"`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.",
|
|
33
|
+
"`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.",
|
|
34
|
+
"`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.",
|
|
35
|
+
"`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.",
|
|
36
|
+
],
|
|
37
|
+
removed: [],
|
|
38
|
+
},
|
|
5
39
|
"0.38.1": {
|
|
6
40
|
migrate: [
|
|
7
41
|
"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`.",
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/core",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.3",
|
|
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.3" — 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.3" — 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
|
|
@@ -1342,7 +1342,7 @@
|
|
|
1342
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.
|
|
1343
1343
|
- `DEFAULT_TOUCH_STYLE` (const): const DEFAULT_TOUCH_STYLE: TouchStyle — Skin used when neither the game nor the player picks one.
|
|
1344
1344
|
- `DecayMeterSet` (interface): interface DecayMeterSet — Set of named survival meters (hunger/thirst/…) that drain and refill over game time.
|
|
1345
|
-
- `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.
|
|
1346
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.
|
|
1347
1347
|
- `DefendMissionOptions` (interface): interface DefendMissionOptions extends ComposedMissionOptions — Options for {@link defendMission}: hold a position, scored by what is left standing.
|
|
1348
1348
|
- `DeliveryEntry` (interface): interface DeliveryEntry { id: string; userId: string; inventoryId: string; items: readonly ItemStack[]; deliverAt: number } — ⚠ undocumented
|
|
@@ -2878,8 +2878,9 @@
|
|
|
2878
2878
|
|
|
2879
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.
|
|
2880
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.
|
|
2881
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}.
|
|
2882
|
-
- `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.
|
|
2883
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.
|
|
2884
2885
|
- `MeterThreshold` (interface): interface MeterThreshold { id: string; label: string; at: number; when: "below" | "above"; severity?: MoodleSeverity; icon?: string } — ⚠ undocumented
|
|
2885
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`.
|