@jgengine/editor 0.26.0 → 0.27.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 CHANGED
@@ -26,6 +26,50 @@ At publish, rename this heading to the new version and mirror the entries into
26
26
  `packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
27
27
  -->
28
28
 
29
+ ## 0.27.0
30
+
31
+ ### Migrate
32
+
33
+ - **Bump lockstep SDK packages to `^0.27.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.24.0`; `@jgengine/github` is `0.14.0`.
34
+
35
+ ### Added
36
+
37
+ - `rollLoot` (`@jgengine/core/loot/lootGen`) rolls a named item from a caller `LootCatalog`: bases with mount slots, parts, affix pools, weighted rarity tiers, manufacturer personalities and a `levelScale` curve. Stats are `base × rarity.statScale × levelScale`, then part stats/multipliers, maker `bias`/`scale`, then additive and multiplicative affixes; additive affix rolls scale with level, multiplicative ones do not.
38
+ - `createLootRoller` wraps a validated catalog with roll counts per rarity, `snapshot()`/`restore()`/`reset()` and an optional devtools probe; `assertLootCatalog` rejects duplicate ids, unresolvable pools, empty item slots and a non-ascending level curve at construction.
39
+ - `validateLootItem` returns coded issues (`unknown-base`, `manufacturer-base`, `missing-slot-part`, `part-category`, `unknown-affix`, `empty-name`, …) and `compareLootItems` returns sorted per-stat deltas plus a weighted score with caller-named `lowerIsBetter` stats.
40
+ - `lootWeaponDefinition` (`@jgengine/core/loot/lootWeapon`) projects a rolled item onto a `WeaponDefinition` through a caller stat map, so a generated gun runs in `createWeaponSystem` after `assertWeaponDefinition`.
41
+ - `defineXpCurve` (`@jgengine/core/gameplay`) resolves one authored growth rule (`flat`, `linear`, `power`, `geometric`, `table`) plus a `maxLevel` cap into per-level thresholds, lifetime totals, a plain-words rule string and a `leveling` track.
42
+ - `createSkillTree` spends and refunds points over authored nodes with `cost`, `requires`, `excludes`, passives, actives and multi-node `synergies`; `resolved()` returns the stat modifiers and actions, and state snapshots/restores as `{ points, ranks }`.
43
+ - `gearScore` / `compareGearSets` score a whole loadout from stat weights, level, rarity and slot coverage.
44
+ - `lootStatContributions` and `scoreLootStats` expose the weight policy `compareLootItems` already used.
45
+ - `createEquipment` (`@jgengine/core/inventory/equipment`) wears gear by slot: slot defs declare accepted kinds and the slots they block, item defs declare the slots an item may occupy, a filled slot swaps and hands back the replaced item, and a two-handed item and the slot it blocks cannot both be filled. `state()` / `restore()` round-trip the worn set; a devtools probe reports filled slots.
46
+ - `characterLoad` and `canCarry` (`@jgengine/core/inventory/carryLoad`) weigh carried inventories plus worn gear against a capacity and hand back the shared encumbrance bands (tier, `moveMultiplier`, sprint/jump gates, `remaining`), so a stash left out of `carried` adds no weight.
47
+ - `fitShapedItem` (`@jgengine/core/inventory/shapedGrid`) places a shaped item at its first fit or names why not: `too-large` (with both sizes), `no-space`, `budget-exceeded`.
48
+ - `InventorySet.snapshot()` / `restore()` / `reset()` (`@jgengine/core/inventory/inventoryModel`) save and reload every declared inventory as detached plain data, so a bag and a stash never share live slots.
49
+ - `splitStack` takes an optional stack `limit`, rejecting `over-stack` instead of merging past the max stack.
50
+ - `createLaneGraph` and `laneGraphFromDocument` (`@jgengine/core/world`) read an authored lane graph out of the scene document: `traffic_lane` paths are one directed span each, `road` paths (the street bake) become a lane per direction split where streets cross, `traffic_intersection` markers carry signal phases and right-of-way in `meta`, and `traffic_parking` markers place kerbside spots.
51
+ - `createTraffic` (`@jgengine/core/world`) runs cars on that graph: IDM car following and braking, one car at a time through each intersection box behind a signal phase or first-come right-of-way, lane changes across authored neighbours, parking that reserves a spot, deterministic injected variety, `snapshot`/`restore`, and a devtools probe.
52
+ - `Traffic.setDemand(count)` is the fleet target; the time-of-day curve stays the caller's policy.
53
+ - `applyVehicleParts` (`@jgengine/core/world`) composes data-driven tuning parts onto a `KinematicVehicleTuning` — stat multipliers/addends applied in install order plus an optional tire grip curve — and returns the composed tuning, the installed ids, and a `VehicleFeel` (top speed, drive, brakes, turn rate, handbrake grip, and the grip curve's peak/slide). `withPart` swaps a part within its slot and `feelOf` reads the numbers out of any tuning.
54
+ - `createVehicleDamage` (`@jgengine/core/world`) is fixed-zone crash damage wired to handling and visuals: impacts accumulate into discrete stages, each stage costs grip/drive/brakes/top speed/drag and pulls the steering (`effect`), and each zone shows one bounded dent (`deformation`) that never exceeds its authored `deform`. Parts can detach at a set stage, a total impulse can disable the vehicle, and `snapshot`/`restore` resume mid-crash. `damagedTuning` and `damagedAxis` fold the effect into a tuning or an input, and `modifiers()` carries it as per-tick overrides.
55
+ - `KinematicVehicleTuning.weightTransfer` (`@jgengine/core/world`) adds load-transfer grip: longitudinal transfer under braking/traction and lateral transfer under cornering cut the tire grip curve (tire load sensitivity), so braking into a corner holds less slide than coasting through it. Each tick reports `weightTransfer` and `loadGrip`; omitting the block keeps the previous flat-grip model exactly.
56
+ - `KinematicVehicleModifiers.gripScale` scales the grip curve for one tick, mirroring `VehicleDynamicsModifiers.gripScale` so a damage layer drives either sim.
57
+ - `decal` scene kind (`@jgengine/core/world/decalKind`) and its renderer (`@jgengine/shell/scene/DecalRenderer`): dirt, cracks, paint and blood decals authored as placed markers, draped on the live terrain or standing on an authored tilt, with a deterministic procedural texture per style+seed.
58
+ - `TerrainMaterialLayer.material` (`@jgengine/core/world/terraform`): a material library name whose scanned color map paints that terrain layer. The runtime samples up to four layer maps at each layer's `tiling` and blends them through the authored mask weights (`createTerrainDetailMaterial`'s `layerMaps`), so painted terrain blends between authored layers instead of tiling one flat map.
59
+ - `grass_field` volumes now paint a ground-cover mat under their blades (`groundCover` rule, `@jgengine/shell/terrain/GrassCover`), so an authored patch reads as turf rather than bare ground with stubble. `GrassEnvironmentConfig.groundCover` does the same for `grass()` patches.
60
+ - `ModelConfig.wind` (`@jgengine/core/game/playableGame`) and `applyModelWind` (`@jgengine/shell/render/modelWind`): per-model foliage sway with height-ramped travel, shared gust clock and per-model strength/speed/direction.
61
+
62
+ ### Fixed
63
+
64
+ - The player shell re-renders only for the ctx slices it reads — the possessed entity and the touch control mode — instead of on every `ctx` write. Subscribing the shell root to the raw ctx version reconciled the canvas, the whole actor tree and the HUD on each per-frame entity pose write, which is what `subscribeMembership` and the markers' imperative `useFrame` pose reads exist to avoid (#625). Scrap Signal: 86.6 ms → 16.7 ms per frame at the same 621 draws; Harbor Heat: 102.8 ms → 46.2 ms at the same ~7.2k draws.
65
+ - The `ocean()` environment feature passes the live ground to its shader, so the sea shades shallow→deep and its shoreline foam band lands where the bed meets the surface.
66
+
67
+ ### Numbers
68
+
69
+ - Test catalog (60/25/10/4/1 rarity weights) over 10,000 drops at level 20, fixed seed: common 59.41% (target 60%), uncommon 25.66% (25%), rare 9.58% (10%), epic 4.20% (4%), legendary 1.15% (1%); every tier inside its 4σ binomial bound. All 10,000 items validated with zero issues and composed names (e.g. `Voss Rare Long Keen Lightframe of Reach`, `Tallow Long Lightframe`, `Voss Snub Lightframe of Steadying`).
70
+ - Level curve `[[1,1],[15,1.6],[30,2.4],[60,4]]`: a level-60 roll's base and additive affix values are 4× the same seed at level 1; maker personalities move the same roll (`Voss` +power/−cadence, `Tallow` the reverse).
71
+ - A rolled epic projects to a hitscan `WeaponDefinition` (rate, damage, integer magazine capacity, spread, range), passes `assertWeaponDefinition`, and its first `tick` fires one shot and one impact through `createDamageRuntime` with the rolled capacity spent.
72
+
29
73
  ## 0.26.0
30
74
 
31
75
  ### Migrate
@@ -1,6 +1,7 @@
1
1
  import { createTerrainSnapshot, editableTerrainFromSnapshot, } from "@jgengine/core/world/terraform";
2
2
  import { resolveScatter, resolveScatterRegion, scatterRegionEstimate, scatterRegionFromPath, SCATTER_PATH_KIND, } from "@jgengine/core/world/scatterRegion";
3
3
  import { TERRAIN_MATERIALS } from "../uiStore.js";
4
+ import { libraryMaterialAsset } from "@jgengine/assets/catalogs/materialLibrary";
4
5
  function brushError(request) {
5
6
  if (![request.x, request.z, request.radius ?? 8, request.strength ?? 1].every(Number.isFinite))
6
7
  return "brush coordinates, radius and strength must be finite";
@@ -153,7 +154,24 @@ export const terrainHandlers = {
153
154
  if (new Set(request.layers.map((layer) => layer.id)).size !== request.layers.length || request.layers.some((layer) => layer.id.length === 0 || layer.surface.length === 0)) {
154
155
  return { ok: false, error: "terrain layers require unique nonempty ids and nonempty surfaces" };
155
156
  }
156
- ctx.session.dispatch({ type: "setTerrainLayers", layers: request.layers });
157
+ const layers = [];
158
+ for (const layer of request.layers) {
159
+ if (layer.material === undefined || layer.material.length === 0) {
160
+ layers.push({ ...layer, materialUrl: undefined });
161
+ continue;
162
+ }
163
+ let url;
164
+ try {
165
+ url = libraryMaterialAsset(layer.material).textures?.color?.url;
166
+ }
167
+ catch {
168
+ return { ok: false, error: `unknown material library name "${layer.material}"` };
169
+ }
170
+ if (url === undefined)
171
+ return { ok: false, error: `material "${layer.material}" has no color map` };
172
+ layers.push({ ...layer, materialUrl: url });
173
+ }
174
+ ctx.session.dispatch({ type: "setTerrainLayers", layers });
157
175
  return { ok: true, result: { layers: ctx.session.getState().document.terrain?.layers ?? [] } };
158
176
  },
159
177
  blend_terrain: (ctx, request) => {
@@ -72,7 +72,7 @@ export const RPC_FIELD_SCHEMAS = {
72
72
  auto_paint: [req(s("surface")), n("minSlope"), n("maxSlope"), n("minHeight"), n("maxHeight")],
73
73
  terrain_materials: [],
74
74
  terrain_layers: [],
75
- set_terrain_layers: [req(oa("layers", [req(s("id")), req(s("surface")), n("roughness"), n("tiling"), b("triplanar"), s("tint"), n("opacity")]))],
75
+ set_terrain_layers: [req(oa("layers", [req(s("id")), req(s("surface")), n("roughness"), n("tiling"), b("triplanar"), s("tint"), n("opacity"), s("material")]))],
76
76
  blend_terrain: [req(s("surface")), req(n("x")), req(n("z")), n("radius"), n("strength"), s("shape", SHAPE)],
77
77
  convert_scatter: [req(s("pathId"))],
78
78
  generate_streets: [s("volumeId"), v3("center"), n("halfX"), n("halfZ"), s("seed"), s("mode", ["net", "circuit"]), s("kind"), o("params")],
package/dist/mcp/tools.js CHANGED
@@ -64,7 +64,7 @@ const TOOL_DESCRIPTIONS = [
64
64
  { name: "fill_terrain", description: "Fill the whole terrain with one material, or null to clear all painted surfaces." },
65
65
  { name: "auto_paint", description: "Paint a material into every cell matching a slope/height rule (e.g. rock on steep, snow up high)." },
66
66
  { name: "terrain_materials", description: "List the terrain paint palette (material id, label, color)." },
67
- { name: "terrain_layers", description: "List the terrain material layer stack (id, surface, roughness/tiling/triplanar/tint/opacity)." },
67
+ { name: "terrain_layers", description: "List the terrain material layer stack (id, surface, roughness/tiling/triplanar/tint/opacity/material — a material library name paints the layer with its scanned map)." },
68
68
  { name: "set_terrain_layers", description: "Replace the reorderable terrain material layer stack; a params-only edit keeps painted blends." },
69
69
  { name: "blend_terrain", description: "Blend-paint a material layer's weight at x/z (weighted multi-layer blend); auto-adds the layer." },
70
70
  { name: "convert_scatter", description: "Detach a foliage/scatter region into individually-editable placed prop markers, removing the region." },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgengine/editor",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Scene/world/asset editor for JGengine — loaded lazily by the runner; never import from game entrypoints.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -31,11 +31,11 @@
31
31
  "test": "bun test src"
32
32
  },
33
33
  "dependencies": {
34
- "@jgengine/assets": "^0.26.0",
35
- "@jgengine/core": "^0.26.0",
36
- "@jgengine/navbake": "^0.26.0",
37
- "@jgengine/react": "^0.26.0",
38
- "@jgengine/shell": "^0.26.0"
34
+ "@jgengine/assets": "^0.27.0",
35
+ "@jgengine/core": "^0.27.0",
36
+ "@jgengine/navbake": "^0.27.0",
37
+ "@jgengine/react": "^0.27.0",
38
+ "@jgengine/shell": "^0.27.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@react-three/drei": "^10.0.0",
@@ -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.26.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
9
+ - `VERSION` (const): const VERSION: "0.27.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.26.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
288
+ - `VERSION` (const): const VERSION: "0.27.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
289
289
 
290
290
  ## @jgengine/core/runtime/adapter
291
291