@jgengine/core 0.36.0 → 0.38.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 +147 -0
- package/dist/combat/projectiles.d.ts +6 -2
- package/dist/combat/projectiles.js +55 -52
- package/dist/combat/shotSpec.d.ts +239 -0
- package/dist/combat/shotSpec.js +216 -0
- package/dist/combat/weaponSystem.js +26 -21
- package/dist/combat.d.ts +1 -0
- package/dist/combat.js +1 -0
- package/dist/game/gameSave.d.ts +8 -2
- package/dist/game/gameSave.js +3 -2
- package/dist/game/playableGame.d.ts +9 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +93 -1
- package/dist/movement/characterController.d.ts +5 -0
- package/dist/movement/characterController.js +8 -1
- package/dist/movement/movementModel.js +4 -1
- package/dist/movement/playerMovement.js +4 -0
- package/dist/physics/physicsBackendConformance.js +17 -0
- package/dist/physics/physicsWorld.d.ts +7 -0
- package/dist/physics/physicsWorld.js +27 -6
- package/dist/run/runLoop.d.ts +57 -3
- package/dist/run/runLoop.js +95 -66
- package/dist/run/sessionLoop.d.ts +152 -0
- package/dist/run/sessionLoop.js +151 -0
- package/dist/world/features.d.ts +15 -1
- package/dist/world/features.js +51 -2
- package/dist/world/lod.d.ts +8 -0
- package/dist/world/lod.js +13 -5
- package/dist/world/modelLod.d.ts +44 -0
- package/dist/world/modelLod.js +75 -0
- package/dist/world.d.ts +2 -1
- package/dist/world.js +2 -1
- package/package.json +1 -1
- package/skills/jgengine/api.md +2 -2
- package/skills/jgengine-combat/SKILL.md +7 -0
- package/skills/jgengine-combat/api.md +61 -1
- package/skills/jgengine-combat/capabilities.md +4 -0
- package/skills/jgengine-combat/reference.md +1 -0
- package/skills/jgengine-gameplay/api.md +22 -0
- package/skills/jgengine-gameplay/capabilities.md +7 -1
- package/skills/jgengine-world/api.md +17 -0
package/dist/world/features.d.ts
CHANGED
|
@@ -214,7 +214,21 @@ export interface TerrainEnvironmentConfig {
|
|
|
214
214
|
materialRegions?: readonly TerrainMaterialRegion[];
|
|
215
215
|
/** Ordered z-banded ground palettes that cross-fade along the world's z axis (vale → marsh → peaks). Painted under `materialRegions`. */
|
|
216
216
|
biomeBands?: readonly BiomeBand[];
|
|
217
|
-
|
|
217
|
+
/**
|
|
218
|
+
* Grid segments per axis, or `[x, z]` for a non-square grid. Honoured as-is for compatibility,
|
|
219
|
+
* but a count says nothing about ground detail until you know the bounds — prefer `cellSize`.
|
|
220
|
+
*/
|
|
221
|
+
segments?: number | readonly [x: number, z: number];
|
|
222
|
+
/**
|
|
223
|
+
* Ground resolution as world units per quad — the density form of `segments`. The per-axis grid is
|
|
224
|
+
* derived from `bounds`, so the same `cellSize` on a bigger world spends more cells instead of
|
|
225
|
+
* stretching one fixed count over it. Set this or `segments`, never both.
|
|
226
|
+
*
|
|
227
|
+
* A world that needs mixed density next (a coarse far ring around a fine playable core, or bands
|
|
228
|
+
* that tighten over a painted path) would carry `density?: readonly { cellSize: number; radius?: number }[]`
|
|
229
|
+
* in place of this single number. Not built.
|
|
230
|
+
*/
|
|
231
|
+
cellSize?: number;
|
|
218
232
|
seed?: string;
|
|
219
233
|
frequency?: number;
|
|
220
234
|
octaves?: number;
|
package/dist/world/features.js
CHANGED
|
@@ -79,6 +79,53 @@ export function environment(config = {}) {
|
|
|
79
79
|
...(config.clearings === undefined || config.clearings.length === 0 ? {} : { clearings: config.clearings }),
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Vertex ceiling for a terrain grid derived from `cellSize`. A density target that reads fine
|
|
84
|
+
* ("0.4 m cells") can quietly ask for millions of vertices on a large world; past this the
|
|
85
|
+
* declaration fails with its own numbers instead of booting a ground mesh nobody can afford.
|
|
86
|
+
*/
|
|
87
|
+
const TERRAIN_VERTEX_CEILING = 1_000_000;
|
|
88
|
+
function gridVertices(bounds, cellSize) {
|
|
89
|
+
const x = Math.max(1, Math.round(bounds.w / cellSize));
|
|
90
|
+
const z = Math.max(1, Math.round(bounds.d / cellSize));
|
|
91
|
+
return { x, z, vertices: (x + 1) * (z + 1) };
|
|
92
|
+
}
|
|
93
|
+
/** Smallest `cellSize` whose derived grid fits the ceiling — quoted back in the failure, so the fix is one number. */
|
|
94
|
+
function minimumCellSize(bounds) {
|
|
95
|
+
let lo = Math.sqrt((bounds.w * bounds.d) / TERRAIN_VERTEX_CEILING) * 0.5;
|
|
96
|
+
let hi = Math.max(bounds.w, bounds.d, 1);
|
|
97
|
+
for (let step = 0; step < 64; step += 1) {
|
|
98
|
+
const mid = (lo + hi) / 2;
|
|
99
|
+
if (gridVertices(bounds, mid).vertices > TERRAIN_VERTEX_CEILING)
|
|
100
|
+
lo = mid;
|
|
101
|
+
else
|
|
102
|
+
hi = mid;
|
|
103
|
+
}
|
|
104
|
+
return hi;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Resolves the authored grid: `segments` passes through, `cellSize` derives a per-axis count from the
|
|
108
|
+
* bounds. Both forms in one declaration is an error rather than a silent precedence rule, because the
|
|
109
|
+
* pair produces exactly the accidental density this API exists to remove.
|
|
110
|
+
*/
|
|
111
|
+
function terrainSegments(bounds, config) {
|
|
112
|
+
const { segments, cellSize } = config;
|
|
113
|
+
if (segments !== undefined && cellSize !== undefined) {
|
|
114
|
+
throw new Error("terrain: set segments or cellSize, not both — cellSize (world units per quad) derives the grid from the bounds, segments counts it directly. Keep cellSize and drop segments.");
|
|
115
|
+
}
|
|
116
|
+
if (cellSize === undefined)
|
|
117
|
+
return segments;
|
|
118
|
+
if (!Number.isFinite(cellSize) || cellSize <= 0) {
|
|
119
|
+
throw new Error(`terrain: cellSize must be a positive number of world units per quad (got ${cellSize})`);
|
|
120
|
+
}
|
|
121
|
+
const { x, z, vertices } = gridVertices(bounds, cellSize);
|
|
122
|
+
if (vertices > TERRAIN_VERTEX_CEILING) {
|
|
123
|
+
// Rounded up: the quoted cell size has to pass the same check, not read well and fail again.
|
|
124
|
+
const minimum = Math.ceil(minimumCellSize(bounds) * 1_000) / 1_000;
|
|
125
|
+
throw new Error(`terrain: cellSize ${cellSize} on ${bounds.w}×${bounds.d} bounds is ${x}×${z} cells = ${vertices.toLocaleString("en-US")} vertices, over the ${TERRAIN_VERTEX_CEILING.toLocaleString("en-US")}-vertex ceiling. Use cellSize ${minimum} or larger, or shrink the bounds.`);
|
|
126
|
+
}
|
|
127
|
+
return x === z ? x : [x, z];
|
|
128
|
+
}
|
|
82
129
|
/** Declares a heightfield terrain patch for `environment()` — bounds, noise, materials, and flatten masks. */
|
|
83
130
|
export function terrain(config = {}) {
|
|
84
131
|
if (config.baseHeight !== undefined &&
|
|
@@ -87,9 +134,11 @@ export function terrain(config = {}) {
|
|
|
87
134
|
typeof console !== "undefined") {
|
|
88
135
|
console.warn("[jgengine:terrain] baseHeight is set but height is not — height is the noise amplitude (defaulting to 0 here), so this terrain is a flat plateau at baseHeight. Set height explicitly (height: 0 for an intentional plateau) to silence this.");
|
|
89
136
|
}
|
|
137
|
+
const bounds = config.bounds ?? { w: 512, d: 512 };
|
|
138
|
+
const segments = terrainSegments(bounds, config);
|
|
90
139
|
return withOptional({
|
|
91
140
|
kind: "terrain",
|
|
92
|
-
bounds
|
|
141
|
+
bounds,
|
|
93
142
|
height: config.height ?? 0,
|
|
94
143
|
}, {
|
|
95
144
|
...(config.heightMap === undefined ? {} : { heightMap: config.heightMap }),
|
|
@@ -98,7 +147,7 @@ export function terrain(config = {}) {
|
|
|
98
147
|
...(config.colors === undefined ? {} : { colors: config.colors }),
|
|
99
148
|
...(config.materialRegions === undefined ? {} : { materialRegions: config.materialRegions }),
|
|
100
149
|
...(config.biomeBands === undefined ? {} : { biomeBands: config.biomeBands }),
|
|
101
|
-
...(
|
|
150
|
+
...(segments === undefined ? {} : { segments }),
|
|
102
151
|
...(config.seed === undefined ? {} : { seed: config.seed }),
|
|
103
152
|
...(config.frequency === undefined ? {} : { frequency: config.frequency }),
|
|
104
153
|
...(config.octaves === undefined ? {} : { octaves: config.octaves }),
|
package/dist/world/lod.d.ts
CHANGED
|
@@ -20,4 +20,12 @@ export interface LodScheduler {
|
|
|
20
20
|
snapshot(): LodSchedulerState;
|
|
21
21
|
restore(next: LodSchedulerState): void;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Index of the band `distance` falls in, `-1` past the last band. Bands must already be sorted near
|
|
25
|
+
* to far. The scheduler and `createModelVariantSelector` both read distances through this one
|
|
26
|
+
* function, so simulation throttling and mesh detail cannot disagree about what band an id is in.
|
|
27
|
+
*/
|
|
28
|
+
export declare function lodBandIndex(bands: readonly {
|
|
29
|
+
maxDistance: number;
|
|
30
|
+
}[], distance: number): number;
|
|
23
31
|
export declare function createLodScheduler(config: LodSchedulerConfig): LodScheduler;
|
package/dist/world/lod.js
CHANGED
|
@@ -7,17 +7,25 @@ function hashSeed(text) {
|
|
|
7
7
|
}
|
|
8
8
|
return h >>> 0;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Index of the band `distance` falls in, `-1` past the last band. Bands must already be sorted near
|
|
12
|
+
* to far. The scheduler and `createModelVariantSelector` both read distances through this one
|
|
13
|
+
* function, so simulation throttling and mesh detail cannot disagree about what band an id is in.
|
|
14
|
+
*/
|
|
15
|
+
export function lodBandIndex(bands, distance) {
|
|
16
|
+
for (let i = 0; i < bands.length; i++) {
|
|
17
|
+
if (distance <= bands[i].maxDistance)
|
|
18
|
+
return i;
|
|
19
|
+
}
|
|
20
|
+
return -1;
|
|
21
|
+
}
|
|
10
22
|
export function createLodScheduler(config) {
|
|
11
23
|
const bands = [...config.bands].sort((a, b) => a.maxDistance - b.maxDistance);
|
|
12
24
|
const beyondInterval = config.beyondInterval ?? null;
|
|
13
25
|
const stagger = config.stagger ?? true;
|
|
14
26
|
const buckets = new Map();
|
|
15
27
|
function bandIndex(distance) {
|
|
16
|
-
|
|
17
|
-
if (distance <= bands[i].maxDistance)
|
|
18
|
-
return i;
|
|
19
|
-
}
|
|
20
|
-
return -1;
|
|
28
|
+
return lodBandIndex(bands, distance);
|
|
21
29
|
}
|
|
22
30
|
function effectiveInterval(band) {
|
|
23
31
|
return band === -1 ? beyondInterval : bands[band].interval;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { LodBand } from "./lod.js";
|
|
2
|
+
/** One mesh level of a {@link ModelVariantLodConfig}: a full or decimated GLB and how far it draws. */
|
|
3
|
+
export interface ModelVariantConfig {
|
|
4
|
+
/** GLB url for this level — the source asset for the near level, a decimated asset for the far ones. */
|
|
5
|
+
url: string;
|
|
6
|
+
/** Furthest distance in metres from the camera this level draws. The last level omits it and covers everything beyond the level before it. */
|
|
7
|
+
maxDistance?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Ordered mesh detail levels for one model, switched per instance by camera distance. The first level
|
|
11
|
+
* is the full mesh the model's own `url` names; every later level is a cheaper asset. A tree line uses
|
|
12
|
+
* this so near trees keep their full mesh while far ones draw a decimated copy of the same species.
|
|
13
|
+
*/
|
|
14
|
+
export interface ModelVariantLodConfig {
|
|
15
|
+
/** Levels ordered near to far; the first covers everything up to its `maxDistance`. Two or more. */
|
|
16
|
+
levels: readonly ModelVariantConfig[];
|
|
17
|
+
/** Metres an instance must travel past a switch boundary before it changes level, so a camera resting on the boundary cannot flicker. Default 6. */
|
|
18
|
+
hysteresis?: number;
|
|
19
|
+
}
|
|
20
|
+
/** Plain JSON state of a {@link ModelVariantSelector}: each id's current level index. */
|
|
21
|
+
export interface ModelVariantSelectorState {
|
|
22
|
+
levels: Record<string, number>;
|
|
23
|
+
}
|
|
24
|
+
/** Per-instance distance switch over a model's detail levels: one answer per id, held across a boundary until the instance clears it. */
|
|
25
|
+
export interface ModelVariantSelector {
|
|
26
|
+
/** Levels near to far, with the last level's `maxDistance` normalized to `Infinity`. */
|
|
27
|
+
levels(): readonly {
|
|
28
|
+
url: string;
|
|
29
|
+
maxDistance: number;
|
|
30
|
+
}[];
|
|
31
|
+
/** The same boundaries as scheduler bands, so a `createLodScheduler` never throttles an id at a different distance than its mesh switches. */
|
|
32
|
+
bands(): readonly LodBand[];
|
|
33
|
+
/** Level index for `id` at `distance` metres, carrying hysteresis from its previous answer. */
|
|
34
|
+
level(id: string, distance: number): number;
|
|
35
|
+
forget(id: string): void;
|
|
36
|
+
clear(): void;
|
|
37
|
+
size(): number;
|
|
38
|
+
snapshot(): ModelVariantSelectorState;
|
|
39
|
+
restore(next: ModelVariantSelectorState): void;
|
|
40
|
+
}
|
|
41
|
+
/** Metres an instance must travel past a switch boundary before it changes level unless the model declares its own. */
|
|
42
|
+
export declare const DEFAULT_VARIANT_HYSTERESIS = 6;
|
|
43
|
+
/** Builds the per-instance distance switch for a model's variant levels. */
|
|
44
|
+
export declare function createModelVariantSelector(config: ModelVariantLodConfig): ModelVariantSelector;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { lodBandIndex } from "./lod.js";
|
|
2
|
+
/** Metres an instance must travel past a switch boundary before it changes level unless the model declares its own. */
|
|
3
|
+
export const DEFAULT_VARIANT_HYSTERESIS = 6;
|
|
4
|
+
function normalize(config) {
|
|
5
|
+
if (!Array.isArray(config.levels) || config.levels.length < 2) {
|
|
6
|
+
throw new Error("Model variants need at least two levels: the full mesh and a cheaper one");
|
|
7
|
+
}
|
|
8
|
+
let previous = 0;
|
|
9
|
+
return config.levels.map((level, index) => {
|
|
10
|
+
if (typeof level.url !== "string" || level.url.length === 0) {
|
|
11
|
+
throw new Error(`Model variant level ${index} needs a url`);
|
|
12
|
+
}
|
|
13
|
+
const last = index === config.levels.length - 1;
|
|
14
|
+
const maxDistance = level.maxDistance;
|
|
15
|
+
if (last) {
|
|
16
|
+
if (maxDistance !== undefined && maxDistance !== Infinity) {
|
|
17
|
+
throw new Error("The last model variant level draws at every distance; leave its maxDistance undefined");
|
|
18
|
+
}
|
|
19
|
+
return { url: level.url, maxDistance: Infinity };
|
|
20
|
+
}
|
|
21
|
+
if (typeof maxDistance !== "number" || !Number.isFinite(maxDistance) || maxDistance <= previous) {
|
|
22
|
+
throw new Error(`Model variant level ${index} needs a finite maxDistance above the level before it`);
|
|
23
|
+
}
|
|
24
|
+
previous = maxDistance;
|
|
25
|
+
return { url: level.url, maxDistance };
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Builds the per-instance distance switch for a model's variant levels. */
|
|
29
|
+
export function createModelVariantSelector(config) {
|
|
30
|
+
const levels = normalize(config);
|
|
31
|
+
const hysteresis = Math.max(0, config.hysteresis ?? DEFAULT_VARIANT_HYSTERESIS);
|
|
32
|
+
const bands = levels.map((level) => ({ maxDistance: level.maxDistance, interval: 0 }));
|
|
33
|
+
const current = new Map();
|
|
34
|
+
const last = levels.length - 1;
|
|
35
|
+
return {
|
|
36
|
+
levels() {
|
|
37
|
+
return levels;
|
|
38
|
+
},
|
|
39
|
+
bands() {
|
|
40
|
+
return bands;
|
|
41
|
+
},
|
|
42
|
+
level(id, distance) {
|
|
43
|
+
let index = current.get(id);
|
|
44
|
+
if (index === undefined) {
|
|
45
|
+
const band = lodBandIndex(bands, distance);
|
|
46
|
+
index = band === -1 ? last : band;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
while (index < last && distance > levels[index].maxDistance + hysteresis)
|
|
50
|
+
index += 1;
|
|
51
|
+
while (index > 0 && distance <= levels[index - 1].maxDistance - hysteresis)
|
|
52
|
+
index -= 1;
|
|
53
|
+
}
|
|
54
|
+
current.set(id, index);
|
|
55
|
+
return index;
|
|
56
|
+
},
|
|
57
|
+
forget(id) {
|
|
58
|
+
current.delete(id);
|
|
59
|
+
},
|
|
60
|
+
clear() {
|
|
61
|
+
current.clear();
|
|
62
|
+
},
|
|
63
|
+
size() {
|
|
64
|
+
return current.size;
|
|
65
|
+
},
|
|
66
|
+
snapshot() {
|
|
67
|
+
return { levels: Object.fromEntries(current) };
|
|
68
|
+
},
|
|
69
|
+
restore(next) {
|
|
70
|
+
current.clear();
|
|
71
|
+
for (const [id, index] of Object.entries(next.levels))
|
|
72
|
+
current.set(id, index);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
package/dist/world.d.ts
CHANGED
|
@@ -129,12 +129,13 @@ export { type Aabb, type AvoidZone } from "./world/geometry.js";
|
|
|
129
129
|
export { GRASS_SCHEMA } from "./world/grassKind.js";
|
|
130
130
|
export { resolveGridInstances } from "./world/gridInstances.js";
|
|
131
131
|
export { grounded } from "./world/grounded.js";
|
|
132
|
-
export { createLodScheduler } from "./world/lod.js";
|
|
132
|
+
export { createLodScheduler, lodBandIndex } from "./world/lod.js";
|
|
133
133
|
export { createAnnotationLayer, type AnnotationLayer, type AnnotationLayerOptions, type AnnotationSnapshot, type MapNote, type MapShapeAnnotation, type MapStroke, } from "./world/mapAnnotations.js";
|
|
134
134
|
export { mapLayerColor, type MapCellStates, type MapRoute, type MapZone } from "./world/mapLayers.js";
|
|
135
135
|
export { DEFAULT_MARKER_KINDS, createMarkerSet, createMarkerSource, markerKindStyle, type MapMarker, type MarkerCollection, type MarkerKindStyle, type MarkerSet, type MarkerSource, type MarkerSourceOptions, type MarkerView, } from "./world/markers.js";
|
|
136
136
|
export { bearingToCardinal, clampToMinimapEdge, compassBearing, headingToBearing, projectToMinimap, relativeBearing, unprojectFromMinimap, type Cardinal, type MinimapView, type WorldXZ, } from "./world/minimap.js";
|
|
137
137
|
export { DEFAULT_MINIMAP_PALETTE, bakeMinimapBackground, bakeMinimapImage, encodeBakePng, minimapBakeToPngDataUri, type MinimapBake, type MinimapBakeBounds, type MinimapBakeOptions, type MinimapBakePalette, type MinimapBakeSource, type MinimapBakeZone, } from "./world/minimapBake.js";
|
|
138
|
+
export { DEFAULT_VARIANT_HYSTERESIS, createModelVariantSelector, type ModelVariantConfig, type ModelVariantLodConfig, type ModelVariantSelector, type ModelVariantSelectorState, } from "./world/modelLod.js";
|
|
138
139
|
export { placeAlongPath } from "./world/pathInstances.js";
|
|
139
140
|
export { withPathProfiles, type PathHeightPolicy, type PathRetaining, type TerrainPathProfile, } from "./world/pathTerrain.js";
|
|
140
141
|
export { isPlaceWorld, resolveWorldPhysics, seedForPlace, world, type BoardGround, type BoardGroundSize, type FlatGround, type FlatGroundSize, type GroundConfig, type GroundGenerator, type GroundMode, type PlaceConfig, type PlaceWorldFeature, type ResolvedGround, type RoundGround, type RoundGroundSize, type SurfaceLaws, type VoxelGround, type VoxelGroundSize, } from "./world/place.js";
|
package/dist/world.js
CHANGED
|
@@ -129,12 +129,13 @@ export {} from "./world/geometry.js";
|
|
|
129
129
|
export { GRASS_SCHEMA } from "./world/grassKind.js";
|
|
130
130
|
export { resolveGridInstances } from "./world/gridInstances.js";
|
|
131
131
|
export { grounded } from "./world/grounded.js";
|
|
132
|
-
export { createLodScheduler } from "./world/lod.js";
|
|
132
|
+
export { createLodScheduler, lodBandIndex } from "./world/lod.js";
|
|
133
133
|
export { createAnnotationLayer, } from "./world/mapAnnotations.js";
|
|
134
134
|
export { mapLayerColor } from "./world/mapLayers.js";
|
|
135
135
|
export { DEFAULT_MARKER_KINDS, createMarkerSet, createMarkerSource, markerKindStyle, } from "./world/markers.js";
|
|
136
136
|
export { bearingToCardinal, clampToMinimapEdge, compassBearing, headingToBearing, projectToMinimap, relativeBearing, unprojectFromMinimap, } from "./world/minimap.js";
|
|
137
137
|
export { DEFAULT_MINIMAP_PALETTE, bakeMinimapBackground, bakeMinimapImage, encodeBakePng, minimapBakeToPngDataUri, } from "./world/minimapBake.js";
|
|
138
|
+
export { DEFAULT_VARIANT_HYSTERESIS, createModelVariantSelector, } from "./world/modelLod.js";
|
|
138
139
|
export { placeAlongPath } from "./world/pathInstances.js";
|
|
139
140
|
export { withPathProfiles, } from "./world/pathTerrain.js";
|
|
140
141
|
export { isPlaceWorld, resolveWorldPhysics, seedForPlace, world, } from "./world/place.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
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.
|
|
9
|
+
- `VERSION` (const): const VERSION: "0.38.0" — 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.
|
|
288
|
+
- `VERSION` (const): const VERSION: "0.38.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
289
289
|
|
|
290
290
|
## @jgengine/core/runtime/adapter
|
|
291
291
|
|
|
@@ -31,6 +31,13 @@ see
|
|
|
31
31
|
For scene-backed `createProjectileSystem`, inject authoritative `rng`; spread
|
|
32
32
|
samples independent pellet rays and prediction stays on the center ray. The
|
|
33
33
|
weapon-plumbing recipe covers the bounded cone and cover contracts.
|
|
34
|
+
When a weapon is not a gun — grenade, mortar, arrow, hitscan explosive — state
|
|
35
|
+
how the shot travels and what it does on contact as one `ShotSpec`
|
|
36
|
+
(`combat/shotSpec`) and pass it as `ProjectileShotInput.spec`, or read
|
|
37
|
+
`resolveShotSpec` yourself. Delivery is `delivery.kind` ("trace" or "body") and
|
|
38
|
+
nothing else decides it; a splash, damage or falloff field never makes a shot
|
|
39
|
+
travel. A new weapon is a few fields, not a code path, so never branch combat
|
|
40
|
+
code on a weapon id or add a second classifier.
|
|
34
41
|
Tune how a weapon feels (recoil, bloom, ADS, shake and rumble) with the
|
|
35
42
|
[weapon-feel recipe](recipes/weapon-feel.md): feel target → `measureWeapon`
|
|
36
43
|
numbers → handling knobs, and `createWeaponFeedbackSignals` into the feedback mixer.
|
|
@@ -44,10 +44,15 @@
|
|
|
44
44
|
- `DEFAULT_ARMOR_HALF_VALUE` (const): const DEFAULT_ARMOR_HALF_VALUE: 100 — Default armor points at which a hit is halved.
|
|
45
45
|
- `DEFAULT_CRIT_MULTIPLIER` (const): const DEFAULT_CRIT_MULTIPLIER: 2 — Default crit multiplier for a zone that does not name one.
|
|
46
46
|
- `DEFAULT_EYE_HEIGHT` (const): const DEFAULT_EYE_HEIGHT: number — Shot-origin and first-person camera eye height above an entity's position: 90% of the default 1.8m hitbox top.
|
|
47
|
+
- `DEFAULT_FALLOFF_END_SCALE` (const): const DEFAULT_FALLOFF_END_SCALE: 0.5 — Damage multiplier left at the end of falloff when a shot names no end scale.
|
|
47
48
|
- `DEFAULT_FIRE_PULSE_SECONDS` (const): const DEFAULT_FIRE_PULSE_SECONDS: 0.12 — Default `RenderCueTuning.firePulseSeconds`.
|
|
48
49
|
- `DEFAULT_HIT_PULSE_SECONDS` (const): const DEFAULT_HIT_PULSE_SECONDS: 0.2 — Default `RenderCueTuning.hitPulseSeconds`.
|
|
49
50
|
- `DEFAULT_MAX_MITIGATION` (const): const DEFAULT_MAX_MITIGATION: 0.9 — Default ceiling on armor mitigation, so armor never fully negates a hit.
|
|
50
51
|
- `DEFAULT_RENDER_CUES` (const): const DEFAULT_RENDER_CUES: Readonly<EntityRenderCues> — Neutral starting cue set: idle, unarmed, undamaged.
|
|
52
|
+
- `DEFAULT_SHOT_PENETRATION` (const): const DEFAULT_SHOT_PENETRATION: 0 — Most extra hits a shot may pass through when it names no penetration.
|
|
53
|
+
- `DEFAULT_SHOT_RADIUS` (const): const DEFAULT_SHOT_RADIUS: 0.5 — Hit-sphere radius of a body that names none, m.
|
|
54
|
+
- `DEFAULT_SHOT_RANGE` (const): const DEFAULT_SHOT_RANGE: 100 — Furthest a shot resolves when the delivery names no range, m.
|
|
55
|
+
- `DEFAULT_SHOT_SPEED` (const): const DEFAULT_SHOT_SPEED: 15 — Launch speed of a body that names none, m/s.
|
|
51
56
|
- `DEFAULT_VFX_PRESET` (const): const DEFAULT_VFX_PRESET: VfxPresetName — The fallback flavor used when a `vfx()` call names an unknown preset — so a typo still shows a spark instead of nothing.
|
|
52
57
|
- `DEFAULT_WEAPON_PELLETS_IN_FLIGHT` (const): const DEFAULT_WEAPON_PELLETS_IN_FLIGHT: 32 — Default live-pellet cap for a weapon that does not name one.
|
|
53
58
|
- `DamageChannelId` (type): type DamageChannelId = string — Caller-owned id for a damage channel — `"fire"`, `"ballistic"`, `"physical"`, `"corrosive"`. The engine never branches on the value; a channel is whatever a game's data declares.
|
|
@@ -123,6 +128,7 @@
|
|
|
123
128
|
- `RenderCueTuning` (interface): interface RenderCueTuning — Tuning knobs for `advanceMotionCues` / `useEntityRenderCues`; every field has a default, override only what a weapon/rig's feel needs to differ.
|
|
124
129
|
- `ResolvedMatchup` (interface): interface ResolvedMatchup — Fully-resolved matchup outputs (every axis populated) plus the contributions that produced them.
|
|
125
130
|
- `ResolvedShot` (interface): interface ResolvedShot — A shot's resolved firing geometry in world space: `origin` is the point the projectile/ray starts from and `direction` is its normalized aim vector, both computed by {@link resolveShot} from the shooter's position, facing, and the chosen origin policy.
|
|
131
|
+
- `ResolvedShotSpec` (interface): interface ResolvedShotSpec — A spec with every default filled in. Read `mode` and the branch it names, never the raw spec.
|
|
126
132
|
- `ResourcePool` (interface): interface ResourcePool { current(): number; max(): number; setMax(value: number): void; fraction(): number; canSpend(amount: number): boolean; spend(amount: number): boolean; gain(amount: number): number; set(value: number): void; tick(dtSeconds: number): void; i… — ⚠ undocumented · used by `createResourcePool`: A regenerating resource pool — mana, stamina, energy — that actions spend from and that refills over time.
|
|
127
133
|
- `RewardClaimSpec` (interface): interface RewardClaimSpec — How a `claimed` allocation exposes its pool: open first-come (`first`) or `reserved` to one recipient.
|
|
128
134
|
- `RewardGrant` (interface): interface RewardGrant — An authoritative, serializable award of one result stack to one recipient — carries `via` (the policy that produced it) and `private` (replication scope) so provenance and visibility survive save/load and viewer projection. Applying a grant twice with the same identity is the caller's idempotency contract; this seam only decides who is owed what.
|
|
@@ -130,7 +136,23 @@
|
|
|
130
136
|
- `RewardRecipient` (interface): interface RewardRecipient — A participant that may receive rewards — an opaque recipient id (player, team, party slot) with an optional split `weight` (shared policy), eligibility `tags`, and an `eligible` gate. Ineligible recipients are excluded before any allocation decision so they never perturb others' streams.
|
|
131
137
|
- `RewardResult` (interface): interface RewardResult — One indivisible reward to distribute among participants — an opaque result id (a rolled drop, a currency bundle, a quest token) with an optional stack `count` and free-form `tags` for eligibility and provenance. Allocation never interprets the id; generation (loot tables, roll formulas) owns what it means, keeping the seam genre-agnostic.
|
|
132
138
|
- `RewardShare` (interface): interface RewardShare — A `{ result, count }` slice of a reward — the unit shared pools and grants are expressed in.
|
|
139
|
+
- `SHOT_GRAVITY` (const): const SHOT_GRAVITY: 9.8 — Standard gravity used to scale `ShotBody.gravity`, m/s².
|
|
140
|
+
- `ShotArc` (interface): interface ShotArc extends ShotLaunch — Where a body lands and how long it took, in the same numbers the runtime simulates.
|
|
141
|
+
- `ShotBody` (interface): interface ShotBody — A travelling body simulated over time. Every dimension is stated; nothing is read off another field, so a slow arrow with a splash and a fast bullet without one are the same shape.
|
|
142
|
+
- `ShotContact` (interface): interface ShotContact — What a shot does on contact: damage, falloff, penetration, splash, ricochet, statuses.
|
|
143
|
+
- `ShotDamage` (interface): interface ShotDamage — One damage channel a shot applies on contact.
|
|
144
|
+
- `ShotDelivery` (type): type ShotDelivery = ShotTrace | ShotBody — How the shot reaches its target.
|
|
145
|
+
- `ShotFalloff` (interface): interface ShotFalloff — Linear damage falloff between two distances.
|
|
146
|
+
- `ShotHitOutcome` (interface): interface ShotHitOutcome — What one contact does: whether the shot still carries damage, at what scale, and whether it goes on.
|
|
147
|
+
- `ShotLaunch` (interface): interface ShotLaunch — Straight-line flight of a body, for a caller that only needs where it starts and how fast.
|
|
133
148
|
- `ShotOriginPolicy` (type): type ShotOriginPolicy = | { kind: "converge"; muzzle?: EntityPosition; height?: number } | { kind: "eye"; height?: number } | { kind: "legacy" } | { kind: "entity" } | { kind: "entityOffset"; offset: EntityPosition } | { kind: "muzzle"; offset?: EntityPosition } | { kind: "camera"; origin: EntityPos… — How a shot's world-space origin (and optional direction) is resolved before prediction/settlement. - `converge` — the shot leaves the gun `muzzle` yet still passes through whatever the shooter's sightline (crosshair) covers: origin is the muzzle offset, direction is bent from the muzzle to the eye ray's aim point. The projectile system's default for a free `{ yaw, pitch }` aim, so a bullet visibly comes from the barrel without missing the reticle. Needs a scene raycast to find the aim point (`convergeShot`); a bare `resolveShot` degrades to a straight muzzle ray. Passes an explicit `{ origin, direction }` aim through untouched. - `eye` — `aim.origin` when present, else the shooter's entity position raised to eye height; the shot traces the shooter's sightline, so what the crosshair covers is what gets hit. - `legacy` — `aim.origin` when present, else the shooter's raw entity position (feet). - `entity` — always the shooter's entity position. - `entityOffset` / `muzzle` — entity-local offset rotated by the shooter's yaw (muzzle on a weapon model). - `camera` — explicit camera/reticle world origin (and optional direction override). - `world` — absolute world origin.
|
|
149
|
+
- `ShotPenetration` (interface): interface ShotPenetration — Extra targets a hit passes through.
|
|
150
|
+
- `ShotRicochet` (interface): interface ShotRicochet — Bounces off the first surface contacted.
|
|
151
|
+
- `ShotSpec` (interface): interface ShotSpec — A shot as plain data: how it travels and what it does on arrival. Every field but `delivery.kind` is optional with a documented default, so a plain gun is three lines.
|
|
152
|
+
- `ShotSplash` (interface): interface ShotSplash — Area damage around the contact point.
|
|
153
|
+
- `ShotStatReader` (type): type ShotStatReader = (stat: string) => number | null — Reads a numeric stat by name; the shape every item registry already exposes.
|
|
154
|
+
- `ShotTrace` (interface): interface ShotTrace — An instant trace: the shot resolves on the tick it is fired and never travels.
|
|
155
|
+
- `ShotVec3` (type): type ShotVec3 = readonly [number, number, number] — One description of how a shot travels and what it does when it arrives, plus the single resolver that turns that description into behaviour. A weapon is data: a rifle, a grenade, a mortar and an arrow differ by fields here, never by a code path, a weapon id or a classifier that reads one stat as proof of another.
|
|
134
156
|
- `SpawnProvider` (type): type SpawnProvider = ( params: Readonly<Record<string, unknown>>, ctx: SpawnProviderContext, ) => readonly EncounterSpawnRequest[] — A spawn provider: given the phase's `params` and context, returns the spawn requests to emit on enter. Deterministic; reads authored scene points from `ctx.spawnPoints` rather than embedding coordinates.
|
|
135
157
|
- `SpawnProviderContext` (interface): interface SpawnProviderContext — Evaluation context a spawn provider receives when its phase is entered.
|
|
136
158
|
- `SquadCombat` (interface): interface SquadCombat — Drive a squad: cover by threat, flanking, fire discipline, suppression and retreat.
|
|
@@ -290,12 +312,18 @@
|
|
|
290
312
|
- `resolveReceivedDamage` (function): function resolveReceivedDamage(input: ReceivedDamageInput): ReceivedDamageResult — Run receiver-side modifiers over an incoming hit, in ascending `order`, recording every step. Amplification, reduction, caps, conversion, redirection, and immunity are all data — the engine applies whichever policies the target's modifiers declare and never branches on channel names. Pure over serializable data with bounded work (one pass over the modifier list); an `immune` policy short-circuits the remaining modifiers.
|
|
291
313
|
- `resolveResistance` (function): function resolveResistance<TCategory extends string = string, TProperty extends string = string>(matrix: ResistanceMatrix<TCategory, TProperty>, category: TCategory | string, targetProperties: readonly (TProperty | string)[]): ResistanceResult — ⚠ undocumented
|
|
292
314
|
- `resolveShot` (function): function resolveShot(deps: ShotOriginDeps, from: string, aim: Aim, policy: ShotOriginPolicy = { kind: "eye" }): ResolvedShot | null — Resolves a shot's world origin and direction from the shooter's eye or muzzle and an {@link Aim}, ready for `ctx.scene.raycast` — the aim half of a hitscan weapon.
|
|
315
|
+
- `resolveShotSpec` (function): function resolveShotSpec(spec: ShotSpec): ResolvedShotSpec — Fill every default and return the one shape firing paths act on. This is the only place that decides how a shot travels, so a delivery can never be guessed from a damage or splash field.
|
|
293
316
|
- `resolveStatusApplication` (function): function resolveStatusApplication(input: StatusApplicationInput): StatusApplicationOutcome — Resolve one status application: reject on immunity, roll landing chance against injected RNG, scale magnitude/duration by the matchup, then fold into any existing instance per the stacking policy. Deterministic given a seeded `rng`; pure over serializable data, so authority can run it and replicate only the returned `instance`.
|
|
294
317
|
- `resolveTableDamage` (function): function resolveTableDamage(table: DamageTableDef, input: { type: string; resistance: string; armor?: number; base?: number }): DamageTableRow — Resolve one hit through the table: type multiplier, then armor mitigation, and the base scaled by both. Formula order matters only for the numbers a UI shows — the product is the same.
|
|
295
318
|
- `resolveVfxPreset` (function): function resolveVfxPreset(name: string | undefined, overrides?: Partial<VfxPreset>): VfxPreset — Resolve a named visual flavor into a concrete `{ kind, color, durationMs?, radius? }`, with any provided `overrides` winning field by field. An unknown name resolves to {@link DEFAULT_VFX_PRESET} rather than throwing, so a flavor typo degrades to a visible spark. Used by `ctx.scene.entity.vfx({ preset })`; call it directly when you need the raw numbers (e.g. to seed a retained `vfxInstance` from the same vocabulary).
|
|
296
319
|
- `rollCheck` (function): function rollCheck(input: CheckInput, rng?: () => number): CheckResult — Resolve a tabletop-style pass/fail roll against a target number with modifiers and crit/fumble bands.
|
|
297
320
|
- `scoreCoverPoint` (function): function scoreCoverPoint(point: CoverPoint, query: CoverQuery): CoverChoice | null — Score one stand point against the threats the member remembers. `hidden` is the confidence-weighted share of remembered threats that cannot see the point, so a point that hides from the shooter that matters scores over one that hides from a fading one. Returns `null` when nothing it can hide from is worth considering.
|
|
298
321
|
- `selectCover` (function): function selectCover(points: readonly CoverPoint[], query: CoverQuery): CoverChoice | null — The best of `points` under one {@link CoverQuery}, ties broken by point id. Pass the candidate set the injected query already bounded; this scores what it is given.
|
|
322
|
+
- `shotArc` (function): function shotArc(shot: ResolvedShotSpec, origin: ShotVec3, direction: ShotVec3): ShotArc — Solve the path of a body from a launch pose. A trace has no path and fails loudly instead of returning a straight line a caller would mistake for an arc.
|
|
323
|
+
- `shotFalloffAt` (function): function shotFalloffAt(shot: ResolvedShotSpec, distance: number): number — Damage multiplier from falloff at `distance`; `1` with no falloff.
|
|
324
|
+
- `shotFuseSeconds` (function): function shotFuseSeconds(shot: ResolvedShotSpec): number | null — Number of seconds the body flies before its fuse ends the flight, or `null` when it flies to contact.
|
|
325
|
+
- `shotHitOutcome` (function): function shotHitOutcome(shot: ResolvedShotSpec, distance: number, hitIndex: number): ShotHitOutcome — What a contact at `distance` does to the `hitIndex`-th target it meets, counting the first as `0`. A trace or body stops once its penetration slots are spent, so penetration is a number, not a branch.
|
|
326
|
+
- `shotSpecFromStats` (function): function shotSpecFromStats(read: ShotStatReader): ShotSpec — Adapt the numeric stat keys older item data carries to a spec. This is the only place those keys are read, and delivery is decided by the keys that describe travel (`projectile.*`) alone: a splash radius or a damage number never turns a trace into a body.
|
|
299
327
|
- `startEncounter` (function): function startEncounter<TData>(config: EncounterConfig<TData>, state: EncounterState<TData>, ctx: EncounterContext = {}): EncounterStep<TData> — Start a created encounter: enter the first root phase, descending to its first leaf, and return the initial enter/spawn events. A no-op (with no events) on an already-started or empty encounter. Mutates and returns `state`.
|
|
300
328
|
- `statusEffectRemainingFraction` (function): function statusEffectRemainingFraction(view: StatusEffectView): number — Fraction of the countdown still remaining, clamped to `[0,1]` — the value a countdown ring fills to. Returns `0` when the duration is non-positive. Pure; safe to call every frame.
|
|
301
329
|
- `threatsFromMemory` (function): function threatsFromMemory(perception: PerceptionService, observerId: string, nowMs: number, options: ThreatReadOptions = {}): ThreatReading[] — Read one member's threat list out of its perception memory — the only source of threat. Sorted by confidence, then recency, then id, and bounded by `max`, so a crowded memory cannot grow the tick.
|
|
@@ -627,7 +655,7 @@
|
|
|
627
655
|
- `ProjectileObjectsDeps` (interface): interface ProjectileObjectsDeps { list(): readonly {instanceId: string; catalogId: string; position: readonly [number, number, number]; rotationY?: number;}[]; inBox?(min: EntityPosition, max: EntityPosition): readonly {instanceId: string; catalogId: string; position: rea… — ⚠ undocumented
|
|
628
656
|
- `ProjectilePrediction` (interface): interface ProjectilePrediction { hits: ProjectileHit[]; blocked?: boolean; origin?: EntityPosition; direction?: EntityPosition; firstImpact?: ProjectileHit | null } — ⚠ undocumented
|
|
629
657
|
- `ProjectileSettleReport` (interface): interface ProjectileSettleReport — Exactly-once settlement with launch identity and actual applied effects.
|
|
630
|
-
- `ProjectileShotInput` (interface): interface ProjectileShotInput { from: string; via: EffectVia; aim: Aim; effect: string; originPolicy?: ShotOriginPolicy; travel?: ProjectileTravelConfig } — ⚠ undocumented
|
|
658
|
+
- `ProjectileShotInput` (interface): interface ProjectileShotInput { from: string; via: EffectVia; spec?: ShotSpec; aim: Aim; effect: string; originPolicy?: ShotOriginPolicy; travel?: ProjectileTravelConfig } — ⚠ undocumented
|
|
631
659
|
- `ProjectileShotState` (interface): interface ProjectileShotState — Serializable captured launch and settlement state.
|
|
632
660
|
- `ProjectileSystem` (interface): interface ProjectileSystem { willHitProjectile(input: ProjectileShotInput): ProjectilePrediction; fireProjectile(input: ProjectileShotInput): string; settleProjectile(shotId: string): SettleResult; advanceProjectiles(dt: number, time: number): SettleResult[]; activeP… — ⚠ undocumented · used by `createProjectileSystem`: Spawn and advance projectiles each frame, resolving travel, lifetime, and hits.
|
|
633
661
|
- `ProjectileSystemDeps` (interface): interface ProjectileSystemDeps { effects: EffectSystem; spatial: CombatSpatialDeps; getStat(itemId: string, stat: string): number | null; raycast?: Raycast; sceneRaycast?: SceneRaycastApi; objects?: ProjectileObjectsDeps; entityCollidersOf?(instanceId: string): EntityCol… — ⚠ undocumented · used by `createProjectileSystem`: Spawn and advance projectiles each frame, resolving travel, lifetime, and hits.
|
|
@@ -721,6 +749,38 @@
|
|
|
721
749
|
- `eyeHeightFromColliders` (function): function eyeHeightFromColliders(set: EntityColliderSet | null | undefined): number — Eye height derived from a collider set: 90% of the tallest hitbox top, or the humanoid default when unknown.
|
|
722
750
|
- `resolveShot` (function): function resolveShot(deps: ShotOriginDeps, from: string, aim: Aim, policy: ShotOriginPolicy = { kind: "eye" }): ResolvedShot | null — Resolves a shot's world origin and direction from the shooter's eye or muzzle and an {@link Aim}, ready for `ctx.scene.raycast` — the aim half of a hitscan weapon.
|
|
723
751
|
|
|
752
|
+
## @jgengine/core/combat/shotSpec
|
|
753
|
+
|
|
754
|
+
- `DEFAULT_FALLOFF_END_SCALE` (const): const DEFAULT_FALLOFF_END_SCALE: 0.5 — Damage multiplier left at the end of falloff when a shot names no end scale.
|
|
755
|
+
- `DEFAULT_SHOT_PENETRATION` (const): const DEFAULT_SHOT_PENETRATION: 0 — Most extra hits a shot may pass through when it names no penetration.
|
|
756
|
+
- `DEFAULT_SHOT_RADIUS` (const): const DEFAULT_SHOT_RADIUS: 0.5 — Hit-sphere radius of a body that names none, m.
|
|
757
|
+
- `DEFAULT_SHOT_RANGE` (const): const DEFAULT_SHOT_RANGE: 100 — Furthest a shot resolves when the delivery names no range, m.
|
|
758
|
+
- `DEFAULT_SHOT_SPEED` (const): const DEFAULT_SHOT_SPEED: 15 — Launch speed of a body that names none, m/s.
|
|
759
|
+
- `ResolvedShotSpec` (interface): interface ResolvedShotSpec — A spec with every default filled in. Read `mode` and the branch it names, never the raw spec.
|
|
760
|
+
- `SHOT_GRAVITY` (const): const SHOT_GRAVITY: 9.8 — Standard gravity used to scale `ShotBody.gravity`, m/s².
|
|
761
|
+
- `ShotArc` (interface): interface ShotArc extends ShotLaunch — Where a body lands and how long it took, in the same numbers the runtime simulates.
|
|
762
|
+
- `ShotBody` (interface): interface ShotBody — A travelling body simulated over time. Every dimension is stated; nothing is read off another field, so a slow arrow with a splash and a fast bullet without one are the same shape.
|
|
763
|
+
- `ShotContact` (interface): interface ShotContact — What a shot does on contact: damage, falloff, penetration, splash, ricochet, statuses.
|
|
764
|
+
- `ShotDamage` (interface): interface ShotDamage — One damage channel a shot applies on contact.
|
|
765
|
+
- `ShotDelivery` (type): type ShotDelivery = ShotTrace | ShotBody — How the shot reaches its target.
|
|
766
|
+
- `ShotFalloff` (interface): interface ShotFalloff — Linear damage falloff between two distances.
|
|
767
|
+
- `ShotHitOutcome` (interface): interface ShotHitOutcome — What one contact does: whether the shot still carries damage, at what scale, and whether it goes on.
|
|
768
|
+
- `ShotLaunch` (interface): interface ShotLaunch — Straight-line flight of a body, for a caller that only needs where it starts and how fast.
|
|
769
|
+
- `ShotPenetration` (interface): interface ShotPenetration — Extra targets a hit passes through.
|
|
770
|
+
- `ShotRicochet` (interface): interface ShotRicochet — Bounces off the first surface contacted.
|
|
771
|
+
- `ShotSpec` (interface): interface ShotSpec — A shot as plain data: how it travels and what it does on arrival. Every field but `delivery.kind` is optional with a documented default, so a plain gun is three lines.
|
|
772
|
+
- `ShotSplash` (interface): interface ShotSplash — Area damage around the contact point.
|
|
773
|
+
- `ShotStatReader` (type): type ShotStatReader = (stat: string) => number | null — Reads a numeric stat by name; the shape every item registry already exposes.
|
|
774
|
+
- `ShotTrace` (interface): interface ShotTrace — An instant trace: the shot resolves on the tick it is fired and never travels.
|
|
775
|
+
- `ShotVec3` (type): type ShotVec3 = readonly [number, number, number] — One description of how a shot travels and what it does when it arrives, plus the single resolver that turns that description into behaviour. A weapon is data: a rifle, a grenade, a mortar and an arrow differ by fields here, never by a code path, a weapon id or a classifier that reads one stat as proof of another.
|
|
776
|
+
- `resolveShotSpec` (function): function resolveShotSpec(spec: ShotSpec): ResolvedShotSpec — Fill every default and return the one shape firing paths act on. This is the only place that decides how a shot travels, so a delivery can never be guessed from a damage or splash field.
|
|
777
|
+
- `shotArc` (function): function shotArc(shot: ResolvedShotSpec, origin: ShotVec3, direction: ShotVec3): ShotArc — Solve the path of a body from a launch pose. A trace has no path and fails loudly instead of returning a straight line a caller would mistake for an arc.
|
|
778
|
+
- `shotFalloffAt` (function): function shotFalloffAt(shot: ResolvedShotSpec, distance: number): number — Damage multiplier from falloff at `distance`; `1` with no falloff.
|
|
779
|
+
- `shotFuseSeconds` (function): function shotFuseSeconds(shot: ResolvedShotSpec): number | null — Number of seconds the body flies before its fuse ends the flight, or `null` when it flies to contact.
|
|
780
|
+
- `shotHitOutcome` (function): function shotHitOutcome(shot: ResolvedShotSpec, distance: number, hitIndex: number): ShotHitOutcome — What a contact at `distance` does to the `hitIndex`-th target it meets, counting the first as `0`. A trace or body stops once its penetration slots are spent, so penetration is a number, not a branch.
|
|
781
|
+
- `shotSpecFromStats` (function): function shotSpecFromStats(read: ShotStatReader): ShotSpec — Adapt the numeric stat keys older item data carries to a spec. This is the only place those keys are read, and delivery is decided by the keys that describe travel (`projectile.*`) alone: a splash radius or a damage number never turns a trace into a body.
|
|
782
|
+
- `shotSpecFromWeapon` (function): function shotSpecFromWeapon(definition: { delivery: | { kind: "hitscan"; maxRange: number } | { kind: "projectile"; speed: number; gravity?: number; drag?: number; lifetime: number; maxRange?: number }; damage: { type: string; base: number; statuses?: readonly string[] }; falloff?: ShotFalloff; }): … — Project an existing {@link WeaponDefinition} onto a spec, so a weapon authored as a definition and one authored as a spec reach the same firing paths.
|
|
783
|
+
|
|
724
784
|
## @jgengine/core/combat/squadCombat
|
|
725
785
|
|
|
726
786
|
- `SquadCombat` (interface): interface SquadCombat — Drive a squad: cover by threat, flanking, fire discipline, suppression and retreat.
|
|
@@ -263,6 +263,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
263
263
|
- `filterOutcomeFor` (function) · `import { filterOutcomeFor } from "@jgengine/core/combat"`
|
|
264
264
|
- `resolveClaim` (function) · `import { resolveClaim } from "@jgengine/core/combat"`
|
|
265
265
|
|
|
266
|
+
## shot-spec — resolve one shot description into explicit travel and contact behaviour
|
|
267
|
+
|
|
268
|
+
- `resolveShotSpec` (function) · `import { resolveShotSpec } from "@jgengine/core/combat"`
|
|
269
|
+
|
|
266
270
|
## spawn-instance-restore — rehydrate a creature stat instance from decoded snapshot data
|
|
267
271
|
|
|
268
272
|
- `restoreSpawnInstance` (function) · `import { restoreSpawnInstance } from "@jgengine/core/stats/spawnLevelStats"`
|
|
@@ -164,6 +164,7 @@ With `events: ctx.game.events` and `healthOf`, every hit and status tick raises
|
|
|
164
164
|
- **Reload states.** `reload()` starts or continues; `interruptReload()` suspends through `Magazine.pauseReload` keeping progress; `resumeReload()` continues from it; `cancelReload()` discards. `ammo()` returns loaded / capacity / reserve / state / reload fraction / burst remaining / cadence fraction, and `stats()` carries shots, rounds spent, pulls, reloads, drops and damage dealt — the whole ammo economy is readable without touching internals.
|
|
165
165
|
- **State.** `snapshot()` / `restore()` carry aim, handling, magazine, trigger, burst, pellets and counters as plain data; `retune(definition)` swaps rate, feel, damage and capacity without losing ammo or aim (a shrinking magazine discards rounds above the new capacity); a `devtoolsName` registers a one-line probe.
|
|
166
166
|
- **When to use which.** `createWeaponRuntime` (`combat/weaponFire`) is the minimal plumbing for a game that already owns its fire loop and raycaster; `createWeaponSystem` is the definition-driven version with fire modes, ballistics, reload states and ammo accounting. Both leave models, audio, muzzle flash, camera and input to the game; feel numbers come from `measureWeapon` and presentation from `weaponPresentation`.
|
|
167
|
+
- **Anything that is not a gun** is a `ShotSpec` (`combat/shotSpec`): `delivery` is `{ kind: "trace", range? }` or `{ kind: "body", speed?, gravity?, drag?, lifetime?, radius?, fuse?: number | "impact", homing?, range? }`, and `contact` carries `damage`, `falloff`, `penetration { count, keepDamage? }`, `splash { radius, falloff?, damageScale? }`, `ricochet { count, angleDeg?, damageScale? }`. Every field but `delivery.kind` has a documented default, so a rifle is `{ delivery: { kind: "trace" }, contact: { damage: { type: "kinetic", base: 24 }, falloff: { start: 25, end: 75, endScale: 0.4 } } }` and a grenade is a body with `fuse: 2` and `splash`. `resolveShotSpec` fills the defaults once, `shotArc` solves the path, and `shotFalloffAt` / `shotHitOutcome` answer damage per distance and per penetrated target. `ProjectileShotInput.spec` takes one directly; `shotSpecFromStats` adapts older numeric item keys in one place and never reads a splash or damage field to decide delivery.
|
|
167
168
|
- **Evidence** (`combat/weaponSystem.test.ts`): the test fixture at 600 rpm with a 30-round magazine empties in 2.9 s, climbs 17.0° pitch and 3.1° yaw over the magazine, is back to half climb in 0.37 s and within 5% in 0.6 s; bloom runs 2.0 → 8.0 → 12.0 mrad (cap) by the tenth shot and settles to base 0.63 s after the last shot. A hitscan and a projectile contact at the same distance produce the same 20 damage (30 on a 1.5x head zone), and one second of held fire cannot exceed four shots.
|
|
168
169
|
|
|
169
170
|
## Encounter sequencing
|
|
@@ -922,6 +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
|
+
- `ModelVariantConfig` (interface): interface ModelVariantConfig — One mesh level of a {@link ModelVariantLodConfig}: a full or decimated GLB and how far it draws.
|
|
926
|
+
- `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.
|
|
925
927
|
- `ModelWindConfig` (interface): interface ModelWindConfig — Wind sway for a foliage model; see {@link ModelConfig.wind}.
|
|
926
928
|
- `MovementCommitFrame` (interface): interface MovementCommitFrame — One frame's movement resolution handed to `PlayerMovementConfig.beforeCommit`.
|
|
927
929
|
- `MovementFeelConfig` (interface): interface MovementFeelConfig — Feel knobs for the built-in walk controller; see {@link PlayerMovementConfig.feel}.
|
|
@@ -2685,13 +2687,33 @@
|
|
|
2685
2687
|
## @jgengine/core/run/runLoop
|
|
2686
2688
|
|
|
2687
2689
|
- `RunChange` (interface): interface RunChange<T> — One change the next run can open with. `T` is the game's own payload (a tighter rival line, an unlocked part, a route modifier) - the loop only draws and remembers which id was drawn.
|
|
2690
|
+
- `RunChoice` (interface): interface RunChoice<T> — One change a run could open with, in the shape a choice screen shows: the id and its payload.
|
|
2688
2691
|
- `RunLoop` (interface): interface RunLoop<T> — A run's lifecycle: what it carries and can lose, and the change it hands the next run.
|
|
2689
2692
|
- `RunLoopConfig` (interface): interface RunLoopConfig<T> — Configuration for {@link createRunLoop}.
|
|
2690
2693
|
- `RunLoopState` (interface): interface RunLoopState — Everything a run loop stores as plain JSON: the run in progress plus lifetime totals.
|
|
2691
2694
|
- `RunOffer` (interface): interface RunOffer<T> — The change a run opens with, and which run it is.
|
|
2692
2695
|
- `RunOutcome` (type): type RunOutcome = "banked" | "lost" — How a run ended: what it carried reached the bank, or it was lost.
|
|
2696
|
+
- `RunProbe` (interface): interface RunProbe — Read-only handle a capture or devtools uses to prove a run loop is live, not merely imported.
|
|
2697
|
+
- `RunReadout` (interface): interface RunReadout — What the loop reports about the run it is playing, for a capture or devtools read.
|
|
2693
2698
|
- `RunSettlement` (interface): interface RunSettlement<T> — What one closed run banked or lost, and what the next run changes.
|
|
2694
2699
|
- `createRunLoop` (function): function createRunLoop<T>(config: RunLoopConfig<T>): RunLoop<T> — Bank-or-lose between runs, with one deterministic change per run drawn from an authored table.
|
|
2700
|
+
- `installRunProbe` (function): function installRunProbe<T>(loop: RunLoop<T>): () => void — Publish the loop's own readout on `window.__jgengineRun`, so a capture or devtools probe can state the live run, what it carries, what it banked or lost and the change it offers. Reads only; it never starts, settles or changes a run. Returns the uninstaller.
|
|
2701
|
+
|
|
2702
|
+
## @jgengine/core/run/sessionLoop
|
|
2703
|
+
|
|
2704
|
+
- `SessionChoice` (interface): interface SessionChoice<T> — One option the next session could open with, in the shape a choice screen shows.
|
|
2705
|
+
- `SessionLoop` (interface): interface SessionLoop<T = never, I = void, R = void> — One session's lifecycle: open it, carry a quantity, close it at its boundary, and optionally open the next one with an option the game authored.
|
|
2706
|
+
- `SessionLoopConfig` (interface): interface SessionLoopConfig<T = never, I = void, R = void> — Configuration for {@link createSessionLoop}.
|
|
2707
|
+
- `SessionLoopState` (interface): interface SessionLoopState — Everything a session loop stores as plain JSON.
|
|
2708
|
+
- `SessionOffer` (interface): interface SessionOffer<T> — The option a session opens with, and which session it is.
|
|
2709
|
+
- `SessionOfferOption` (interface): interface SessionOfferOption<T> — One option the next session may open with. `T` is the game's own payload (a tighter rival line, an unlocked part, a shorter route) - the loop only draws and remembers which id was drawn.
|
|
2710
|
+
- `SessionPhase` (type): type SessionPhase = "idle" | "open" | "closed" — Where a session stands: not opened yet, in progress, or closed at its boundary.
|
|
2711
|
+
- `SessionProbe` (interface): interface SessionProbe — Read-only handle a capture or devtools uses to prove a session loop is live, not merely imported.
|
|
2712
|
+
- `SessionReadout` (interface): interface SessionReadout — What the loop reports about the session it is playing, for a capture or devtools read.
|
|
2713
|
+
- `SessionSettlement` (interface): interface SessionSettlement<R = void, T = never> — What closing one session did, and what the next session opens with.
|
|
2714
|
+
- `SettlingSession` (interface): interface SettlingSession<I = void> — The session closing, handed to a game's own `settle`.
|
|
2715
|
+
- `createSessionLoop` (function): function createSessionLoop<T = never, I = void, R = void>(config: SessionLoopConfig<T, I, R> = {}): SessionLoop<T, I, R> — The shared shape of a game session, with the game's own rules supplied as config: a session opens, it may carry a quantity, its boundary closes it, and the game's `settle` callback decides what happens to what it carried. A park day, a night shift and a wave are the same base with a different `settle`, a different `kind` and, where nothing is at stake, `carries: false`.
|
|
2716
|
+
- `installSessionProbe` (function): function installSessionProbe<T, I, R>(loop: SessionLoop<T, I, R>): () => void — Publish the loop's own readout on `window.__jgengineRun`, so a capture or devtools probe can state which session is live, its phase, what it carries and the option it offers. Reads only; it never starts, closes or changes a session. Returns the uninstaller.
|
|
2695
2717
|
|
|
2696
2718
|
## @jgengine/core/session/contestedChannel
|
|
2697
2719
|
|
|
@@ -649,9 +649,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
649
649
|
- `rerollRules` (function) · `import { rerollRules } from "@jgengine/core/gameplay"`
|
|
650
650
|
- `selectRules` (function) · `import { selectRules } from "@jgengine/core/gameplay"`
|
|
651
651
|
|
|
652
|
-
## run-loop — bank or lose what a run carries, with one deterministic change per run
|
|
652
|
+
## run-loop — deprecated roguelike composition of session-loop - bank or lose what a run carries, with one deterministic change per run
|
|
653
653
|
|
|
654
654
|
- `createRunLoop` (function) · `import { createRunLoop } from "@jgengine/core/run/runLoop"`
|
|
655
|
+
- `installRunProbe` (function) · `import { installRunProbe } from "@jgengine/core/run/runLoop"`
|
|
655
656
|
|
|
656
657
|
## run-modifiers — a roguelike run built from stacking drafted modifier picks
|
|
657
658
|
|
|
@@ -677,6 +678,11 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
677
678
|
|
|
678
679
|
- `seededRng` (function) · `import { seededRng } from "@jgengine/core/gameplay"`
|
|
679
680
|
|
|
681
|
+
## session-loop — open and close a game session, settle the quantity it carried and optionally draw the next session's authored option
|
|
682
|
+
|
|
683
|
+
- `createSessionLoop` (function) · `import { createSessionLoop } from "@jgengine/core/run/sessionLoop"`
|
|
684
|
+
- `installSessionProbe` (function) · `import { installSessionProbe } from "@jgengine/core/run/sessionLoop"`
|
|
685
|
+
|
|
680
686
|
## set — a bounded keyed value in a caller-owned serializable record
|
|
681
687
|
|
|
682
688
|
- `setValue` (function) · `import { setValue } from "@jgengine/core/gameplay"`
|