@jgengine/core 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/combat/effects.d.ts +11 -4
- package/dist/combat/effects.js +24 -17
- package/dist/combat/magazine.d.ts +13 -0
- package/dist/combat/magazine.js +27 -0
- package/dist/combat/vfxPresets.d.ts +176 -0
- package/dist/combat/vfxPresets.js +74 -0
- package/dist/combat/weaponFire.d.ts +114 -0
- package/dist/combat/weaponFire.js +96 -0
- package/dist/combat.d.ts +4 -2
- package/dist/combat.js +3 -1
- package/dist/devtools/urlFlags.d.ts +22 -0
- package/dist/devtools/urlFlags.js +62 -0
- package/dist/game/clipRoles.d.ts +51 -0
- package/dist/game/clipRoles.js +142 -0
- package/dist/game/defineGame.d.ts +2 -2
- package/dist/game/partAnimation.d.ts +78 -0
- package/dist/game/partAnimation.js +147 -0
- package/dist/game/playableGame.d.ts +38 -5
- package/dist/game/progression.d.ts +2 -2
- package/dist/game/progression.js +4 -2
- package/dist/inventory/inventoryModel.d.ts +24 -8
- package/dist/inventory/inventoryModel.js +47 -8
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +25 -1
- package/dist/runtime/context/combat.js +2 -1
- package/dist/runtime/context/combatFx.js +15 -5
- package/dist/runtime/context/scene.d.ts +2 -1
- package/dist/runtime/context/scene.js +39 -1
- package/dist/runtime/gameContext.js +50 -4
- package/dist/runtime/gameContextTypes.d.ts +37 -4
- package/dist/runtime/headlessRunner.d.ts +13 -0
- package/dist/runtime/headlessRunner.js +14 -0
- package/dist/scene/assetCatalog.d.ts +2 -0
- package/dist/scene/colliders.d.ts +21 -0
- package/dist/scene/colliders.js +62 -0
- package/dist/scene/entityStats.js +9 -1
- package/dist/stats/statModifiers.d.ts +16 -0
- package/dist/stats/statModifiers.js +30 -1
- package/dist/stats/statPool.js +5 -2
- package/dist/ui/panelModel.d.ts +120 -0
- package/dist/ui/panelModel.js +168 -0
- package/dist/ui.d.ts +1 -0
- package/dist/ui.js +1 -0
- package/dist/world/buildingLots.d.ts +84 -0
- package/dist/world/buildingLots.js +119 -0
- package/dist/world/buildings.d.ts +6 -0
- package/dist/world/cityGenerator.d.ts +41 -0
- package/dist/world/cityGenerator.js +58 -0
- package/dist/world/cityKind.d.ts +2 -2
- package/dist/world/cityKind.js +4 -4
- package/dist/world/environmentSummary.js +57 -1
- package/dist/world/features.d.ts +35 -2
- package/dist/world/features.js +5 -2
- package/dist/world/roads.d.ts +19 -2
- package/dist/world/roads.js +60 -3
- package/dist/world/{pathNetwork.d.ts → streetGenerator.d.ts} +37 -37
- package/dist/world/{pathNetwork.js → streetGenerator.js} +9 -9
- package/dist/world/streets.d.ts +74 -1
- package/dist/world/streets.js +218 -0
- package/dist/world/terrain.d.ts +20 -1
- package/dist/world/terrain.js +41 -2
- package/package.json +1 -1
- package/skills/jgengine/SKILL.md +1 -0
- package/skills/jgengine/api.md +13 -5
- package/skills/jgengine-combat/SKILL.md +15 -1
- package/skills/jgengine-combat/api.md +39 -2
- package/skills/jgengine-combat/capabilities.md +17 -1
- package/skills/jgengine-combat/recipes/portable-damage-effects.md +197 -0
- package/skills/jgengine-combat/recipes/portable-runtime-state.md +154 -0
- package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +156 -0
- package/skills/jgengine-combat/recipes/visible-attacks.md +81 -0
- package/skills/jgengine-combat/reference.md +22 -3
- package/skills/jgengine-gameplay/SKILL.md +8 -0
- package/skills/jgengine-gameplay/api.md +35 -5
- package/skills/jgengine-gameplay/capabilities.md +18 -0
- package/skills/jgengine-gameplay/recipes/portable-xp-leveling.md +168 -0
- package/skills/jgengine-gameplay/reference-systems.md +9 -0
- package/skills/jgengine-world/api.md +52 -24
- package/skills/jgengine-world/capabilities.md +8 -4
- package/skills/jgengine-world/reference.md +1 -1
package/dist/world/streets.js
CHANGED
|
@@ -197,3 +197,221 @@ export function sidewalkPoint(road, side, fraction) {
|
|
|
197
197
|
const along = Math.max(0, Math.min(1, fraction)) * pathLength(path);
|
|
198
198
|
return pointAlong(path, along).point;
|
|
199
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* The two curb lines of a road — offset polylines running along the outer edge of the asphalt on
|
|
202
|
+
* each side. Mesh these with a thin darker ribbon to give the road a curb/edge strip instead of a
|
|
203
|
+
* hard asphalt-to-terrain seam. `inset` (default 0) nudges the line inward off the raw edge so the
|
|
204
|
+
* strip straddles the border.
|
|
205
|
+
*/
|
|
206
|
+
export function curbPaths(road, inset = 0) {
|
|
207
|
+
const edge = road.width / 2 - inset;
|
|
208
|
+
return [offsetPath(road.path, edge), offsetPath(road.path, -edge)];
|
|
209
|
+
}
|
|
210
|
+
const EPS = 1e-6;
|
|
211
|
+
/** Intersection point of segments a→b and c→d, or null when they don't cross (endpoints included). */
|
|
212
|
+
function segmentIntersection(a, b, c, d) {
|
|
213
|
+
const r0 = b[0] - a[0];
|
|
214
|
+
const r1 = b[1] - a[1];
|
|
215
|
+
const s0 = d[0] - c[0];
|
|
216
|
+
const s1 = d[1] - c[1];
|
|
217
|
+
const denom = r0 * s1 - r1 * s0;
|
|
218
|
+
if (Math.abs(denom) < EPS)
|
|
219
|
+
return null; // parallel or degenerate
|
|
220
|
+
const t = ((c[0] - a[0]) * s1 - (c[1] - a[1]) * s0) / denom;
|
|
221
|
+
const u = ((c[0] - a[0]) * r1 - (c[1] - a[1]) * r0) / denom;
|
|
222
|
+
if (t < -EPS || t > 1 + EPS || u < -EPS || u > 1 + EPS)
|
|
223
|
+
return null;
|
|
224
|
+
return [a[0] + r0 * t, a[1] + r1 * t];
|
|
225
|
+
}
|
|
226
|
+
function unit(dx, dz) {
|
|
227
|
+
const len = Math.hypot(dx, dz) || 1;
|
|
228
|
+
return [dx / len, dz / len];
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Add the arms a road contributes at junction point `p`: the road continues past `p` in each
|
|
232
|
+
* direction that still has centerline beyond it, so a mid-path crossing yields two opposing arms
|
|
233
|
+
* and a road that terminates at `p` (a T-junction) yields only the one inward arm.
|
|
234
|
+
*/
|
|
235
|
+
function armsAt(path, p, width, roadIndex) {
|
|
236
|
+
const arms = [];
|
|
237
|
+
// Nearest vertex "before" (toward path start) and "after" (toward path end) of p.
|
|
238
|
+
// Walk the path measuring where p falls; use the two adjacent tangents.
|
|
239
|
+
let hitSeg = -1;
|
|
240
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
241
|
+
const a = path[i];
|
|
242
|
+
const b = path[i + 1];
|
|
243
|
+
const abx = b[0] - a[0];
|
|
244
|
+
const abz = b[1] - a[1];
|
|
245
|
+
const lengthSq = abx * abx + abz * abz;
|
|
246
|
+
const t = lengthSq === 0 ? 0 : ((p[0] - a[0]) * abx + (p[1] - a[1]) * abz) / lengthSq;
|
|
247
|
+
const px = a[0] + abx * Math.max(0, Math.min(1, t));
|
|
248
|
+
const pz = a[1] + abz * Math.max(0, Math.min(1, t));
|
|
249
|
+
if (Math.hypot(p[0] - px, p[1] - pz) < 1e-3) {
|
|
250
|
+
hitSeg = i;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (hitSeg < 0)
|
|
255
|
+
return arms;
|
|
256
|
+
const a = path[hitSeg];
|
|
257
|
+
const b = path[hitSeg + 1];
|
|
258
|
+
const forward = unit(b[0] - a[0], b[1] - a[1]);
|
|
259
|
+
const atStart = Math.hypot(p[0] - path[0][0], p[1] - path[0][1]) < 1e-3;
|
|
260
|
+
const atEnd = Math.hypot(p[0] - path[path.length - 1][0], p[1] - path[path.length - 1][1]) < 1e-3;
|
|
261
|
+
// Arm toward path end exists unless p sits exactly at the end.
|
|
262
|
+
if (!atEnd)
|
|
263
|
+
arms.push({ roadIndex, direction: forward, width });
|
|
264
|
+
// Arm toward path start exists unless p sits exactly at the start.
|
|
265
|
+
if (!atStart)
|
|
266
|
+
arms.push({ roadIndex, direction: [-forward[0], -forward[1]], width });
|
|
267
|
+
return arms;
|
|
268
|
+
}
|
|
269
|
+
function mergeArm(arms, arm) {
|
|
270
|
+
for (const existing of arms) {
|
|
271
|
+
if (existing.roadIndex === arm.roadIndex) {
|
|
272
|
+
const dot = existing.direction[0] * arm.direction[0] + existing.direction[1] * arm.direction[1];
|
|
273
|
+
if (dot > 0.985)
|
|
274
|
+
return; // same road, near-identical direction — already have this arm
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
arms.push(arm);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Detect welded intersections across a set of roads: wherever two road centerlines cross (or a road
|
|
281
|
+
* terminates on another) within the same elevation band, emit a {@link RoadJunction} whose patch
|
|
282
|
+
* covers the crossing ribbons. Nearby crossings merge into one junction. Deterministic and bounded —
|
|
283
|
+
* O(roads² · segments²) with a `maxJunctions` cap — so a scene builds its intersections once.
|
|
284
|
+
*/
|
|
285
|
+
export function findRoadJunctions(roads, options = {}) {
|
|
286
|
+
const elevationBand = options.elevationBand ?? 0.12;
|
|
287
|
+
const mergeDistance = options.mergeDistance ?? 6;
|
|
288
|
+
const maxJunctions = options.maxJunctions ?? 256;
|
|
289
|
+
const radiusScale = options.radiusScale ?? 1.15;
|
|
290
|
+
const mergeSq = mergeDistance * mergeDistance;
|
|
291
|
+
const candidates = [];
|
|
292
|
+
const emit = (p, ri, ci) => {
|
|
293
|
+
if (candidates.length >= maxJunctions && findNear(candidates, p, mergeSq) === null)
|
|
294
|
+
return;
|
|
295
|
+
const roadA = roads[ri];
|
|
296
|
+
const roadB = roads[ci];
|
|
297
|
+
const half = Math.max(roadA.width, roadB.width) / 2;
|
|
298
|
+
const radius = half * radiusScale;
|
|
299
|
+
const elevation = Math.max(roadA.elevation, roadB.elevation);
|
|
300
|
+
const wider = roadA.width >= roadB.width ? roadA : roadB;
|
|
301
|
+
const near = findNear(candidates, p, mergeSq);
|
|
302
|
+
const arms = [
|
|
303
|
+
...armsAt(roadA.path, p, roadA.width, ri),
|
|
304
|
+
...armsAt(roadB.path, p, roadB.width, ci),
|
|
305
|
+
];
|
|
306
|
+
if (near === null) {
|
|
307
|
+
const merged = [];
|
|
308
|
+
for (const arm of arms)
|
|
309
|
+
mergeArm(merged, arm);
|
|
310
|
+
candidates.push({
|
|
311
|
+
x: p[0],
|
|
312
|
+
z: p[1],
|
|
313
|
+
radius,
|
|
314
|
+
elevation,
|
|
315
|
+
color: wider.color,
|
|
316
|
+
colorWidth: wider.width,
|
|
317
|
+
arms: merged,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
near.radius = Math.max(near.radius, radius);
|
|
322
|
+
near.elevation = Math.max(near.elevation, elevation);
|
|
323
|
+
if (wider.width > near.colorWidth) {
|
|
324
|
+
near.color = wider.color;
|
|
325
|
+
near.colorWidth = wider.width;
|
|
326
|
+
}
|
|
327
|
+
for (const arm of arms)
|
|
328
|
+
mergeArm(near.arms, arm);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
for (let i = 0; i < roads.length; i += 1) {
|
|
332
|
+
const roadA = roads[i];
|
|
333
|
+
if (roadA.path.length < 2)
|
|
334
|
+
continue;
|
|
335
|
+
for (let j = i + 1; j < roads.length; j += 1) {
|
|
336
|
+
const roadB = roads[j];
|
|
337
|
+
if (roadB.path.length < 2)
|
|
338
|
+
continue;
|
|
339
|
+
if (Math.abs(roadA.elevation - roadB.elevation) > elevationBand)
|
|
340
|
+
continue;
|
|
341
|
+
for (let si = 0; si < roadA.path.length - 1; si += 1) {
|
|
342
|
+
for (let sj = 0; sj < roadB.path.length - 1; sj += 1) {
|
|
343
|
+
const hit = segmentIntersection(roadA.path[si], roadA.path[si + 1], roadB.path[sj], roadB.path[sj + 1]);
|
|
344
|
+
if (hit !== null)
|
|
345
|
+
emit(hit, i, j);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return candidates.map((c) => ({
|
|
351
|
+
center: [c.x, c.z],
|
|
352
|
+
radius: c.radius,
|
|
353
|
+
elevation: c.elevation,
|
|
354
|
+
color: c.color,
|
|
355
|
+
approaches: c.arms.map((arm) => ({ direction: arm.direction, width: arm.width })),
|
|
356
|
+
}));
|
|
357
|
+
}
|
|
358
|
+
function findNear(candidates, p, mergeSq) {
|
|
359
|
+
for (const c of candidates) {
|
|
360
|
+
const dx = c.x - p[0];
|
|
361
|
+
const dz = c.z - p[1];
|
|
362
|
+
if (dx * dx + dz * dz <= mergeSq)
|
|
363
|
+
return c;
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
/** Circular marking-exclusion zones for the junctions, sized to hide the center line under each patch. */
|
|
368
|
+
export function junctionExclusions(junctions, pad = 0.5) {
|
|
369
|
+
return junctions.map((j) => ({ center: j.center, radius: j.radius + pad }));
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Data-driven junction entry markings: a stop-line bar across each approach and a band of crosswalk
|
|
373
|
+
* bars just outside the welded patch. Each returned polyline is a thin centerline meant to be meshed
|
|
374
|
+
* with {@link buildRoadRibbon} at a small width. Fully skippable per-feature via the options.
|
|
375
|
+
*/
|
|
376
|
+
export function junctionMarkings(junction, options = {}) {
|
|
377
|
+
const wantStop = options.stopLines ?? true;
|
|
378
|
+
const wantCross = options.crosswalks ?? true;
|
|
379
|
+
const barCount = Math.max(1, Math.floor(options.crosswalkBars ?? 5));
|
|
380
|
+
const gap = options.gap ?? 0.6;
|
|
381
|
+
const depth = options.crosswalkDepth ?? 2.4;
|
|
382
|
+
const [cx, cz] = junction.center;
|
|
383
|
+
const stopLines = [];
|
|
384
|
+
const crosswalkBars = [];
|
|
385
|
+
for (const approach of junction.approaches) {
|
|
386
|
+
const [dx, dz] = approach.direction;
|
|
387
|
+
// Across-the-road axis, perpendicular to travel.
|
|
388
|
+
const ax = -dz;
|
|
389
|
+
const az = dx;
|
|
390
|
+
const half = approach.width / 2;
|
|
391
|
+
const stopDist = junction.radius + gap;
|
|
392
|
+
if (wantStop) {
|
|
393
|
+
const px = cx + dx * stopDist;
|
|
394
|
+
const pz = cz + dz * stopDist;
|
|
395
|
+
stopLines.push([
|
|
396
|
+
[px + ax * half, pz + az * half],
|
|
397
|
+
[px - ax * half, pz - az * half],
|
|
398
|
+
]);
|
|
399
|
+
}
|
|
400
|
+
if (wantCross) {
|
|
401
|
+
const near = stopDist + 0.4;
|
|
402
|
+
const barWidth = half * 2 * 0.16;
|
|
403
|
+
const usable = half * 2 - barWidth;
|
|
404
|
+
for (let b = 0; b < barCount; b += 1) {
|
|
405
|
+
const frac = barCount === 1 ? 0 : b / (barCount - 1) - 0.5;
|
|
406
|
+
const offset = frac * usable;
|
|
407
|
+
const bx = cx + ax * offset;
|
|
408
|
+
const bz = cz + az * offset;
|
|
409
|
+
crosswalkBars.push([
|
|
410
|
+
[bx + dx * near, bz + dz * near],
|
|
411
|
+
[bx + dx * (near + depth), bz + dz * (near + depth)],
|
|
412
|
+
]);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return { stopLines, crosswalkBars };
|
|
417
|
+
}
|
package/dist/world/terrain.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BiomeBand, EnvironmentWorldFeature, TerrainDetailConfig, TerrainEnvironmentConfig, TerrainEnvironmentDescriptor, TerrainIslandDescriptor, TerrainMaterial, TerrainMaterialMaps, WorldBounds, WorldFeature } from "./features.js";
|
|
1
|
+
import type { BiomeBand, EnvironmentWorldFeature, RoadEnvironmentDescriptor, TerrainDetailConfig, TerrainEnvironmentConfig, TerrainEnvironmentDescriptor, TerrainIslandDescriptor, TerrainMaterial, TerrainMaterialMaps, WorldBounds, WorldFeature } from "./features.js";
|
|
2
2
|
import type { AvoidZone } from "./geometry.js";
|
|
3
3
|
import { type TerrainPathProfile } from "./pathTerrain.js";
|
|
4
4
|
import type { TerraformSnapshot } from "./terraform.js";
|
|
@@ -107,6 +107,25 @@ export declare function flattenFieldAround(base: TerrainField, zones: readonly A
|
|
|
107
107
|
* through the one seam every consumer already reads.
|
|
108
108
|
* @internal
|
|
109
109
|
*/
|
|
110
|
+
/**
|
|
111
|
+
* Drapes each `road()` ribbon's `elevation` onto the ground so a body standing on a road snaps to the
|
|
112
|
+
* road *surface* the renderer draws (`sampleHeight + elevation`), not the bare terrain beneath it. Without
|
|
113
|
+
* this the ground field and {@link RoadRibbons}/{@link CityRenderer} disagree by `elevation` per road, and
|
|
114
|
+
* anything grounded on a road — the player, NPCs, spawns via `groundHeightAt` — sinks by that offset into
|
|
115
|
+
* the asphalt. Overlapping ribbons (intersections) take the highest lift. Only `sampleHeight` is draped:
|
|
116
|
+
* the road ribbons are flat, so `sampleNormal` stays the smooth terrain normal — draping it too would spoof
|
|
117
|
+
* a near-vertical slope at every curb edge and trip slope-slide.
|
|
118
|
+
* @internal — reached through `environment({ roads })`; not called directly by games.
|
|
119
|
+
*/
|
|
120
|
+
export declare function roadDrapedField(base: TerrainField, roads: readonly RoadEnvironmentDescriptor[]): TerrainField;
|
|
121
|
+
/**
|
|
122
|
+
* The full ground field for an environment world: base `terrain` composed with any `islands`, then
|
|
123
|
+
* any authored `sculpt` snapshot, then any `clearings`, then any `roads` draped on top — so an
|
|
124
|
+
* editor-sculpted heightfield drives both the rendered mesh and player collision, gameplay spots
|
|
125
|
+
* stay level, and anything grounded on a road stands on the drawn asphalt rather than the bare
|
|
126
|
+
* terrain beneath it, through the one seam every consumer already reads.
|
|
127
|
+
* @internal
|
|
128
|
+
*/
|
|
110
129
|
export declare function resolveEnvironmentField(feature: EnvironmentWorldFeature): TerrainField;
|
|
111
130
|
export interface TerrainSlopeSample {
|
|
112
131
|
/** Unit XZ direction pointing downhill; `[0, 0]` on level ground. */
|
package/dist/world/terrain.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { withPathProfiles } from "./pathTerrain.js";
|
|
2
|
-
import { nearestOnPath } from "./roads.js";
|
|
2
|
+
import { isOnRoad, nearestOnPath } from "./roads.js";
|
|
3
3
|
function clamp01(value) {
|
|
4
4
|
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
5
5
|
}
|
|
@@ -308,15 +308,54 @@ export function flattenFieldAround(base, zones) {
|
|
|
308
308
|
* through the one seam every consumer already reads.
|
|
309
309
|
* @internal
|
|
310
310
|
*/
|
|
311
|
+
/**
|
|
312
|
+
* Drapes each `road()` ribbon's `elevation` onto the ground so a body standing on a road snaps to the
|
|
313
|
+
* road *surface* the renderer draws (`sampleHeight + elevation`), not the bare terrain beneath it. Without
|
|
314
|
+
* this the ground field and {@link RoadRibbons}/{@link CityRenderer} disagree by `elevation` per road, and
|
|
315
|
+
* anything grounded on a road — the player, NPCs, spawns via `groundHeightAt` — sinks by that offset into
|
|
316
|
+
* the asphalt. Overlapping ribbons (intersections) take the highest lift. Only `sampleHeight` is draped:
|
|
317
|
+
* the road ribbons are flat, so `sampleNormal` stays the smooth terrain normal — draping it too would spoof
|
|
318
|
+
* a near-vertical slope at every curb edge and trip slope-slide.
|
|
319
|
+
* @internal — reached through `environment({ roads })`; not called directly by games.
|
|
320
|
+
*/
|
|
321
|
+
export function roadDrapedField(base, roads) {
|
|
322
|
+
if (roads.length === 0)
|
|
323
|
+
return base;
|
|
324
|
+
const sampleHeight = (x, z) => {
|
|
325
|
+
let lift = 0;
|
|
326
|
+
for (const road of roads) {
|
|
327
|
+
if (road.elevation > lift && isOnRoad(road.path, road.width, x, z))
|
|
328
|
+
lift = road.elevation;
|
|
329
|
+
}
|
|
330
|
+
return base.sampleHeight(x, z) + lift;
|
|
331
|
+
};
|
|
332
|
+
return {
|
|
333
|
+
sampleHeight,
|
|
334
|
+
sampleNormal: base.sampleNormal,
|
|
335
|
+
...(base.bounds === undefined ? {} : { bounds: base.bounds }),
|
|
336
|
+
...(base.waterLevel === undefined ? {} : { waterLevel: base.waterLevel }),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* The full ground field for an environment world: base `terrain` composed with any `islands`, then
|
|
341
|
+
* any authored `sculpt` snapshot, then any `clearings`, then any `roads` draped on top — so an
|
|
342
|
+
* editor-sculpted heightfield drives both the rendered mesh and player collision, gameplay spots
|
|
343
|
+
* stay level, and anything grounded on a road stands on the drawn asphalt rather than the bare
|
|
344
|
+
* terrain beneath it, through the one seam every consumer already reads.
|
|
345
|
+
* @internal
|
|
346
|
+
*/
|
|
311
347
|
export function resolveEnvironmentField(feature) {
|
|
312
348
|
const base = feature.terrain === undefined ? null : resolveTerrainField(feature.terrain);
|
|
313
349
|
const composed = feature.islands === undefined || feature.islands.length === 0
|
|
314
350
|
? base ?? flatField()
|
|
315
351
|
: composeIslandFields(base, feature.islands);
|
|
316
352
|
const sculpted = feature.sculpt === undefined ? composed : sculptedField(composed, feature.sculpt);
|
|
317
|
-
|
|
353
|
+
const cleared = feature.clearings === undefined || feature.clearings.length === 0
|
|
318
354
|
? sculpted
|
|
319
355
|
: flattenFieldAround(sculpted, feature.clearings);
|
|
356
|
+
return feature.roads === undefined || feature.roads.length === 0
|
|
357
|
+
? cleared
|
|
358
|
+
: roadDrapedField(cleared, feature.roads);
|
|
320
359
|
}
|
|
321
360
|
/** Local slope from the field's normal — the input for gravity-roll, ski acceleration, and slide checks (#284.6).
|
|
322
361
|
* @internal
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "jgengine core — agent-first game framework SDK (npm @jgengine/core). Entity stores, commands, combat, inventory, multiplayer seams. Zero dependencies. Not an ECS. https://jgengine.com",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jgengine",
|
package/skills/jgengine/SKILL.md
CHANGED
|
@@ -42,6 +42,7 @@ World content routes editor-first: any request that adds, moves, restyles, or re
|
|
|
42
42
|
|
|
43
43
|
Within each selected domain:
|
|
44
44
|
|
|
45
|
+
0. Fastest start for a common composition: `npx jgengine recipe <name>` prints a vetted, SDK-typechecked snippet (imports + wired code) for `combat-loop`, `boss-telegraph`, `loot`, `quest`, `coop-presence`, `third-person-camera`; `npx jgengine recipe` lists them. Copy that instead of reconning per-file.
|
|
45
46
|
1. Search `capabilities.md` by intent; it maps needs to imports.
|
|
46
47
|
2. Open `api.md` only when exact signatures or export inventory are needed.
|
|
47
48
|
3. Open the linked reference only for a deeper recipe or trap.
|
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.13.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
10
10
|
|
|
11
11
|
## @jgengine/core/authoring
|
|
12
12
|
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
- `WorldFeature` (type): type WorldFeature = | PlaceWorldFeature | ({ kind: "biomes" } & BiomesWorldConfig) | ({ kind: "voxel" } & VoxelWorldConfig) | ({ kind: "plots" } & PlotsWorldConfig) | ({ kind: "tilemap" } & TilemapWorldConfig) | EnvironmentWorldFeature | { kind: "flat" } — A declared world shape passed to `defineGame`. The preferred model is the place feature from `world()` (`@jgengine/core/world/place`): substrate + laws, with all dressing authored in the editor. The remaining members — biomes, voxel grid, plots, tilemap, environment, flat — are the legacy code-declared shapes kept for existing games.
|
|
39
39
|
- `auditManifest` (function): function auditManifest(manifest: SceneOwnershipManifest): OwnershipDiagnostic[] — Audit every declaration in a manifest, keying diagnostics by each object's provenance id. An empty result means the manifest declares all of its content cleanly — every object is authored, generated, bakeable, or a reasoned runtime/transient object.
|
|
40
40
|
- `biomes` (function): function biomes(config: BiomesWorldConfig): WorldFeature — Declares a biome-painted world — the whole-world alternative to a single `environment()` terrain.
|
|
41
|
-
- `building` (function): function building(config: BuildingEnvironmentConfig = {}): BuildingEnvironmentDescriptor — Declares a cluster of procedurally-massed buildings for `environment()` — count, footprint, stories, style.
|
|
41
|
+
- `building` (function): function building(config: BuildingEnvironmentConfig = {}): BuildingEnvironmentDescriptor — Declares a cluster of procedurally-massed buildings for `environment()` — count, footprint, stories, style. Pass `along` to line road frontage instead of gridding around `position`.
|
|
42
42
|
- `classifyOwnership` (function): function classifyOwnership(declaration: SceneOwnershipDeclaration): OwnershipVerdict — Resolve one declaration into a single boundary verdict.
|
|
43
43
|
- `collectOwnershipDiagnostics` (function): function collectOwnershipDiagnostics(entries: Iterable<readonly [string, SceneOwnershipDeclaration]>): OwnershipDiagnostic[] — Audit a keyed set of declarations and return one diagnostic per object that breaks the boundary contract (a `reject` verdict carrying a `violation`). Deterministic: diagnostics come back in the iteration order of `entries`.
|
|
44
44
|
- `contextVerb` (function): function contextVerb(label: string, command: string, args?: Record<string, unknown>): ContextVerb — Builds a {@link ContextVerb} for a right-click menu entry.
|
|
@@ -209,11 +209,18 @@
|
|
|
209
209
|
- `Tunable` (interface): interface Tunable<T> — Typed handle to a registered control, exposing its live value and subscribe/set/reset operations.
|
|
210
210
|
- `TunableAccessor` (interface): interface TunableAccessor — Get/set accessor pair plus initial value used to bind a discovered field to a control.
|
|
211
211
|
|
|
212
|
+
## @jgengine/core/devtools/urlFlags
|
|
213
|
+
|
|
214
|
+
- `readUrlFlag` (function): function readUrlFlag(param: string): boolean — True when `param` is present and not an explicit off value (`0`/`false`/`off`/`no`).
|
|
215
|
+
- `readUrlParam` (function): function readUrlParam(param: string): string | null — The current query value of `param`, or null when absent (or no DOM).
|
|
216
|
+
- `subscribeUrlChange` (function): function subscribeUrlChange(listener: () => void): () => void — Fires `listener` whenever the query string may have changed out from under a flag — browser back/forward (`popstate`). Manual address-bar edits reload the page, so initial reads cover those; this keeps in-app state honest across history navigation. Returns an unsubscribe.
|
|
217
|
+
- `writeUrlParam` (function): function writeUrlParam(param: string, value: string | null): void — Sets `param` to `value`, or removes it when `value` is null, rewriting the URL in place with `history.replaceState` so the rest of the query and the hash survive and no history entry is pushed. No-ops without a DOM, and skips the write when the URL already matches.
|
|
218
|
+
|
|
212
219
|
## @jgengine/core/meta/changelog
|
|
213
220
|
|
|
214
221
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
215
222
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
216
|
-
- `VERSION` (const): const VERSION: "0.
|
|
223
|
+
- `VERSION` (const): const VERSION: "0.13.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
217
224
|
|
|
218
225
|
## @jgengine/core/runtime/adapter
|
|
219
226
|
|
|
@@ -276,7 +283,7 @@
|
|
|
276
283
|
- `SceneObjectContext` (interface): interface SceneObjectContext extends ObjectStore — ⚠ undocumented
|
|
277
284
|
- `SceneWorldItemContext` (interface): interface SceneWorldItemContext — ⚠ undocumented
|
|
278
285
|
- `TelegraphInput` (interface): interface TelegraphInput — ⚠ undocumented
|
|
279
|
-
- `VfxInput` (interface): interface VfxInput — Request a transient spell/ability VFX burst; `from`/`to` accept an instance id or a world point
|
|
286
|
+
- `VfxInput` (interface): interface VfxInput — Request a transient spell/ability VFX burst. The easy path is a named `preset` — `vfx({ preset: "arrow", from: caster, to: enemy })` renders a visible bolt with no color or archetype tuning; `"lightning"`, `"web"`, `"slash"`, `"shield"`, `"heal"`, `"explosion"` and the rest of {@link vfxPresets} likewise just work. `from`/`to` accept an instance id (the shell follows its live pose) or a fixed world point. Anything you also pass — `kind`, `color` (`0xRRGGBB`), `radius`, `durationMs` — overrides the preset; supply `kind` + `color` yourself for a fully custom burst with no preset. `durationMs` defaults per `kind`.
|
|
280
287
|
- `WorldItemPickupResult` (type): type WorldItemPickupResult = | { status: "ok"; record: WorldItemRecord } | { status: "rejected"; reason: string } — ⚠ undocumented
|
|
281
288
|
- `createGameContext` (function): function createGameContext<TAssetRef extends ModelAssetRef, TMultiplayer>(options: GameContextOptions<TAssetRef, TMultiplayer>): GameContext — ⚠ undocumented
|
|
282
289
|
|
|
@@ -307,7 +314,7 @@
|
|
|
307
314
|
- `SceneObjectContext` (interface): interface SceneObjectContext extends ObjectStore — ⚠ undocumented
|
|
308
315
|
- `SceneWorldItemContext` (interface): interface SceneWorldItemContext — ⚠ undocumented
|
|
309
316
|
- `TelegraphInput` (interface): interface TelegraphInput — ⚠ undocumented
|
|
310
|
-
- `VfxInput` (interface): interface VfxInput — Request a transient spell/ability VFX burst; `from`/`to` accept an instance id or a world point
|
|
317
|
+
- `VfxInput` (interface): interface VfxInput — Request a transient spell/ability VFX burst. The easy path is a named `preset` — `vfx({ preset: "arrow", from: caster, to: enemy })` renders a visible bolt with no color or archetype tuning; `"lightning"`, `"web"`, `"slash"`, `"shield"`, `"heal"`, `"explosion"` and the rest of {@link vfxPresets} likewise just work. `from`/`to` accept an instance id (the shell follows its live pose) or a fixed world point. Anything you also pass — `kind`, `color` (`0xRRGGBB`), `radius`, `durationMs` — overrides the preset; supply `kind` + `color` yourself for a fully custom burst with no preset. `durationMs` defaults per `kind`.
|
|
311
318
|
- `WorldItemPickupResult` (type): type WorldItemPickupResult = | { status: "ok"; record: WorldItemRecord } | { status: "rejected"; reason: string } — ⚠ undocumented
|
|
312
319
|
|
|
313
320
|
## @jgengine/core/runtime/gameRuntime
|
|
@@ -322,6 +329,7 @@
|
|
|
322
329
|
|
|
323
330
|
## @jgengine/core/runtime/headlessRunner
|
|
324
331
|
|
|
332
|
+
- `HeadlessCommandInterface` (interface): interface HeadlessCommandInterface — Canvas-free command/intent surface for {@link HeadlessRunner} — invoke a game's registered UI commands and read the resulting reactive state off `ctx`.
|
|
325
333
|
- `HeadlessInput` (interface): interface HeadlessInput — One step's worth of player intent handed to {@link HeadlessRunner.step} — the held-action set and pointer state the shell would otherwise publish from the browser each frame.
|
|
326
334
|
- `HeadlessRunner` (interface): interface HeadlessRunner — A renderer-free driver for a game loop: builds a {@link GameContext} from a {@link GameDefinition}, runs the init hooks, then advances the simulation one step at a time from injected input. No React, R3F, or three.js — the whole play path (time, input, `onTick`, behaviour nav, optional player movement) runs from `core` primitives alone, so a non-React host (a server tick, a test, a CLI replay) can play a real game and read its world snapshot. The shell's FrameDriver is one such driver bolted to `useFrame`; this is the same step distilled out of the render tree.
|
|
327
335
|
- `HeadlessRunnerOptions` (interface): interface HeadlessRunnerOptions<TAssetRef extends ModelAssetRef, TMultiplayer> — ⚠ undocumented
|
|
@@ -14,7 +14,21 @@ Search [capabilities.md](capabilities.md), then use [api.md](api.md) for signatu
|
|
|
14
14
|
Existing projects keep their resource state and follow the
|
|
15
15
|
[portable stat-pool recipe](recipes/portable-stat-pools.md). Adapt the caller's
|
|
16
16
|
store through `StatPoolAccess`; do not require `StatValueMap`, `GameContext`, or
|
|
17
|
-
a parallel entity store.
|
|
17
|
+
a parallel entity store. To bolt firing onto an existing game, compose
|
|
18
|
+
`createWeaponRuntime` (cadence + magazine + the caller's own raycast + portable
|
|
19
|
+
`resolveDamageHit`) per the
|
|
20
|
+
[portable weapon-plumbing recipe](recipes/portable-weapon-plumbing.md); never add
|
|
21
|
+
a default gun or FPS kit.
|
|
22
|
+
|
|
23
|
+
Damage commitment accepts the same structural pool adapter while the matchup,
|
|
24
|
+
receiver, interceptor, and status stages remain pure. Follow the
|
|
25
|
+
[portable damage/effects recipe](recipes/portable-damage-effects.md) to compose
|
|
26
|
+
them over an existing store, clock, RNG, spatial index, and death flow.
|
|
27
|
+
|
|
28
|
+
Closure-backed magazines and stat modifiers expose plain snapshot/restore
|
|
29
|
+
state for caller-owned saves, rollback, replay, and workers. Follow the
|
|
30
|
+
[portable runtime-state recipe](recipes/portable-runtime-state.md); the caller
|
|
31
|
+
keeps its clock, reserve state, schema, and authority.
|
|
18
32
|
|
|
19
33
|
## Canonical pipeline
|
|
20
34
|
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
- `DEFAULT_FIRE_PULSE_SECONDS` (const): const DEFAULT_FIRE_PULSE_SECONDS: 0.12 — Default `RenderCueTuning.firePulseSeconds`.
|
|
29
29
|
- `DEFAULT_HIT_PULSE_SECONDS` (const): const DEFAULT_HIT_PULSE_SECONDS: 0.2 — Default `RenderCueTuning.hitPulseSeconds`.
|
|
30
30
|
- `DEFAULT_RENDER_CUES` (const): const DEFAULT_RENDER_CUES: Readonly<EntityRenderCues> — Neutral starting cue set: idle, unarmed, undamaged.
|
|
31
|
+
- `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.
|
|
31
32
|
- `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.
|
|
32
33
|
- `DamageClampConfig` (interface): interface DamageClampConfig — Caller data for a flat per-hit damage cap.
|
|
33
34
|
- `DamageContext` (interface): interface DamageContext — The facts a received-damage modifier predicates over: the incoming `channel`, who dealt it, the statuses the target currently carries, and any caller tags. All caller-owned strings; the engine never assigns meaning to a channel, source, status, or tag.
|
|
@@ -50,6 +51,8 @@
|
|
|
50
51
|
- `EntityRenderCues` (interface): interface EntityRenderCues — Per-entity render cues: the motion/animation signal a custom `renderEntity` or first-person viewmodel component needs to drive gait, muzzle flash, reload poses, and hit reactions — without diffing the parent group's position itself or reading a game-side module map for attack timing.
|
|
51
52
|
- `EventMeter` (interface): interface EventMeter — ⚠ undocumented
|
|
52
53
|
- `EventMeterFeedResult` (interface): interface EventMeterFeedResult — ⚠ undocumented
|
|
54
|
+
- `FireCadence` (interface): interface FireCadence — A minimal minimum-interval rate gate for a repeated action — weapon fire, ability spam, any "no faster than N per second" rule. Deterministic and serializable: its whole state is the elapsed time since the last action, exposed via {@link FireCadence.elapsedMs}.
|
|
55
|
+
- `FireCadenceConfig` (interface): interface FireCadenceConfig — Tuning for {@link createFireCadence}: the minimum interval between actions and whether it starts ready.
|
|
53
56
|
- `FrameRange` (interface): interface FrameRange — ⚠ undocumented
|
|
54
57
|
- `HitReactionConfig` (interface): interface HitReactionConfig — ⚠ undocumented
|
|
55
58
|
- `ImmunityWindow` (interface): interface ImmunityWindow — A per-target timed immunity policy: an interceptor plus grant/query/serialize controls, toggled by transitions.
|
|
@@ -58,7 +61,9 @@
|
|
|
58
61
|
- `InterceptRecord` (interface): interface InterceptRecord — One row of provenance: which interceptor acted, what it decided, and the amount before/after it acted.
|
|
59
62
|
- `MAX_INTERCEPT_STEPS` (const): const MAX_INTERCEPT_STEPS: 256 — Upper bound on interceptor evaluations for a single resolution; guards against runaway `split`/`redirect` loops.
|
|
60
63
|
- `Magazine` (interface): interface Magazine — A per-weapon magazine: discrete loaded rounds, a timed reload that refills from a reserve pool, and the reserve-pool interaction itself — the primitive that replaces hand-rolling mag size, reload delay, and reserve bookkeeping per game (#536.2).
|
|
64
|
+
- `MagazineConfig` (interface): interface MagazineConfig — Tuning for `createMagazine`: mag capacity, reload delay, and where reserve ammo is drawn from.
|
|
61
65
|
- `MagazineReserve` (interface): interface MagazineReserve — Draws ammo for a `Magazine`'s reload from wherever the reserve pool actually lives.
|
|
66
|
+
- `MagazineSnapshot` (interface): interface MagazineSnapshot — Complete serializable state for a {@link Magazine}; `reserve: null` means infinite reserve.
|
|
62
67
|
- `MatchupOutputs` (interface): interface MatchupOutputs — The independent, typed outputs a channel-vs-trait matchup can scale. Every field is an optional multiplier that defaults to `1` (identity), so a matchup entry names only the axes it bends. `impact` scales the direct hit, `applyChance` the odds a status lands, `magnitude` and `duration` the applied status itself, and `extra` carries any further caller-defined scalar (knockback, stagger, heat) the engine forwards without interpreting.
|
|
63
68
|
- `MeterAddResult` (interface): interface MeterAddResult — ⚠ undocumented
|
|
64
69
|
- `ObjectRaycastHit` (interface): interface ObjectRaycastHit — ⚠ undocumented
|
|
@@ -95,6 +100,7 @@
|
|
|
95
100
|
- `StatPoolInput` (interface): interface StatPoolInput — Input used to create a normalized pool. `current` defaults to `max`; `min` defaults to zero.
|
|
96
101
|
- `StatPoolPatch` (interface): interface StatPoolPatch — Partial update applied without mutating the previous pool.
|
|
97
102
|
- `Stats` (interface): interface Stats<TStat extends string> — ⚠ undocumented
|
|
103
|
+
- `StatsSnapshot` (interface): interface StatsSnapshot<TStat extends string> — Complete plain-data state for a {@link Stats} runtime.
|
|
98
104
|
- `StatusApplicationOutcome` (interface): interface StatusApplicationOutcome — The result of one status application: the outcome, the resulting instance (if any), and the roll.
|
|
99
105
|
- `StatusApplicationSpec` (interface): interface StatusApplicationSpec — A serializable, effect-agnostic description of a status a hit tries to apply: an opaque `status` id the game routes to its own effect/DoT pipeline, a base landing `chance`, and the duration/ticks/magnitude/attribution/stacking data the resolver needs. The engine never interprets `status` — it is a reference, not a built-in condition.
|
|
100
106
|
- `StatusImmunity` (interface): interface StatusImmunity — Statuses a target cannot receive at all — the immunity half of application policy.
|
|
@@ -108,6 +114,12 @@
|
|
|
108
114
|
- `ThresholdMark` (interface): interface ThresholdMark — Generic numeric threshold-crossing tracker (#931).
|
|
109
115
|
- `ThresholdTracker` (interface): interface ThresholdTracker — A stateful crossing tracker over one numeric source.
|
|
110
116
|
- `ThresholdTrackerSnapshot` (interface): interface ThresholdTrackerSnapshot — Serializable tracker state for save/load.
|
|
117
|
+
- `VfxPreset` (interface): interface VfxPreset — A named visual flavor for a one-shot `combat.vfx` burst: the archetype (`kind`), a `0xRRGGBB` tint, and optional default `durationMs`/`radius`. Purely presentation — a preset never touches damage, range, or any authoritative value; it just answers "what does this attack look like" so a game does not hand-roll colors and archetypes.
|
|
118
|
+
- `VfxPresetName` (type): type VfxPresetName = keyof typeof vfxPresets — A named key into {@link vfxPresets} (`"arrow" | "fireball" | "lightning" | "web" | "slash" | "shield" | "heal" | ...`).
|
|
119
|
+
- `WeaponFireResult` (interface): interface WeaponFireResult<THit> — The result of a {@link WeaponRuntime.fire} attempt. On `"fired"`, `hits` is whatever the caller's raycast returned and `resolutions` holds the portable damage provenance for each hit that produced a {@link DamageHitInput}; on any blocked status both are empty and no round was spent.
|
|
120
|
+
- `WeaponFireStatus` (type): type WeaponFireStatus = "fired" | "cooling" | "empty" | "reloading" — Why a {@link WeaponRuntime.fire} attempt resolved the way it did.
|
|
121
|
+
- `WeaponRuntime` (interface): interface WeaponRuntime<TAim, THit> — A composed weapon fire controller: a {@link FireCadence} gate, an optional {@link Magazine}, the caller's raycast, and portable {@link resolveDamageHit} damage resolution wired together. The caller retains ownership of models, animations, muzzle flash, audio, input, camera, aiming, and the raycaster itself.
|
|
122
|
+
- `WeaponRuntimeConfig` (interface): interface WeaponRuntimeConfig<TAim, THit> — Configuration for {@link createWeaponRuntime}. Everything time/ammo-related is plain data; the two callbacks are where the caller keeps ownership of aiming, its own raycaster/projectile query, and how a hit maps to damage. There is deliberately no default weapon, projectile, or damage table here — those are game content.
|
|
111
123
|
- `activePhaseId` (function): function activePhaseId<TData>(state: EncounterState<TData>): string | null — The deepest active phase id — the completion cursor a HUD reads to label the current wave/phase — or null before start and once done.
|
|
112
124
|
- `advanceCombo` (function): function advanceCombo(input: AdvanceComboInput): AdvanceComboResult — ⚠ undocumented
|
|
113
125
|
- `advanceMotionCues` (function): function advanceMotionCues(cues: EntityRenderCues, speed: number, dt: number, tuning?: RenderCueTuning): EntityRenderCues — Advances `bobPhase` and decays `recoil` from a live `speed` sample (e.g. `groundSpeed(entity)`); leaves event-driven fields untouched.
|
|
@@ -133,9 +145,10 @@
|
|
|
133
145
|
- `createDefensiveWindow` (function): function createDefensiveWindow(config: DefensiveWindowConfig): DefensiveWindow — Open a timed defensive window — block, parry, or i-frames — and test incoming hits against it.
|
|
134
146
|
- `createDotField` (function): function createDotField(): DotField — Builds an empty {@link DotField}; `apply` DoTs onto it and drain damage each frame with `tick`.
|
|
135
147
|
- `createDownedState` (function): function createDownedState(config: DownedConfig): DownedState — A downed/bleed-out state that ticks toward death and that teammates can revive before the timer runs out.
|
|
136
|
-
- `createEffectSystem` (function): function createEffectSystem(deps: EffectSystemDeps): EffectSystem —
|
|
148
|
+
- `createEffectSystem` (function): function createEffectSystem(deps: EffectSystemDeps): EffectSystem — Resolve direct or area resource effects through caller-owned stat pools, including ordered absorption, restoration, modifiers, and lethal context.
|
|
137
149
|
- `createEncounterState` (function): function createEncounterState<TData>(config: EncounterConfig<TData>): EncounterState<TData> — Build the initial, serializable state for an encounter from its config. Deep-copies the authored phase tree (so later injection never mutates the config), verifies phase ids are unique, and leaves every node pending. Does not enter the first phase — call {@link startEncounter} for that.
|
|
138
150
|
- `createEventMeter` (function): function createEventMeter(config: EventMeterConfig): EventMeter — A heat/hype gauge that rises as tagged events land and cools between them, firing when it fills or breaks — the streak/overdrive meter shooters and fighters hand-roll.
|
|
151
|
+
- `createFireCadence` (function): function createFireCadence(config: FireCadenceConfig): FireCadence — Build a {@link FireCadence} rate gate. Reach for this instead of hand-tracking a `lastFiredAt` timestamp and comparing against a fire interval per game.
|
|
139
152
|
- `createImmunityWindow` (function): function createImmunityWindow(id = "immunity"): ImmunityWindow — A per-target immunity/invulnerability window as an ordinary damage policy — granted and cleared by state transitions (i-frames, phase invuln), rejecting damage while active. Deterministic (time is passed in) and serializable.
|
|
140
153
|
- `createMagazine` (function): function createMagazine(config: MagazineConfig): Magazine — Builds a {@link Magazine}: discrete loaded ammo, a timed reload, and reserve-pool interaction.
|
|
141
154
|
- `createProjectileSystem` (function): function createProjectileSystem(deps: ProjectileSystemDeps): ProjectileSystem — Spawn and advance projectiles each frame, resolving travel, lifetime, and hits.
|
|
@@ -145,6 +158,7 @@
|
|
|
145
158
|
- `createStatPool` (function): function createStatPool(input: StatPoolInput): StatPool — Normalize caller data into a complete plain-data pool.
|
|
146
159
|
- `createStats` (function): function createStats<TStat extends string>(base: Record<TStat, number>, options?: CreateStatsOptions): Stats<TStat> — A stat block whose base values take stacking, timed buffs and debuffs, resolving the modified value on read.
|
|
147
160
|
- `createThresholdTracker` (function): function createThresholdTracker(config: ThresholdCrossingConfig): ThresholdTracker — Track a numeric source against named thresholds and emit ordered crossing events with direction, hysteresis, once/repeat policy, and skipped-threshold handling. Deterministic and serializable; drives phase transitions, armor breaks, resource warnings, and any other threshold consumer.
|
|
161
|
+
- `createWeaponRuntime` (function): function createWeaponRuntime<TAim, THit>(config: WeaponRuntimeConfig<TAim, THit>): WeaponRuntime<TAim, THit> — Compose a portable weapon fire controller from the existing generic primitives — a {@link FireCadence} gate, an optional {@link Magazine}, a caller-provided raycast, and portable {@link resolveDamageHit} damage resolution. No `GameContext`, entity store, renderer, or default weapon is involved; the caller owns aiming, its raycaster, and all presentation, and the result carries full damage provenance for authority to apply and replicate.
|
|
148
162
|
- `encounterProgress` (function): function encounterProgress<TData>(state: EncounterState<TData>): { completed: number; total: number; done: boolean; } — Progress across the whole tree: how many phases have completed out of the total node count, plus whether the encounter has finished. Counts injected phases.
|
|
149
163
|
- `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.
|
|
150
164
|
- `filterOutcomeFor` (function): function filterOutcomeFor(outcome: AllocationOutcome, viewerId: string): AllocationOutcome — Project an allocation outcome for one viewer, enforcing replication authoritatively: private grants belonging to other recipients are dropped, and pools the viewer cannot claim (not eligible, or reserved for someone else) are hidden — so private rewards never leak across the wire while provenance on the viewer's own grants is preserved.
|
|
@@ -167,10 +181,12 @@
|
|
|
167
181
|
- `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
|
|
168
182
|
- `resolveShot` (function): function resolveShot(deps: ShotOriginDeps, from: string, aim: Aim, policy: ShotOriginPolicy = { kind: "eye" }): ResolvedShot | null — ⚠ undocumented
|
|
169
183
|
- `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`.
|
|
184
|
+
- `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).
|
|
170
185
|
- `rollCheck` (function): function rollCheck(input: CheckInput, rng: () => number = Math.random): CheckResult — Resolve a tabletop-style pass/fail roll against a target number with modifiers and crit/fumble bands.
|
|
171
186
|
- `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`.
|
|
172
187
|
- `tierAt` (function): function tierAt(value: number, tiers: readonly MeterTier[]): string | null — The highest tier id whose `at` threshold `value` has reached, or `null` below every tier — the pure lookup `createAccumulatorMeter`/`createEventMeter` call on every `add`/`tick`.
|
|
173
188
|
- `updateEncounter` (function): function updateEncounter<TData>(config: EncounterConfig<TData>, state: EncounterState<TData>, dt: number, ctx: EncounterContext = {}): EncounterStep<TData> — Advance an encounter by `dt` seconds against the caller `ctx`. Accumulates time, then repeatedly: evaluates the cursor node's completion predicate; on completion, either re-runs its subtree (`repeat`) or marks it complete and advances to the next sibling — bubbling up to parent groups whose children are exhausted — entering the next chain and emitting enter/spawn/complete events. Emits a final `done` event when the last phase completes. Bounded per call by node count; mutates and returns `state`.
|
|
189
|
+
- `vfxPresets` (const): const vfxPresets: { readonly arrow: { readonly kind: "projectile"; readonly color: 14207392; }; readonly bolt: { readonly kind: "projectile"; readonly color: 16777215; }; readonly fireball: { readonly kind: "projectile"; readonly color: 16738842; readonly durationMs: 460; }; readonly firebolt: { rea… — Ready-made visual flavors for the things players actually shoot, swing, and cast — so `ctx.scene.entity.vfx({ preset: "arrow", from, to })` renders a visible bolt with zero tuning, and `"lightning"`, `"web"`, `"slash"`, `"shield"`, `"heal"`, `"explosion"` likewise just work. Each entry only picks a `kind` (`projectile | beam | nova | glow | spark`), a color, and a lifetime; it is visual vocabulary (like named CSS colors), not a gameplay archetype — combat numbers stay in the caller's own effect/ability data. Any field passed to `vfx()` overrides the preset, and unknown names fall back to `spark` so a flavor typo still shows *something* rather than nothing.
|
|
174
190
|
|
|
175
191
|
## @jgengine/core/combat/abilityKit
|
|
176
192
|
|
|
@@ -350,7 +366,7 @@
|
|
|
350
366
|
- `ReceiveMap` (type): type ReceiveMap = Record<string, ReceiveRule> — ⚠ undocumented
|
|
351
367
|
- `ReceiveRule` (interface): interface ReceiveRule — ⚠ undocumented
|
|
352
368
|
- `SingleTargetEffectInput` (interface): interface SingleTargetEffectInput — ⚠ undocumented
|
|
353
|
-
- `createEffectSystem` (function): function createEffectSystem(deps: EffectSystemDeps): EffectSystem —
|
|
369
|
+
- `createEffectSystem` (function): function createEffectSystem(deps: EffectSystemDeps): EffectSystem — Resolve direct or area resource effects through caller-owned stat pools, including ordered absorption, restoration, modifiers, and lethal context.
|
|
354
370
|
- `resolveAreaTargets` (function): function resolveAreaTargets(spatial: CombatSpatialDeps, input: AreaTargetInput, accept?: (instanceId: string) => boolean): AreaTarget[] — ⚠ undocumented
|
|
355
371
|
|
|
356
372
|
## @jgengine/core/combat/encounterSequence
|
|
@@ -400,6 +416,7 @@
|
|
|
400
416
|
- `Magazine` (interface): interface Magazine — A per-weapon magazine: discrete loaded rounds, a timed reload that refills from a reserve pool, and the reserve-pool interaction itself — the primitive that replaces hand-rolling mag size, reload delay, and reserve bookkeeping per game (#536.2).
|
|
401
417
|
- `MagazineConfig` (interface): interface MagazineConfig — Tuning for `createMagazine`: mag capacity, reload delay, and where reserve ammo is drawn from.
|
|
402
418
|
- `MagazineReserve` (interface): interface MagazineReserve — Draws ammo for a `Magazine`'s reload from wherever the reserve pool actually lives.
|
|
419
|
+
- `MagazineSnapshot` (interface): interface MagazineSnapshot — Complete serializable state for a {@link Magazine}; `reserve: null` means infinite reserve.
|
|
403
420
|
- `createMagazine` (function): function createMagazine(config: MagazineConfig): Magazine — Builds a {@link Magazine}: discrete loaded ammo, a timed reload, and reserve-pool interaction.
|
|
404
421
|
|
|
405
422
|
## @jgengine/core/combat/projectiles
|
|
@@ -526,6 +543,25 @@
|
|
|
526
543
|
- `ThresholdTrackerSnapshot` (interface): interface ThresholdTrackerSnapshot — Serializable tracker state for save/load.
|
|
527
544
|
- `createThresholdTracker` (function): function createThresholdTracker(config: ThresholdCrossingConfig): ThresholdTracker — Track a numeric source against named thresholds and emit ordered crossing events with direction, hysteresis, once/repeat policy, and skipped-threshold handling. Deterministic and serializable; drives phase transitions, armor breaks, resource warnings, and any other threshold consumer.
|
|
528
545
|
|
|
546
|
+
## @jgengine/core/combat/vfxPresets
|
|
547
|
+
|
|
548
|
+
- `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.
|
|
549
|
+
- `VfxPreset` (interface): interface VfxPreset — A named visual flavor for a one-shot `combat.vfx` burst: the archetype (`kind`), a `0xRRGGBB` tint, and optional default `durationMs`/`radius`. Purely presentation — a preset never touches damage, range, or any authoritative value; it just answers "what does this attack look like" so a game does not hand-roll colors and archetypes.
|
|
550
|
+
- `VfxPresetName` (type): type VfxPresetName = keyof typeof vfxPresets — A named key into {@link vfxPresets} (`"arrow" | "fireball" | "lightning" | "web" | "slash" | "shield" | "heal" | ...`).
|
|
551
|
+
- `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).
|
|
552
|
+
- `vfxPresets` (const): const vfxPresets: { readonly arrow: { readonly kind: "projectile"; readonly color: 14207392; }; readonly bolt: { readonly kind: "projectile"; readonly color: 16777215; }; readonly fireball: { readonly kind: "projectile"; readonly color: 16738842; readonly durationMs: 460; }; readonly firebolt: { rea… — Ready-made visual flavors for the things players actually shoot, swing, and cast — so `ctx.scene.entity.vfx({ preset: "arrow", from, to })` renders a visible bolt with zero tuning, and `"lightning"`, `"web"`, `"slash"`, `"shield"`, `"heal"`, `"explosion"` likewise just work. Each entry only picks a `kind` (`projectile | beam | nova | glow | spark`), a color, and a lifetime; it is visual vocabulary (like named CSS colors), not a gameplay archetype — combat numbers stay in the caller's own effect/ability data. Any field passed to `vfx()` overrides the preset, and unknown names fall back to `spark` so a flavor typo still shows *something* rather than nothing.
|
|
553
|
+
|
|
554
|
+
## @jgengine/core/combat/weaponFire
|
|
555
|
+
|
|
556
|
+
- `FireCadence` (interface): interface FireCadence — A minimal minimum-interval rate gate for a repeated action — weapon fire, ability spam, any "no faster than N per second" rule. Deterministic and serializable: its whole state is the elapsed time since the last action, exposed via {@link FireCadence.elapsedMs}.
|
|
557
|
+
- `FireCadenceConfig` (interface): interface FireCadenceConfig — Tuning for {@link createFireCadence}: the minimum interval between actions and whether it starts ready.
|
|
558
|
+
- `WeaponFireResult` (interface): interface WeaponFireResult<THit> — The result of a {@link WeaponRuntime.fire} attempt. On `"fired"`, `hits` is whatever the caller's raycast returned and `resolutions` holds the portable damage provenance for each hit that produced a {@link DamageHitInput}; on any blocked status both are empty and no round was spent.
|
|
559
|
+
- `WeaponFireStatus` (type): type WeaponFireStatus = "fired" | "cooling" | "empty" | "reloading" — Why a {@link WeaponRuntime.fire} attempt resolved the way it did.
|
|
560
|
+
- `WeaponRuntime` (interface): interface WeaponRuntime<TAim, THit> — A composed weapon fire controller: a {@link FireCadence} gate, an optional {@link Magazine}, the caller's raycast, and portable {@link resolveDamageHit} damage resolution wired together. The caller retains ownership of models, animations, muzzle flash, audio, input, camera, aiming, and the raycaster itself.
|
|
561
|
+
- `WeaponRuntimeConfig` (interface): interface WeaponRuntimeConfig<TAim, THit> — Configuration for {@link createWeaponRuntime}. Everything time/ammo-related is plain data; the two callbacks are where the caller keeps ownership of aiming, its own raycaster/projectile query, and how a hit maps to damage. There is deliberately no default weapon, projectile, or damage table here — those are game content.
|
|
562
|
+
- `createFireCadence` (function): function createFireCadence(config: FireCadenceConfig): FireCadence — Build a {@link FireCadence} rate gate. Reach for this instead of hand-tracking a `lastFiredAt` timestamp and comparing against a fire interval per game.
|
|
563
|
+
- `createWeaponRuntime` (function): function createWeaponRuntime<TAim, THit>(config: WeaponRuntimeConfig<TAim, THit>): WeaponRuntime<TAim, THit> — Compose a portable weapon fire controller from the existing generic primitives — a {@link FireCadence} gate, an optional {@link Magazine}, a caller-provided raycast, and portable {@link resolveDamageHit} damage resolution. No `GameContext`, entity store, renderer, or default weapon is involved; the caller owns aiming, its raycaster, and all presentation, and the result carries full damage provenance for authority to apply and replicate.
|
|
564
|
+
|
|
529
565
|
## @jgengine/core/stats/accumulatorMeter
|
|
530
566
|
|
|
531
567
|
- `AccumulatorMeter` (interface): interface AccumulatorMeter — ⚠ undocumented
|
|
@@ -569,6 +605,7 @@
|
|
|
569
605
|
- `StatModifier` (interface): interface StatModifier — ⚠ undocumented
|
|
570
606
|
- `StatModifierSet` (type): type StatModifierSet<TStat extends string> = Partial<Record<TStat, StatModifier>> — ⚠ undocumented
|
|
571
607
|
- `Stats` (interface): interface Stats<TStat extends string> — ⚠ undocumented
|
|
608
|
+
- `StatsSnapshot` (interface): interface StatsSnapshot<TStat extends string> — Complete plain-data state for a {@link Stats} runtime.
|
|
572
609
|
- `createStats` (function): function createStats<TStat extends string>(base: Record<TStat, number>, options?: CreateStatsOptions): Stats<TStat> — A stat block whose base values take stacking, timed buffs and debuffs, resolving the modified value on read.
|
|
573
610
|
|
|
574
611
|
## @jgengine/core/stats/statPool
|
|
@@ -120,6 +120,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
120
120
|
|
|
121
121
|
- `createEventMeter` (function) · `import { createEventMeter } from "@jgengine/core/combat"`
|
|
122
122
|
|
|
123
|
+
## fire-cadence — a serializable minimum-interval rate gate for repeated actions (weapon fire, ability spam)
|
|
124
|
+
|
|
125
|
+
- `createFireCadence` (function) · `import { createFireCadence } from "@jgengine/core/combat"`
|
|
126
|
+
|
|
123
127
|
## immunity-window — reject damage during a per-target invulnerability window installed by transitions
|
|
124
128
|
|
|
125
129
|
- `createImmunityWindow` (function) · `import { createImmunityWindow } from "@jgengine/core/combat"`
|
|
@@ -204,10 +208,22 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
204
208
|
|
|
205
209
|
- `resolveStatusApplication` (function) · `import { resolveStatusApplication } from "@jgengine/core/combat"`
|
|
206
210
|
|
|
207
|
-
## status-effects —
|
|
211
|
+
## status-effects — commit direct or area effects through caller-owned stat pools with ordered spillover and lethal results
|
|
208
212
|
|
|
209
213
|
- `createEffectSystem` (function) · `import { createEffectSystem } from "@jgengine/core/combat"`
|
|
210
214
|
|
|
211
215
|
## threshold-crossings — emit ordered crossing events as a numeric source passes named thresholds with hysteresis and once/repeat policy
|
|
212
216
|
|
|
213
217
|
- `createThresholdTracker` (function) · `import { createThresholdTracker } from "@jgengine/core/combat"`
|
|
218
|
+
|
|
219
|
+
## vfx-preset-resolve — turn a named attack flavor into concrete vfx kind/color/duration with per-field overrides
|
|
220
|
+
|
|
221
|
+
- `resolveVfxPreset` (function) · `import { resolveVfxPreset } from "@jgengine/core/combat"`
|
|
222
|
+
|
|
223
|
+
## vfx-presets — render a named attack visual (arrow, fireball, lightning, web, slash, shield, heal, explosion) in one call
|
|
224
|
+
|
|
225
|
+
- `vfxPresets` (const) · `import { vfxPresets } from "@jgengine/core/combat"`
|
|
226
|
+
|
|
227
|
+
## weapon-runtime — compose cadence, magazine, a caller-owned raycast, and portable damage resolution into a fire controller
|
|
228
|
+
|
|
229
|
+
- `createWeaponRuntime` (function) · `import { createWeaponRuntime } from "@jgengine/core/combat"`
|