@jgengine/core 0.15.0-next.0 → 0.16.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 +49 -0
- package/dist/audio/musicState.d.ts +47 -0
- package/dist/audio/musicState.js +38 -0
- package/dist/audio/stepCadence.d.ts +47 -0
- package/dist/audio/stepCadence.js +29 -0
- package/dist/audio/synth.d.ts +12 -0
- package/dist/game/playableGame.d.ts +36 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +167 -1
- package/dist/multiplayer.d.ts +1 -0
- package/dist/multiplayer.js +1 -0
- package/dist/runtime/context/objectSlots.d.ts +60 -0
- package/dist/runtime/context/objectSlots.js +103 -0
- package/dist/runtime/context/player.d.ts +3 -1
- package/dist/runtime/context/player.js +1 -0
- package/dist/runtime/context/scene.d.ts +9 -0
- package/dist/runtime/context/scene.js +14 -0
- package/dist/runtime/descriptors/baseline.d.ts +3 -1
- package/dist/runtime/descriptors/baseline.js +11 -0
- package/dist/runtime/gameContext.js +6 -0
- package/dist/runtime/gameContextTypes.d.ts +13 -0
- package/dist/runtime/hostPersistence.js +9 -5
- package/dist/runtime/objectRows.d.ts +17 -0
- package/dist/runtime/objectRows.js +35 -0
- package/dist/runtime/snapshot.d.ts +15 -0
- package/dist/runtime/snapshot.js +12 -0
- package/dist/scene/objectStore.d.ts +26 -0
- package/dist/scene/objectStore.js +51 -9
- package/dist/world/buildingKit.d.ts +110 -0
- package/dist/world/buildingKit.js +93 -0
- package/dist/world/buildings.d.ts +25 -3
- package/dist/world/buildings.js +39 -5
- package/dist/world/cityKind.d.ts +6 -0
- package/dist/world/cityKind.js +3 -0
- package/dist/world/environmentSummary.js +13 -3
- package/dist/world/features.d.ts +7 -1
- package/dist/world/features.js +1 -0
- package/dist/world.d.ts +1 -0
- package/dist/world.js +1 -0
- package/package.json +1 -1
- package/skills/jgengine/api.md +15 -3
- package/skills/jgengine/capabilities.md +12 -0
- package/skills/jgengine-gameplay/api.md +2 -1
- package/skills/jgengine-world/SKILL.md +5 -0
- package/skills/jgengine-world/api.md +40 -2
- package/skills/jgengine-world/capabilities.md +16 -0
- package/skills/jgengine-world/recipes/building-kit.md +78 -0
package/CHANGELOG.md
CHANGED
|
@@ -28,6 +28,49 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
28
28
|
|
|
29
29
|
### Migrate
|
|
30
30
|
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
## 0.16.0
|
|
38
|
+
|
|
39
|
+
### Migrate
|
|
40
|
+
|
|
41
|
+
- **Bump lockstep SDK packages to `^0.16.0`:** `@jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}`. CLI `jgengine` is `0.13.0`; `@jgengine/github` is `0.3.0`.
|
|
42
|
+
- **Placed objects now replicate.** `objects` joined the always-on baseline snapshot modules, so `ctx.hydrate` replaces the client's placed object set with the host's, the way `entities` already did. A game whose peers all run the same authored placement is unaffected; a game that places props on the client only, after hydrate, must move that placement to the host or keep it out of `ctx.scene.object`.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **`GeneratedBuilding`'s `kit` prop is now the `BuildingKit`**; the per-part React escape hatch it used to name moved to `partRenderer`, so `kit` means the same thing on `GeneratedBuilding` and `InstancedBuildings`. Rename the prop if you passed a `BuildingKitRenderer`.
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- **`modelLoadFallbacks()` from `@jgengine/shell/render/modelLoad`** lists every model the shared loader diagnosed as broken and replaced with a magenta placeholder. A placeholder was only ever a console warning, so a shipped game could render one in its starting area and nobody found out until someone looked at a screenshot; a capture host or smoke test can now require the list to be empty. `clearModelLoadFallbacks()` resets it between runs.
|
|
51
|
+
- **`worldHealthBars.occlude` / `WorldHealthBars`+`WorldNameplates` `occlude` prop.** World bars and nameplates are a screen-space overlay drawn outside the WebGL depth buffer, so nothing in the scene can hide them and an enemy's health bar reads straight through the building it stands behind. Set `occlude: true` to drop a bar when world geometry blocks the line from the player to the entity. Default `false` preserves the always-visible behaviour every shipped game currently relies on.
|
|
52
|
+
|
|
53
|
+
- **`DustField` + `WeatherLayer` mode `"dust"` (`@jgengine/shell/weather`) — wind-borne particulate.** `WeatherLayer` could only do `clear`/`rain`/`snow`/`mixed`, so an arid, volcanic, ashen, or blown-out world had no way to put anything in the air. Dust drifts on horizontal wind with a slow vertical bob rather than falling; `groundBias` packs density toward the surface, motes fade near the top of the volume so recycled ones do not pop against the sky, and faster motes ride higher. Set `dustAlways` to keep it in the air alongside rain or snow instead of replacing them. Defaults `DEFAULT_DUST_COUNT`/`DEFAULT_DUST_DENSITY` sit beside the rain and snow pair.
|
|
54
|
+
|
|
55
|
+
- **Authored `city` volumes render their near-LOD facades with a `BuildingKit`.** `CityRules.buildingKit` names a kit; a game registers the bound art with `registerBuildingKit(kit)` from `@jgengine/shell/structures/buildingKitRegistry` and the scene document carries only the name, so a city stays portable and free of model paths. Unset or unknown names keep the palette blocks. This puts the whole `city` primitive — not just the `building()` feature — on the kit path.
|
|
56
|
+
- **`BuildingKit` from `@jgengine/core/world/buildingKit`** binds a generated building's facade part slots to real models, so procedural buildings render as an instanced asset kit instead of untextured blocks. `defineBuildingKit({ parts: { wall: ["Wall_A.glb", …] } })` maps a part kind — or a `${facade}.${kind}` slot — to ordered variants that `kit.variant` indexes, with `fit`/`offset`/`rotation`/`scale`/`tint`/`autoOrient` per variant. Kinds a kit leaves unbound keep rendering the palette block, so a partial kit is a valid kit, and `omit` drops a kind entirely. Pass one through `building({ kit })` or straight to `InstancedBuildings`; `buildingKitVariantCounts(kit)` feeds `generateBuilding` so its seeded picks span exactly the models the kit binds.
|
|
57
|
+
- **`generateBuilding`, `generateBuildingDistrict`, and `createBuildingConfig` are now documented public API** (`@jgengine/core/world/buildings`) and appear in `capabilities.md` under `building-generator`.
|
|
58
|
+
- **`capture.views` on `defineGame`** — named framings (`GameCaptureView`: `look`, `lookFrom`, `spawn`, `state`, `run`, `settleMs`) that `shoot <game> --view <name>` and `drive --view <name>` replay exactly, so a before/after pair is the same view by construction. `shoot <game> --list-views` prints what a game declares; an unknown name fails listing them, and any explicit capture flag still wins over the view's value.
|
|
59
|
+
- **`modelLoadIdleMs()` from `@jgengine/shell/render/modelLoad`** reports how long the shared GLB loader has been idle (`0` while a model is in flight). Capture hosts wait on it instead of guessing a settle delay, so an establishing shot no longer races streaming models.
|
|
60
|
+
- **Per-instance state on placed objects** — `SceneObject.state` (opaque `Record`), set through `ctx.scene.object.setState`/`patchState`/`place({ state })`. It survives move/rotate/setVisual, replicates and saves with the object, and round-trips as `RuntimeObjectRow.flags` via `toRuntimeObjectRow`/`fromRuntimeObjectRow` (`@jgengine/core/multiplayer`). A chest's locked flag or a machine's fill level no longer needs a game-owned map keyed by `instanceId`.
|
|
61
|
+
- **`slotInventory` is a working feature** — `ctx.scene.object.slots` gives objects whose catalog entry declares it real per-instance container contents: `layout`/`state`/`count`, `put`/`take`, and `transferIn`/`transferOut` between a player inventory and the object, all validated against the declared slot count, `accepts` kind filter, and stack limits, with neither side mutated on a rejection. Contents live on `SceneObject.slots`, so they replicate and save with the placement.
|
|
62
|
+
- **`loadServerSnapshot` and `persistServerSnapshot` are public** (`@jgengine/convex/server`), and `createGameServerFunctions` also returns `helpers` (`loadSnapshot`, `applyCommand`, `persistSnapshot`, `runCommand`) bound to the same runtimes and auth mode. A host mutation can now pair snapshot work with its own table writes in one transaction; the registered `runCommand` mutation delegates to `helpers.runCommand`, so there is one implementation.
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- **Open storefronts are no longer holes through the building.** `generateBuilding` emitted an open ground-floor store as a lone `storefront` panel with no wall behind it, while every upper-floor window was correctly backed by one. Storefront and window materials render at 0.56 opacity and a generated building is a hollow shell, so those bays let you see straight through the building to the world beyond — and open stores only land on the street-facing facade, so a row of shops along a road was the worst case. The bay now emits its wall first, exactly as an upper floor does.
|
|
67
|
+
- **`joinServer`, `flushSave`, `flushDirtyServers`, and the `auto` autosave can write player profiles again.** All four cleared the snapshot's dirty set before handing it to the persist plan, which gates profile writes on exactly that set — so the functions named "flush" flushed the server row and chunks only, and an `onNewPlayer` grant (starting money, starting items) never reached `jgPlayerProfiles`. `joinServer` now persists the snapshot it just marked; the flush paths mark every player dirty first, since hydrating from storage resets the dirty set. `planServerPersist` also clears `dirtyAt` once everything outstanding is written, so a successful flush stops autosave from re-firing forever, and a profile patch no longer clobbers a host-maintained `dirtyAt`.
|
|
68
|
+
|
|
69
|
+
## 0.15.0
|
|
70
|
+
|
|
71
|
+
### Migrate
|
|
72
|
+
|
|
73
|
+
- **Bump lockstep SDK packages to `^0.15.0`:** `@jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}`. CLI `jgengine` is `0.12.0`; `@jgengine/github` is `0.2.0`.
|
|
31
74
|
- **`jgengine create` scaffolds a textured world by default (#1550).** `--world` is now the default and `--no-world` is the opt-out (matching `--no-editor`); `--world` is still accepted. The default scaffold previously emitted untextured proxy geometry, which the AGENTS.md it ships declares a failing result. `create` now also pulls the starter asset packs into `public/models` after install, and never fails the scaffold if that pull cannot run (offline, blocked proxy, `--no-install`) — it prints `npx assets pull starter` in next steps instead, and the emitted AGENTS.md documents the same command. Skip the pull explicitly with `--no-assets`.
|
|
32
75
|
- **Start game code at `@jgengine/shell/gameKit` (#1541).** Prefer that surface for `defineGame`, `GameHost`, stores, systems, authored-scene helpers, and HUD building blocks. `@jgengine/core/authoring` remains for pure core helpers the kit does not re-export but is no longer a competing start-here path.
|
|
33
76
|
- **Every game must now declare its run-phase story — silence is a gate error (#1337).** The run-phase
|
|
@@ -73,10 +116,16 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
73
116
|
|
|
74
117
|
### Added
|
|
75
118
|
|
|
119
|
+
- **`advanceStepCadence` (`@jgengine/core/audio/stepCadence`) — distance-driven footstep timing.** Pure state-in/state-out: feed it the ground distance a mover covered this frame and it reports how many steps landed and which foot led, so foley stays locked to distance instead of a wall-clock timer that desyncs the moment speed changes. Configurable stride, per-advance `strideScale` for crouch/sprint, and a shorter first step from rest so walking off is heard immediately.
|
|
120
|
+
- **`resolveMusicState` (`@jgengine/core/audio/musicState`) — combat-reactive soundtrack selection.** The engine could already crossfade themes but nothing decided *when*, so every game re-handrolled the threshold and got a track that flapped as one enemy walked in and out of range. Give it a tier ladder (intensity → theme) and it resolves the theme to play: escalation is immediate, de-escalation waits for the tier's `holdMs` and needs intensity to fall a `release` margin below the tier it is leaving. Pure and serialisable.
|
|
121
|
+
- **Synth voices take a `sustain` (`ToneVoice`/`NoiseVoice`).** Seconds held at full gain before the decay. Default 0 preserves every existing patch. Without it there was no way to author a *sustained* sound at all: every voice decayed to silence inside its duration, so a patch rendered once and looped as an ambient bed pulsed audibly once per loop. Set `sustain` to `duration` for a flat wind, rain, or machine-hum bed that loops seamlessly.
|
|
122
|
+
|
|
76
123
|
- **`ModelMaterialOverride.rim` — a fresnel rim light on any entity/object model.** `{ color, strength, power }` adds a bright edge where the surface turns away from the viewer, patched into the material's own shader so it shadows, fogs, and tonemaps with the body rather than floating in front of it. This is the standard fix for an actor that shares a value band with its ground: no albedo choice separates a body from terrain lit by the same key, but an edge does. Omit for no rim (default).
|
|
77
124
|
|
|
78
125
|
### Fixed
|
|
79
126
|
|
|
127
|
+
- **Noise voices longer than the shared noise buffer no longer go silent partway through.** The 1-second buffer is sampled from a random offset, so any `NoiseVoice` whose duration exceeded the remainder ran off the end and finished in silence — invisible on short impact bursts, fatal for anything sustained. The source now wraps; the voice still ends on time.
|
|
128
|
+
|
|
80
129
|
- **`terrain({ detail: { material: { tint } } })` separates a ground texture's grain from its hue.** A detail material's colour map used to replace the terrain's authored colour outright, so the biome palette, macro sweeps, and slope-rock blend under it were painted over and a whole region flattened to the texture's single hue — despite the API documenting that it "blends with, never replaces". `tint` defaults to 1 (the existing behaviour, so no shipped game changes); drop it toward 0 to keep the authored colour and take only the map's light and shade.
|
|
81
130
|
- **`sky({ gradientExponent, cloudiness })` are now authorable — and `sky()` no longer silently drops them.** `gradientExponent` controls how fast the gradient reaches `zenithColor` off the horizon (default 0.65, unchanged): below 1 pulls the zenith hue down into the frame, which is what an eye-level camera in flat terrain actually needs — at the default the sky reads as one horizon-coloured wash however saturated the authored zenith is. `cloudiness` (default 0.35, unchanged) is the procedural cloud-band opacity.
|
|
82
131
|
- **A model that throws while rendering is now reported instead of vanishing (#1588).** `ModelFallbackBoundary` swapped in its fallback (usually nothing) and stayed silent: `probes.fallbacks` stayed empty, no warning was logged, and an entity simply disappeared. It now reports a new `renderError` fallback cause on every failed render and warns once with the model url and the caught error.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hysteretic music-state selector: turns a continuous intensity signal (nearby threat,
|
|
3
|
+
* alert count, chase proximity) into a stable theme id. The engine can already crossfade
|
|
4
|
+
* a soundtrack; this decides *when*, without the flapping a bare threshold produces when
|
|
5
|
+
* one enemy walks in and out of range.
|
|
6
|
+
*/
|
|
7
|
+
export interface MusicTier {
|
|
8
|
+
id: string;
|
|
9
|
+
/** Theme id passed to `ctx.game.audio.music`; null silences the soundtrack in this tier. */
|
|
10
|
+
theme: string | null;
|
|
11
|
+
/** Intensity at or above which this tier may be entered. */
|
|
12
|
+
enter: number;
|
|
13
|
+
/** Minimum time in this tier before it may drop to a lower one. Default 0. */
|
|
14
|
+
holdMs?: number;
|
|
15
|
+
}
|
|
16
|
+
/** The tier ladder plus the release margin that keeps a hovering intensity from flapping between tiers. */
|
|
17
|
+
export interface MusicStateConfig {
|
|
18
|
+
/** Ordered or unordered; the highest `enter` at or below the intensity wins. */
|
|
19
|
+
tiers: readonly MusicTier[];
|
|
20
|
+
/** Intensity must fall this far below a tier's `enter` before leaving it. Default 0.2. */
|
|
21
|
+
release?: number;
|
|
22
|
+
}
|
|
23
|
+
/** Which tier is playing and when it was entered, on the caller's clock. Serialisable. */
|
|
24
|
+
export interface MusicStateSnapshot {
|
|
25
|
+
tierId: string | null;
|
|
26
|
+
/** Timestamp the current tier was entered, on the caller's clock. */
|
|
27
|
+
sinceMs: number;
|
|
28
|
+
}
|
|
29
|
+
/** The current intensity signal and clock reading for one resolve. */
|
|
30
|
+
export interface MusicStateInput {
|
|
31
|
+
intensity: number;
|
|
32
|
+
nowMs: number;
|
|
33
|
+
}
|
|
34
|
+
/** Next state, the theme that should be playing, and whether this advance is the one that changed it. */
|
|
35
|
+
export interface MusicStateResult {
|
|
36
|
+
state: MusicStateSnapshot;
|
|
37
|
+
theme: string | null;
|
|
38
|
+
/** True only on the advance the tier changed — the caller's cue to crossfade. */
|
|
39
|
+
changed: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** Fresh state with no tier entered, so the first resolve always settles and reports `changed`. */
|
|
42
|
+
export declare function createMusicState(): MusicStateSnapshot;
|
|
43
|
+
/**
|
|
44
|
+
* Pick the theme for the current intensity. Escalation is immediate; de-escalation waits for the
|
|
45
|
+
* tier's `holdMs` and needs the intensity to fall a `release` margin below the tier it is leaving.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveMusicState(state: MusicStateSnapshot, input: MusicStateInput, config: MusicStateConfig): MusicStateResult;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Fresh state with no tier entered, so the first resolve always settles and reports `changed`. */
|
|
2
|
+
export function createMusicState() {
|
|
3
|
+
return { tierId: null, sinceMs: 0 };
|
|
4
|
+
}
|
|
5
|
+
function tierById(config, id) {
|
|
6
|
+
return id === null ? undefined : config.tiers.find((tier) => tier.id === id);
|
|
7
|
+
}
|
|
8
|
+
function highestTierAt(config, intensity) {
|
|
9
|
+
let best;
|
|
10
|
+
for (const tier of config.tiers) {
|
|
11
|
+
if (intensity < tier.enter)
|
|
12
|
+
continue;
|
|
13
|
+
if (best === undefined || tier.enter > best.enter)
|
|
14
|
+
best = tier;
|
|
15
|
+
}
|
|
16
|
+
return best;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Pick the theme for the current intensity. Escalation is immediate; de-escalation waits for the
|
|
20
|
+
* tier's `holdMs` and needs the intensity to fall a `release` margin below the tier it is leaving.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveMusicState(state, input, config) {
|
|
23
|
+
const current = tierById(config, state.tierId);
|
|
24
|
+
const candidate = highestTierAt(config, input.intensity) ?? config.tiers.find((tier) => tier.enter <= 0);
|
|
25
|
+
if (current !== undefined && candidate !== undefined && candidate.enter < current.enter) {
|
|
26
|
+
const release = config.release ?? 0.2;
|
|
27
|
+
const heldLongEnough = input.nowMs - state.sinceMs >= (current.holdMs ?? 0);
|
|
28
|
+
if (!heldLongEnough || input.intensity > current.enter - release) {
|
|
29
|
+
return { state, theme: current.theme, changed: false };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const next = candidate ?? current;
|
|
33
|
+
if (next === undefined)
|
|
34
|
+
return { state, theme: null, changed: false };
|
|
35
|
+
if (next.id === state.tierId)
|
|
36
|
+
return { state, theme: next.theme, changed: false };
|
|
37
|
+
return { state: { tierId: next.id, sinceMs: input.nowMs }, theme: next.theme, changed: true };
|
|
38
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Distance-driven footstep cadence. Pure state-in/state-out: feed it how far a mover
|
|
3
|
+
* travelled this frame and it tells you how many steps landed, so foley stays locked to
|
|
4
|
+
* ground distance instead of a wall-clock timer that desyncs when speed changes.
|
|
5
|
+
*/
|
|
6
|
+
export interface StepCadenceConfig {
|
|
7
|
+
/** World units travelled per step at `strideScale` 1. */
|
|
8
|
+
strideDistance: number;
|
|
9
|
+
/**
|
|
10
|
+
* Fraction of a stride the first step after standing still lands at, so walking off
|
|
11
|
+
* from rest is heard immediately rather than half a stride later. Default 0.5.
|
|
12
|
+
*/
|
|
13
|
+
firstStepFraction?: number;
|
|
14
|
+
}
|
|
15
|
+
/** Cadence bookkeeping between advances — banked distance, last moving state, and the step count whose parity is the foot. */
|
|
16
|
+
export interface StepCadenceState {
|
|
17
|
+
/** Distance banked toward the next step. */
|
|
18
|
+
travelled: number;
|
|
19
|
+
/** Whether the mover was moving last advance — drives the from-rest first step. */
|
|
20
|
+
moving: boolean;
|
|
21
|
+
/** Monotonic step counter; its parity is the left/right foot. */
|
|
22
|
+
steps: number;
|
|
23
|
+
}
|
|
24
|
+
/** What the mover did this advance: ground distance covered, whether feet are planting, and the stride multiplier. */
|
|
25
|
+
export interface StepCadenceInput {
|
|
26
|
+
/** Ground distance covered since the last advance, in world units. */
|
|
27
|
+
distance: number;
|
|
28
|
+
/** False when the mover is airborne, downed, or otherwise not planting feet. */
|
|
29
|
+
moving: boolean;
|
|
30
|
+
/** Multiplies `strideDistance` — crouch shortens it, sprint lengthens it. Default 1. */
|
|
31
|
+
strideScale?: number;
|
|
32
|
+
}
|
|
33
|
+
/** Outcome of one advance: the next state plus how many steps landed and which foot led. */
|
|
34
|
+
export interface StepCadenceResult {
|
|
35
|
+
state: StepCadenceState;
|
|
36
|
+
/** Steps that landed this advance — 0 or 1 normally, more only on a very long frame. */
|
|
37
|
+
steps: number;
|
|
38
|
+
/** Parity of the first step landed this advance: 0 = left, 1 = right. */
|
|
39
|
+
foot: 0 | 1;
|
|
40
|
+
}
|
|
41
|
+
/** Fresh cadence state for a mover that is standing still. */
|
|
42
|
+
export declare function createStepCadenceState(): StepCadenceState;
|
|
43
|
+
/**
|
|
44
|
+
* Bank this advance's distance and report the steps it completed. Pure — same state and input
|
|
45
|
+
* always give the same result, so footstep timing is testable headless and replays identically.
|
|
46
|
+
*/
|
|
47
|
+
export declare function advanceStepCadence(state: StepCadenceState, input: StepCadenceInput, config: StepCadenceConfig): StepCadenceResult;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Fresh cadence state for a mover that is standing still. */
|
|
2
|
+
export function createStepCadenceState() {
|
|
3
|
+
return { travelled: 0, moving: false, steps: 0 };
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Bank this advance's distance and report the steps it completed. Pure — same state and input
|
|
7
|
+
* always give the same result, so footstep timing is testable headless and replays identically.
|
|
8
|
+
*/
|
|
9
|
+
export function advanceStepCadence(state, input, config) {
|
|
10
|
+
const stride = Math.max(config.strideDistance * (input.strideScale ?? 1), Number.EPSILON);
|
|
11
|
+
if (!input.moving) {
|
|
12
|
+
return { state: { travelled: 0, moving: false, steps: state.steps }, steps: 0, foot: (state.steps % 2) };
|
|
13
|
+
}
|
|
14
|
+
const fromRest = !state.moving;
|
|
15
|
+
const threshold = fromRest ? stride * (config.firstStepFraction ?? 0.5) : stride;
|
|
16
|
+
let travelled = state.travelled + Math.max(0, input.distance);
|
|
17
|
+
let steps = 0;
|
|
18
|
+
let remaining = threshold;
|
|
19
|
+
while (travelled >= remaining) {
|
|
20
|
+
travelled -= remaining;
|
|
21
|
+
steps += 1;
|
|
22
|
+
remaining = stride;
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
state: { travelled, moving: true, steps: state.steps + steps },
|
|
26
|
+
steps,
|
|
27
|
+
foot: (state.steps % 2),
|
|
28
|
+
};
|
|
29
|
+
}
|
package/dist/audio/synth.d.ts
CHANGED
|
@@ -23,6 +23,13 @@ export interface ToneVoice {
|
|
|
23
23
|
slideTo?: number;
|
|
24
24
|
/** Attack ramp in seconds. Default 0.012. */
|
|
25
25
|
attack?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Seconds held at full gain after the attack, before the decay to silence at `duration`.
|
|
28
|
+
* Default 0 — a plain percussive decay. Set it to `duration` for a flat, sustained bed:
|
|
29
|
+
* a looped buffer only reads as continuous if its amplitude does not fall across the loop,
|
|
30
|
+
* so ambient hums and wind beds need this or they pulse once per loop.
|
|
31
|
+
*/
|
|
32
|
+
sustain?: number;
|
|
26
33
|
}
|
|
27
34
|
/**
|
|
28
35
|
* A filtered white-noise burst — impacts, whooshes, breath, crackle. Realised
|
|
@@ -43,6 +50,11 @@ export interface NoiseVoice {
|
|
|
43
50
|
decay?: number;
|
|
44
51
|
/** Start offset in seconds from the cue trigger. Default 0. */
|
|
45
52
|
delay?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Seconds held at full gain before the decay begins. Default 0. Set it to `duration` for a
|
|
55
|
+
* flat noise bed (wind, rain, machine roar) that loops without pulsing.
|
|
56
|
+
*/
|
|
57
|
+
sustain?: number;
|
|
46
58
|
}
|
|
47
59
|
/** One layered voice of a synth cue. */
|
|
48
60
|
export type SynthVoice = ToneVoice | NoiseVoice;
|
|
@@ -340,6 +340,33 @@ export interface PresentationEffectsConfig {
|
|
|
340
340
|
shake?: boolean;
|
|
341
341
|
}
|
|
342
342
|
/** How a screenshot host reaches live gameplay in this game — the data behind `shoot --mode play`. */
|
|
343
|
+
/**
|
|
344
|
+
* One named framing in {@link GameCaptureConfig.views}: everything about a capture except the host-side
|
|
345
|
+
* viewport (`--device`, `--size`) and `--mode`, which the runner routes on before the game module loads.
|
|
346
|
+
* Every field mirrors the equivalent capture flag, and an explicit flag on the command line wins over the
|
|
347
|
+
* shot's value so a declared framing stays a starting point rather than a cage.
|
|
348
|
+
*/
|
|
349
|
+
export interface GameCaptureView {
|
|
350
|
+
/** One line naming what this shot is for — printed by `shoot <game> --list-views`. */
|
|
351
|
+
description?: string;
|
|
352
|
+
/** `--look`: `x,z`, `x,y,z`, `@marker:<id>`, or `@entity:<id>`. */
|
|
353
|
+
look?: string;
|
|
354
|
+
/** `--look-from`: `dist[,height[,angle]]`. */
|
|
355
|
+
lookFrom?: string;
|
|
356
|
+
/** `--spawn`: `x,y,z` or `x,y,z,yaw`, for a shot framed from the player's vantage instead of a detached camera. */
|
|
357
|
+
spawn?: string;
|
|
358
|
+
/** `--state`: a {@link GameCaptureConfig.states} entry to reach before capturing. */
|
|
359
|
+
state?: string;
|
|
360
|
+
/** `--run`: commands to dispatch before capturing, when the shot needs staging no declared state covers. */
|
|
361
|
+
run?: readonly string[];
|
|
362
|
+
/** `--settle`: milliseconds to wait before the frame, for a shot behind an animation. */
|
|
363
|
+
settleMs?: number;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* How a game makes itself screenshottable: the commands that reach live play or a named screen, the
|
|
367
|
+
* framings worth re-capturing, and the progress metrics a bot playtest samples. Declaring these once
|
|
368
|
+
* is what lets a capture host reproduce a view instead of re-deriving it by hand every run.
|
|
369
|
+
*/
|
|
343
370
|
export interface GameCaptureConfig {
|
|
344
371
|
/**
|
|
345
372
|
* Commands run (in order, via `ctx.game.commands.run`) right after boot when a capture host requests the
|
|
@@ -363,6 +390,13 @@ export interface GameCaptureConfig {
|
|
|
363
390
|
name: string;
|
|
364
391
|
input?: unknown;
|
|
365
392
|
})[]>;
|
|
393
|
+
/**
|
|
394
|
+
* Named framings worth re-capturing — the views a reviewer actually asks about (`street`, `keep-exterior`,
|
|
395
|
+
* `boss-arena`), each pinning the camera, the staging, and the settle for that shot. `shoot <game> --view <name>`
|
|
396
|
+
* reproduces one exactly, so a before/after pair is the same framing by construction instead of two hand-typed
|
|
397
|
+
* flag sets that drifted. An unknown name fails the capture listing the declared views.
|
|
398
|
+
*/
|
|
399
|
+
views?: Record<string, GameCaptureView>;
|
|
366
400
|
/** Extra milliseconds the capture host waits after `play` commands before taking the play-mode screenshot — cover an intro cinematic or spawn-in fade. Default 2500. */
|
|
367
401
|
settleMs?: number;
|
|
368
402
|
/** Bot-playtest read hook: maps live game state to a flat vector of numeric progress metrics the `drive --playtest` harness samples over time (e.g. `{ x, z, score, phase }`). Genre-agnostic — "progress" is whatever the game reports; the engine stays neutral. The harness watches these numbers move under scripted input: any metric changing beyond an epsilon counts as forward progress, all metrics flat past the softlock threshold under active input flags a softlock. Return every value that should count as advancing; omit to opt the game out of the playtest rung. */
|
|
@@ -422,6 +456,8 @@ export interface PlayableGame<TUi = unknown, TWorldOverlay = unknown, TRenderEnt
|
|
|
422
456
|
statId?: string;
|
|
423
457
|
roles?: readonly CatalogEntityRole[];
|
|
424
458
|
maxDistance?: number;
|
|
459
|
+
/** Hide a bar when world geometry stands between the player and the entity. These bars are a screen-space overlay outside the depth buffer, so by default they draw through walls. Default false. */
|
|
460
|
+
occlude?: boolean;
|
|
425
461
|
};
|
|
426
462
|
/**
|
|
427
463
|
* Opt in to billboarded nameplates (name + optional HP bar) floating over non-local entities — the
|
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.16.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[];
|