@aethervtt/shard-project 0.0.1 → 0.2.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/dist/docs.js CHANGED
@@ -7,6 +7,7 @@ import { allComponents } from "@aethervtt/shard-core";
7
7
  import { allGenerators } from "@aethervtt/shard-procgen";
8
8
  import { saveJsonSchema } from "@aethervtt/shard-save";
9
9
  import { prefabJsonSchema, sceneJsonSchema } from "@aethervtt/shard-scene";
10
+ import { capturePlanJsonSchema, perfRecordJsonSchema } from "@aethervtt/shard-verify";
10
11
  import { BUILTIN_PLUGINS, manifestJsonSchema } from "./manifest.js";
11
12
  const GENERATED_START = "<!-- shard:generated -->";
12
13
  const GENERATED_END = "<!-- /shard:generated -->";
@@ -343,9 +344,15 @@ Add \`"sprite"\` to \`plugins\` in \`shard.json\`. 2D uses the same renderer as
343
344
  "duration": 0.1 }, ...], "loop": "loop", "events": [{ "frame": 2, "name": "step" }] }\`, then
344
345
  \`"sprite/SpriteAnimation": { "clip": { "path": "assets/walk.clip.json" } }\`. Read events with
345
346
  \`world.reader(SpriteAnimationEvent)\`.
346
- 4. Tilemaps: \`level.tilemap.json\` layers are base64 u16 tiles (atlas region + 1, 0 empty), and
347
- \`"sprite/Tilemap": { "atlas": ..., "data": ..., "tileSize": [1, 1] }\`. Edit at runtime with
348
- \`setTile(world, map, x, y, tile)\`; only that chunk re-uploads.
347
+ 4. Tilemaps: \`level.tilemap.json\` names tiles by atlas region in a \`palette\`, and stores each layer
348
+ as \`chunks\` of rows: \`"0,0": ["grass*12 stone*4", "grass stone:fx . water*13"]\` (runs of
349
+ \`name[:flags][*count]\`, \`.\` empty, flags \`fx\`/\`fy\`/\`r90\` joined by \`+\`), then
350
+ \`"sprite/Tilemap": { "atlas": ..., "data": ..., "tileSize": [1, 1] }\`. Read and edit cells by
351
+ name with MCP \`tilemap_read\` / \`tilemap_edit\` (live; \`save: true\` writes the file) or
352
+ \`shard tiles read|edit\`; \`shard tiles <file> --encoding base64\` compacts a big generated map.
353
+ In code: \`setTile(world, map, x, y, data.tileId('stone'))\`; only that chunk re-uploads. On a
354
+ tabletop, add \`"render/GroundLayer": { "band": 10 }\` and lay it flat (\`tilemapOnGround()\`'s
355
+ rotation): it draws among the ground bands, lit by 3D lights, and picks return the cell.
349
356
  5. Pixel art: \`"render/PixelPerfect": { "pixelsPerUnit": 16 }\` on an orthographic camera.
350
357
  6. Check: \`shard validate\`, MCP \`preview_asset\` on the atlas (regions outlined and numbered) or
351
358
  the clip, and \`render.describe\` \u2192 \`sprites\` (draw calls, sprites per layer, tilemap chunks).
@@ -740,6 +747,9 @@ whose path no longer exists is kept in the file and logged as \`prefab/stale-ove
740
747
  `,
741
748
  "add-sound.md": `# Add sound
742
749
 
750
+ For authored events, mixers, snapshots, acoustics, adaptive music and offline previews, read
751
+ [the audio guide](../audio.md).
752
+
743
753
  Add \`"audio"\` to \`plugins\` in \`shard.json\`. Drop \`.ogg\`, \`.mp3\`, \`.wav\`, or \`.flac\` files under
744
754
  \`assets/\` and run \`shard import\`: each is an \`AudioClip\` (MCP \`get_asset\` shows duration, channels,
745
755
  sample rate, codec). Files stay compressed. Clips under \`music/\` stream; set \`"mode": "stream"\` in
@@ -758,7 +768,7 @@ the \`.meta\` for other long files, and \`loopStart\`/\`loopEnd\` (seconds) for
758
768
 
759
769
  - Sources autoplay; set \`playing\` to start or stop one. A clip that doesn't loop clears
760
770
  \`playing\` at its end and sends \`audio/AudioFinished\`. Spatial sources follow their transform;
761
- the listener is the first \`audio/AudioListener\` (put it on the camera).
771
+ the fallback listener has the lowest entity ID; setAudioListener selects one explicitly.
762
772
  - \`rolloff\`: \`inverse\` (default, natural), \`linear\` (silent at \`maxDistance\`; past it the source
763
773
  goes virtual and holds no voice), \`exponential\`. \`panning\`: \`equal-power\` or \`hrtf\`.
764
774
  - One-shots from code: \`playSound(world, weapon.sound, { position: muzzle, bus: 'sfx' })\` from
@@ -1158,6 +1168,81 @@ orbit down to 0.4 m vertex spacing on the ground. Put the camera in the planet's
1158
1168
  \`describe_terrain\` shows \`detail.lodBias\` above 1 while it's coarsening.
1159
1169
  - Give it air: \`"render/Atmosphere": {}\` on the planet entity takes the planet's radius, draws
1160
1170
  the sky from the ground to orbit, and hazes distant terrain (tune-an-atmosphere.md).
1171
+ - Rocks, trees and grass: give biomes (or the planet) a scatter set (scatter-props-and-foliage.md).
1172
+ - A bounded landscape you shape (roads, valleys) rather than a whole planet: make-a-terrain.md.
1173
+ `,
1174
+ "make-a-terrain.md": `# Make a heightfield terrain (an open world)
1175
+
1176
+ A bounded landscape, 256 m to 64 km a side, shaped on purpose: a \`*.terrain.json\` file stacks
1177
+ height layers (noise, heightmap images, splines that flatten, raise or carve) and paint layers, and an
1178
+ entity with \`terrain/Terrain\` (it brings \`transform/Grid\`) draws, streams and collides with it. Add
1179
+ \`terrain\` to \`shard.json\`'s plugins (with \`render/forward\` and \`physics3d\`). The file's schema is
1180
+ \`.shard/schemas/terrain.schema.json\`; coordinates are metres from the terrain's corner.
1181
+
1182
+ \`\`\`json
1183
+ { "size": [4096, 4096], "spacing": 1, "heightRange": [-200, 800], "seed": 7,
1184
+ "splines": { "road": { "points": [[120, "ground", 300], [900, "ground", 420]], "width": 8, "falloff": 14 } },
1185
+ "height": [
1186
+ { "noise": { "path": "assets/noise/hills.noise.json" }, "scale": 260 },
1187
+ { "image": { "path": "assets/terrain/valley.height.png" }, "at": [2048, 1800], "size": [1200, 900],
1188
+ "rotation": 20, "range": [0, 140], "blend": "max", "falloff": 80 },
1189
+ { "spline": "road", "mode": "flatten" } ],
1190
+ "layers": [{ "name": "grass" }, { "name": "rock", "triplanar": true }, { "name": "gravel" }],
1191
+ "paint": [{ "layer": "grass" }, { "layer": "rock", "slope": [32, 90], "blend": 6 },
1192
+ { "layer": "gravel", "spline": "road", "blend": 2 }] }
1193
+ \`\`\`
1194
+
1195
+ - Sizes are whole numbers of roots: 64 \xD7 spacing \xD7 2^k a side, at most 64 roots (2048, 4096 or
1196
+ 16384 m at 1 m spacing). \`heightRange\` is what the bake stores in 16 bits: keep it close to the
1197
+ real range (a clipped height is \`terrain/out-of-range\`).
1198
+ - Heightmaps are 16-bit grayscale PNGs (name them \`*.height.png\`, or set \`"importer": "heightmap"\`
1199
+ in the \`.meta\`) or \`.r16\`/\`.r32\` raw files with \`width\` and \`height\` in the \`.meta\` settings.
1200
+ - \`shard import\` bakes stale terrains into \`.shard/cache/terrain\` (only the blocks an edit touches);
1201
+ \`shard terrain bake\` forces it, \`shard terrain stats\` shows size on disk and time per block.
1202
+ \`shard dev\` rebakes after you save the file.
1203
+ - Standing on it: characters, dynamic bodies, \`NavAgent\`s and \`terrain/TerrainAnchor\`s get
1204
+ heightfield colliders within \`colliderRadius\`, exactly the drawn ground. \`terrainHeightAt(world,
1205
+ terrain, x, z)\` is the ground under a point; \`await loadTerrainRegion(world, terrain, rect)\` first
1206
+ makes it exact away from anchors (spawn points). A plain \`nav/NavMesh\` paths on it.
1207
+ - Rocks and trees: \`terrain/Terrain.scatter\` takes a \`*.scatter.json\` (scatter-props-and-foliage.md).
1208
+ - Check it: MCP \`terrain_map\` (\`{ "mode": "layers" }\`, or \`"bake"\` to see which blocks an edit
1209
+ rebuilt), \`sample_terrain\` (\`{ "points": [[120, 300], [124, 300]] }\`: is the road flat across?),
1210
+ and \`describe_terrain\` (bake, pages, collider tiles). Overlays \`terrain-lod\`, \`terrain-pages\`
1211
+ and \`terrain-colliders\`.
1212
+ `,
1213
+ "scatter-props-and-foliage.md": `# Scatter rocks, trees and grass
1214
+
1215
+ Add \`scatter\` to \`shard.json\`'s plugins. A \`*.scatter.json\` (a \`scatter/ScatterSet\`) holds rules;
1216
+ put it on a biome (\`"scatter": { "path": "assets/scatter/forest.scatter.json" }\` in a \`*.biome.json\`:
1217
+ it applies where that biome dominates), on the planet (\`terrain/Planet.scatter\`: everywhere), or on
1218
+ a flat level's ground mesh (\`scatter/ScatterSurface\` next to its \`render/Mesh3d\`).
1219
+
1220
+ \`\`\`json
1221
+ { "rules": [
1222
+ { "name": "trees", "items": [{ "generator": "shard/Tree", "variants": 6 }],
1223
+ "density": 0.02, "spacing": 4, "masks": { "slope": [0, 25] }, "scale": [0.7, 1.3],
1224
+ "collider": "ball", "range": 400 },
1225
+ { "name": "grass", "kind": "foliage", "items": [{ "generator": "shard/GrassClump", "variants": 4 }],
1226
+ "density": 5, "avoid": ["trees"], "range": 50 } ] }
1227
+ \`\`\`
1228
+
1229
+ - Two kinds. \`prop\` rules spawn entities (\`scatter/Prop\`, with LODs and an optional collider) in
1230
+ chunks around cameras and \`TerrainAnchor\`s; \`foliage\` rules are GPU instances near cameras only,
1231
+ millions of them, no entities. Grass, flowers and small pebbles are foliage; anything you bump into is a prop.
1232
+ - Items are mesh generators (\`shard/Rock\`, \`shard/Tree\`, \`shard/Bush\`, \`shard/GrassClump\`,
1233
+ \`shard/Crystal\`, or a project's) with \`params\` and \`variants\` (4\u20138 shapes read as unique), or
1234
+ prefabs. Without a \`material\`, engine generators get \`scatter/Vegetation\` tinted for their kind.
1235
+ - \`density\` is per m\xB2 where the masks pass, \`spacing\` the least distance between items. Too dense
1236
+ for the spacing is \`scatter/density-too-high\` (it says the most that fits). Rule order is priority:
1237
+ \`avoid\` names earlier rules. \`align\` 0 stands items upright, 1 along the slope. \`sink\` buries
1238
+ them by a share of their height.
1239
+ - Placement is a global jittered lattice: the same seed puts the same tree in the same place on every
1240
+ visit and machine. A prop the game despawns stays gone (saved in \`scatter/Removed\`).
1241
+ - Check it: MCP \`preview_generator\` with \`{ "generator": "assets/scatter/forest.scatter.json" }\`
1242
+ shows the set on a 64 m patch from above and at eye level; \`describe_scatter\` gives per-rule
1243
+ counts; \`sample_scatter\` lists props near a point (\`{ "entity": "landing-pad", "radius": 10 }\`).
1244
+ Overlays \`scatter\` and \`foliage-chunks\` show placements in a screenshot.
1245
+ - \`scatter/Budget\` caps props spawned per frame; \`scatter/Wind\` sways foliage and leaves.
1161
1246
  `,
1162
1247
  "tune-an-atmosphere.md": `# Tune an alien sky
1163
1248
 
@@ -1237,6 +1322,47 @@ g.label(position, 'target', [1, 1, 1, 1])
1237
1322
  - \`shard screenshot <scene> --out shot.png --frames 60\` renders headless (no window needed).
1238
1323
  - With the MCP server: \`screenshot\` returns the image; \`step\` advances frames; \`press\`/\`hold\`
1239
1324
  drive input; \`query_entities\` / \`get_entity\` read state; \`recent_errors\` shows what failed.
1325
+ `,
1326
+ "check-in-browsers.md": `# Check it in real browsers
1327
+
1328
+ Headless screenshots are for regressions. Filtering, color and canvas compositing only show in a
1329
+ browser, and so do real frame times.
1330
+
1331
+ 1. Write a plan (\`.shard/schemas/capture-plan.schema.json\`): the URL (\`shard dev\`'s, or a host
1332
+ page), browsers, DPRs, viewport, and shots with a \`state\` the page's
1333
+ \`installCapturePage(app, { apply })\` understands. \`shard dev\` pages take shots as they are.
1334
+ 2. \`shard capture plan.json --out captures/latest --json\` takes them, runs \`steps\` and their
1335
+ \`expect\` checks in every client, and records each \`scenario\` (\`records/*.json\`).
1336
+ 3. \`shard compare captures/latest --approved captures/approved --json\` diffs against approved
1337
+ images (\u0394E2000 and SSIM) and writes \`report.html\`. Changed or new shots fail.
1338
+ 4. A change you meant: \`shard approve <shot> --reason "why it looks different"\`. A change you
1339
+ didn't: fix it. Approvals without a reason are refused.
1340
+ 5. \`shard perf-check captures/latest/records --plan plan.json\` fails on a broken threshold and
1341
+ names the metric, the value and the budget.
1342
+
1343
+ With the MCP server attached to a running page: \`metrics_reset\`, exercise the scene, then
1344
+ \`metrics_record\` returns the same record the scripts write.
1345
+ `,
1346
+ "find-what-is-slow.md": `# Find what's slow, or what stutters
1347
+
1348
+ The profiler is always on. Systems, schedules, the frame, render-graph encodes (\`render/<node>\`),
1349
+ GPU passes (\`gpu:<node>\`), worker jobs and asset loads are spans.
1350
+
1351
+ - On average: \`describe_perf\` (or \`shard profile\`'s top spans) lists last, avg, p95 and max ms per
1352
+ span over 120 frames. \`{ "spans": ["render"] }\` keeps the spans a key covers.
1353
+ - Against a budget: \`describe_budgets\` names the machine, lists every budget with its latest p95
1354
+ (over budget first), and splits a scenario's frame into slices, each measured share next to
1355
+ its budget share. Where \`describe_perf\` says \`"overlapping": true\` (Apple and other tile GPUs),
1356
+ pass times don't add up: \`ablate_passes\` measures what each pass costs.
1357
+ - A stutter: \`capture_perf { "frames": 300 }\`, or a flight recorder that waits for one:
1358
+ \`{ "until": { "frameMs": 30 } }\`. Read \`worst\`: each slow frame lists \`over\`, the spans that ran
1359
+ longest above their median there. The first one is the lead.
1360
+ - Your own code: \`const SPAN = defineSpan('<project>/spawn')\` once, then in a system
1361
+ \`const t = profiler.begin(SPAN)\` \u2026 \`profiler.end(t)\` with \`world.resource(ProfilerResource)\`.
1362
+ - The line: \`shard profile --cpu-prof\` (or \`"sample": true\`) adds the hottest functions and
1363
+ writes a \`.cpuprofile\`. Traces go to \`.shard/captures/*.trace.json\`: open them in Perfetto.
1364
+ - In a browser, \`shard dev\`'s pages are cross-origin isolated, so timings are to 5 \xB5s. Turn on the
1365
+ \`perf\` overlay (\`debug_overlays\`) to watch frame time and the top spans live.
1240
1366
  `
1241
1367
  };
1242
1368
  function agentsBlock(manifest) {
@@ -1252,13 +1378,16 @@ component schemas, game code is ECS (components, systems) in \`scripts/\`.
1252
1378
  shard validate --json # manifest, assets, scenes: every error with a path
1253
1379
  shard import --json # import new and changed asset files
1254
1380
  shard mv <from> <to> # move an asset and rewrite references to it
1381
+ shard tiles read <map> # tilemap cells as rows; tiles edit <map> --cell x,y=name
1255
1382
  shard run --frames 600 # headless run
1383
+ shard profile --frames 600 # capture spans: the worst frames and what grew in them
1256
1384
  shard screenshot ${manifest.startScene} --out shot.png
1257
1385
  shard test --json # gameplay tests in tests/
1258
1386
  shard check --json # type-check scripts: file, line, column
1259
1387
  shard gen <generator> --seeds 1-9 --out sheet.png # preview a generator's outputs
1260
1388
  shard dev # play it in a browser; saves hot reload in place
1261
- shard docs # regenerate this block, .agents/, and .shard/schemas/
1389
+ shard capture plan.json # real-browser shots and perf records; then compare, approve, perf-check
1390
+ shard docs # regenerate this block, .agents/, and .shard/schemas/
1262
1391
  shard mcp # MCP server for this project (see .mcp.json)
1263
1392
  \`\`\`
1264
1393
 
@@ -1272,6 +1401,9 @@ shard mcp # MCP server for this project (see .mcp.json)
1272
1401
  - \`.agents/components.md\`: every component and field; \`.agents/assets.md\`: importers and
1273
1402
  project data types;
1274
1403
  \`.agents/errors.md\`: error codes
1404
+ - \`.agents/audio.md\`: authored sound, music, mixing and measured previews
1405
+ - \`.agents/effects.md\`: reusable effect timelines, target bindings and owned lifetimes
1406
+ - \`.agents/vfx.md\`: trails, beams, projected decals and receiver masks
1275
1407
  - \`.agents/skills/\`: recipes for common tasks
1276
1408
 
1277
1409
  Engine plugins available: ${Object.entries(BUILTIN_PLUGINS).map(([n, d]) => `\`${n}\` (${d})`).join("; ")}.
@@ -1293,8 +1425,143 @@ Project-specific notes for agents go here; \`shard docs\` keeps them.
1293
1425
  ${existing}`;
1294
1426
  return `${existing.slice(0, start)}${block}${existing.slice(end + GENERATED_END.length)}`;
1295
1427
  }
1428
+ const AUDIO_GUIDE = `# Authoring audio
1429
+
1430
+ Enable \`audio/events\` for layered events and parameters, \`audio/mixer\` for effects and snapshots,
1431
+ \`audio/acoustics\` for zones/obstruction, and \`audio/music\` for adaptive music. Their dependencies
1432
+ are enabled automatically. Base \`audio\` keeps AudioSource/playSound for simple clips.
1433
+
1434
+ Assets are ordinary JSON: \`.audio-event.json\`, \`.audio-parameters.json\`, \`.audio-collection.json\`,
1435
+ \`.audio-mixer.json\`, \`.audio-snapshot.json\` and \`.audio-music.json\`. Use their generated schemas
1436
+ under \`.shard/schemas\`. Import edits with \`shard import\`; references can use paths or stable GUIDs.
1437
+ Authored gains use dB and pitches use semitones. Legacy volume/gain use linear multipliers.
1438
+
1439
+ Prepare a collection with \`await prepareAudio(world, ref)\`, play its events with \`playAudioEvent\`,
1440
+ and release the returned lease when its scope ends. Active instances retain their own pins.
1441
+ Use local \`parameters\` for surface labels, RPM or intensity; shared parameter assets connect
1442
+ weather/environment values to events, music and mixer curves. Built-in distance/speed are readonly.
1443
+ Sequence/no-repeat selectors retain bounded session history. Layers have independent RNG streams.
1444
+
1445
+ Simulation event decisions follow timeScale and pause. Their samples normally follow audio time;
1446
+ \`pitchLinked\` opts into scaled playback. Realtime UI/ambience can continue without visual frames.
1447
+ Transient blocked events expire after 250 ms by default. \`wait\` preserves their phase; \`restart\`
1448
+ starts their timeline again after unlock. Choose an explicit AudioListener with setAudioListener;
1449
+ the fallback is the lowest live listener entity ID. There is one audio session per app/protocol target.
1450
+
1451
+ Mixer domain buses (music/sfx/ui/voice) are direct children of master. Put submixes below them.
1452
+ Each event layer can declare up to eight \`sends\` with \`target\`, \`gainDb\`, and an optional
1453
+ parameter/points curve. Declare those routes in its mixer bus first. Per-instance sends tap after
1454
+ spatialization and preserve the originating category's user gate. Event \`tags\` can drive ducking;
1455
+ only physically rendering voices trigger it by default. Duck rules also include descendant buses.
1456
+
1457
+ Parent inserts process the summed children once. Pre/post sends feed named returns, and user
1458
+ category mute gates that category's dry and wet output. Apply imported mixer edits with
1459
+ \`await configureAudioMixer(world, ref)\`: scalar edits preserve effect history; topology/IR
1460
+ changes crossfade within the overlap budget. Delay and convolution tails are finite;
1461
+ decoded PCM and generated DSP kernels have separate bounded memory budgets. Snapshots compose
1462
+ by priority and stable identity; they do not overwrite user volume settings.
1463
+ Player controls apply to master and ordinary direct-child categories; nested/return controls reject.
1464
+ Use \`setAudioUserBus(world, 'music', { volume: 0.5, muted: false })\` for player settings;
1465
+ \`setBus\` remains the independent gameplay fader. Effect target fields
1466
+ are \`effectId/parameter\` (for example \`water/frequency\`), and send fields are \`send/returnBus\`.
1467
+ Zones use spheres or world-axis boxes and at most four smoothly handed-off returns. Obstruction
1468
+ comes from an injected bounded query provider; headless tests can supply deterministic queries.
1469
+ Optional \`gainCurve\` and \`lowpassCurve\` map transmission to dB and Hz.
1470
+
1471
+ Music programs use decoded stems, fixed tempo/meter, source sample markers and a 960-PPQ grid.
1472
+ Rate conform is explicit and pitch changing, bounded by authored cents. Prepare then start a
1473
+ program, request beat/bar/marker transitions, and set intensity parameters. Current, incoming and
1474
+ crossfading sections own PCM; retired sections are released. Entire stem groups reserve voices.
1475
+ Pause/seek cancel future sources, and resume creates fresh scheduled sources at the saved phase.
1476
+ Cancelling an already audible destination pauses it and releases both sides of its crossfade.
1477
+ Replacing a stinger reserves overlap capacity first; failure preserves the existing stinger.
1478
+ Persist only intentional sustained events/music; save restoration diagnoses changed revisions.
1479
+
1480
+ Use \`audio.describe\` and \`audio.log\` (cursor plus truncation metadata), \`audio.events.inspect\`,
1481
+ \`audio.mix.inspect\`, \`audio.music.inspect\`, and \`audio.meters\`. Headless gains are modeled; meters
1482
+ must say unavailable there. Gameplay tests expose \`game.audio.describe/events/music/mix/log\`.
1483
+ CLI: \`shard audio call audio.parameter.set --attach --params '{"instance":1,"name":"rpm","value":0.8}'\`.
1484
+ Without --attach, the CLI uses a temporary headless target. MCP exposes matching audio_* tools.
1485
+
1486
+ \`shard audio preview plans/showcase.json --out captures/mix.wav --json\` runs Chromium's offline
1487
+ renderer through the same event/music/mixer implementations. The preview schema accepts clips,
1488
+ authored events/music, seed, controls, movement, duration, rate and tails. Controls evaluate at
1489
+ 60 Hz aligned upward to 128-sample render quanta; the trace includes requested/actual times.
1490
+ Music timestamps are scheduled natively. Output includes WAV, decisions, peak/RMS/clipping,
1491
+ backend and capabilities. Limits: 60 seconds including tails and 64 MiB each for input/PCM/output.
1492
+ No audio device, microphone or GPU is needed. Install Playwright Chromium in the CLI package.
1493
+ See \`examples/audio-lab\` for a complete authored showcase and original synthetic assets.
1494
+ `;
1495
+ const VFX_GUIDE = `# Trails, beams and projected decals
1496
+
1497
+ "render/forward" includes ribbonsPlugin and decalsPlugin. SDK hosts can use forwardCorePlugin
1498
+ plus just the optional plugins they need. Components are schema-authored; use render.describe
1499
+ for ribbons/decals records, points/triangles, age, allocation sizes and structured error codes.
1500
+
1501
+ Trail follows its Transform (including parent/bone attachments) and local offset. Old points
1502
+ stay in world space. capacity bounds the history, lifetime expires points, minDistance rejects
1503
+ duplicates, and breakDistance clears history after teleporting. playing=false drains the trail;
1504
+ paused=true freezes its clock. Beam uses local start/end, or end relative to its target entity;
1505
+ local points override endpoints. segments/noise/seed control deterministic endpoint-beam shape.
1506
+ Both accept width, ordered widthCurve [{at,value}], color/endColor, opacity, repeat, scroll,
1507
+ blend alpha/additive, layers and a Texture handle. Width curves use trail age or beam distance.
1508
+ Colors are display-referred unlit overlays, not PBR lighting inputs. Alpha compositing uses
1509
+ stable creation order; intersecting transparent ribbons are not sorted per segment.
1510
+
1511
+ Decal projects along local -Z, with local X/Y as UVs and size as the clipping box. Mark eligible
1512
+ rigid Mesh3d entities with DecalReceiver; its mask must overlap receiverMask. The projector clips
1513
+ real triangles and caches them until geometry/transforms/masks change. maxAngle rejects grazing
1514
+ and back-facing surfaces; offset avoids z fighting. lifetime=0 persists; fadeOut fades the end
1515
+ of a finite lifetime. paused freezes age, playing=false hides it. maxVertices and a 32768 input
1516
+ triangle budget bound projection; overflow gives a diagnostic and no partial mark. Skinned,
1517
+ morph/compute-deformed and transparent receivers are unsupported. No PBR normal/roughness decals.
1518
+
1519
+ All texture handles resolve through the host AssetServer catalog (GUIDs work). No file picker,
1520
+ URL loading or database dependency is introduced. Prefabs carry reusable VFX component settings;
1521
+ 0081 visual tracks own activation and cleanup. A beam visual track's endpoint binds another
1522
+ named sequence target. Use property tracks for opacity/width changes. Effects are presentation;
1523
+ the host remains responsible for damage, collision and persistent game state.
1524
+
1525
+ Preview: playground/vfx.html. Toggle each feature to isolate its pixels; replay a seed, pause,
1526
+ clear, and switch perspective/orthographic cameras. The host registers the original textures.
1527
+ `;
1528
+ const EFFECTS_GUIDE = `# Coordinate reusable effect sequences
1529
+
1530
+ Enable "effects" in shard.json and the plugins used by your tracks: "animation", "particles",
1531
+ "audio/events" and "render/forward" for visual tracks. The scene plugin handles owned prefab targets. Author *.effect-sequence.json
1532
+ against .shard/schemas/effect-sequence.schema.json. Hosts may supply the same JSON directly;
1533
+ all dependencies resolve through the host's AssetServer catalog, including GUID-only assets.
1534
+
1535
+ A sequence has duration, parameters, named targets and tracks. Targets with a prefab handle
1536
+ are spawned/owned; other targets are borrowed entity bindings. Property tracks interpolate
1537
+ float/vector/color component fields (from/to arrays, linear or smoothstep, optional parameter
1538
+ multiplier). Animation and particle tracks require owned targets. Audio tracks use authored
1539
+ AudioEvent assets. Visual tracks require owned targets and name render/Trail, render/Beam or
1540
+ render/Decal in component. They activate playing, pause their clocks, and stop at track end;
1541
+ trails drain their remaining history. Beam tracks may bind an endpoint by target name.
1542
+ Marker tracks record presentation milestones; gameplay applies damage itself.
1543
+
1544
+ Use prepareEffect(world, source), then playEffect(world, preparation, { bindings, parameters,
1545
+ seed, parent, ownerEntity }). The optional parent anchors owned targets in a local frame, so
1546
+ one prepared spell can play at different positions/orientations. Removing the parent cancels
1547
+ the effect unless another ownerEntity was supplied. Release the preparation when no more plays are needed: active instances
1548
+ retain it. cancelEffect restores borrowed fields, stops audio and removes owned prefab trees.
1549
+ pauseEffect pauses only that instance. Keep claimed fields free of other writers while playing.
1550
+ No runtime file picker, URL fetcher or persistence backend is assumed.
1551
+
1552
+ Protocol/MCP: effects.prepare, effects.play, effects.pause, effects.cancel, effects.release,
1553
+ effects.inspect (trace cursor, active instances and preparation references). The inspect trace
1554
+ reports late windows skipped after a large frame step. A missing target fails and cleans up the
1555
+ whole instance. Calls return structured effects/* errors. Dispose the app to release all plans.
1556
+
1557
+ Preview at playground/effects.html; its spell asset and host catalog adapter demonstrate the
1558
+ complete flow. Replay by cancelling and playing again; backward scrubbing is not implemented.
1559
+ `;
1296
1560
  function generateDocs(manifest, errors) {
1297
1561
  const files = {
1562
+ ".agents/audio.md": AUDIO_GUIDE,
1563
+ ".agents/effects.md": EFFECTS_GUIDE,
1564
+ ".agents/vfx.md": VFX_GUIDE,
1298
1565
  ".agents/components.md": renderComponentCatalog(),
1299
1566
  ".agents/errors.md": renderErrorCatalog(errors),
1300
1567
  ".agents/generators.md": renderGeneratorCatalog(),
@@ -1309,6 +1576,11 @@ function generateDocs(manifest, errors) {
1309
1576
  ".shard/schemas/prefab.schema.json": `${JSON.stringify(prefabJsonSchema(), null, 2)}
1310
1577
  `,
1311
1578
  ".shard/schemas/save.schema.json": `${JSON.stringify(saveJsonSchema(), null, 2)}
1579
+ `,
1580
+ // Browser verification (0062): other renderers' pages validate their records against it.
1581
+ ".shard/schemas/perf-record.schema.json": `${JSON.stringify(perfRecordJsonSchema(), null, 2)}
1582
+ `,
1583
+ ".shard/schemas/capture-plan.schema.json": `${JSON.stringify(capturePlanJsonSchema(), null, 2)}
1312
1584
  `
1313
1585
  };
1314
1586
  for (const [name, content] of Object.entries(SKILLS))
package/dist/host.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { GpuContext } from '@aethervtt/shard-gpu';
1
+ import type { BakedTranslations, GpuContext } from '@aethervtt/shard-gpu';
2
2
  import type { AudioBackend, InputSource, Platform } from '@aethervtt/shard-platform';
3
3
  import { type RenderTarget } from '@aethervtt/shard-render';
4
4
  import { App, type Plugin } from '@aethervtt/shard-runtime';
@@ -12,6 +12,11 @@ export interface BuildAppOptions {
12
12
  canvas?: HTMLCanvasElement | OffscreenCanvas;
13
13
  /** Headless stand-in for the window (CLI screenshots). */
14
14
  target?: RenderTarget;
15
+ /**
16
+ * The project's baked WebGL2 translations (`shard shaders bake`), or their URL: on WebGL2, the
17
+ * shaders they cover load without naga (0064). WebGPU never reads them.
18
+ */
19
+ webgl2Shaders?: string | URL | BakedTranslations;
15
20
  inputSource?: InputSource;
16
21
  /** Where sound goes (`platform.audio`). Without one, audio records voices (headless). */
17
22
  audio?: AudioBackend;
package/dist/host.js CHANGED
@@ -1,7 +1,16 @@
1
1
  import { animationPlugin } from "@aethervtt/shard-animation";
2
- import { audioPlugin } from "@aethervtt/shard-audio";
2
+ import {
3
+ audioAcousticsPlugin,
4
+ audioEventsPlugin,
5
+ audioMixerPlugin,
6
+ audioMusicPlugin,
7
+ audioPlugin
8
+ } from "@aethervtt/shard-audio";
9
+ import { controlsPlugin } from "@aethervtt/shard-controls";
10
+ import { dicePlugin } from "@aethervtt/shard-dice";
11
+ import { effectsPlugin } from "@aethervtt/shard-effects";
3
12
  import { gltfPlugin } from "@aethervtt/shard-gltf";
4
- import { inputPlugin } from "@aethervtt/shard-input";
13
+ import { gesturesPlugin, inputPlugin } from "@aethervtt/shard-input";
5
14
  import { loadNavCache, Nav, navGridPlugin, navPlugin } from "@aethervtt/shard-nav";
6
15
  import { noisePlugin } from "@aethervtt/shard-noise";
7
16
  import { particlesPlugin } from "@aethervtt/shard-particles";
@@ -11,6 +20,7 @@ import { forwardPlugin, renderPlugin, Shaders } from "@aethervtt/shard-render";
11
20
  import { materialNoisePlugin } from "@aethervtt/shard-render/noise";
12
21
  import { App, LogResource } from "@aethervtt/shard-runtime";
13
22
  import { savePlugin } from "@aethervtt/shard-save";
23
+ import { scatterPlugin } from "@aethervtt/shard-scatter";
14
24
  import { loadScene, ScenePlugin } from "@aethervtt/shard-scene";
15
25
  import { spritePlugin } from "@aethervtt/shard-sprite";
16
26
  import { terrainPlugin } from "@aethervtt/shard-terrain";
@@ -20,20 +30,33 @@ import { uiPlugin } from "@aethervtt/shard-ui";
20
30
  function buildApp(options) {
21
31
  const { manifest } = options;
22
32
  const names = new Set(manifest.plugins);
23
- if (names.has("sprite") || names.has("text") || names.has("particles") || names.has("animation"))
33
+ if (names.has("sprite") || names.has("text") || names.has("particles") || names.has("animation") || names.has("dice"))
24
34
  names.add("render/forward");
35
+ if (names.has("controls")) {
36
+ names.add("gestures");
37
+ names.add("render/forward");
38
+ }
39
+ if (names.has("audio/acoustics")) names.add("audio/mixer");
40
+ if (names.has("audio/music") || names.has("audio/mixer")) names.add("audio/events");
41
+ if (names.has("audio/events")) names.add("audio");
42
+ if (names.has("gestures")) names.add("input");
25
43
  if (names.has("render/forward")) {
26
44
  names.add("render");
27
45
  names.add("core/transform");
28
46
  }
29
- if (names.has("physics3d") || names.has("physics2d") || names.has("audio") || names.has("ui") || names.has("nav") || names.has("nav/grid") || names.has("terrain"))
47
+ if (names.has("physics3d") || names.has("physics2d") || names.has("audio") || names.has("ui") || names.has("nav") || names.has("nav/grid") || names.has("terrain") || names.has("scatter"))
30
48
  names.add("core/transform");
31
49
  const app = new App({ seed: manifest.seed });
32
50
  app.addPlugin(gltfPlugin);
33
51
  if (names.has("core/transform")) app.addPlugin(TransformPlugin);
34
52
  if (names.has("render")) {
35
53
  app.addPlugin(
36
- renderPlugin({ gpu: options.gpu, canvas: options.canvas, target: options.target })
54
+ renderPlugin({
55
+ gpu: options.gpu,
56
+ canvas: options.canvas,
57
+ target: options.target,
58
+ webgl2: options.webgl2Shaders ? { shaders: options.webgl2Shaders } : void 0
59
+ })
37
60
  );
38
61
  }
39
62
  if (names.has("render/forward")) {
@@ -44,16 +67,26 @@ function buildApp(options) {
44
67
  if (names.has("text")) app.addPlugin(textPlugin);
45
68
  if (names.has("particles")) app.addPlugin(particlesPlugin);
46
69
  if (names.has("animation")) app.addPlugin(animationPlugin);
47
- if (names.has("physics3d")) app.addPlugin(physics3dPlugin);
48
- if (names.has("physics2d")) app.addPlugin(physics2dPlugin);
70
+ if (names.has("dice")) app.addPlugin(dicePlugin());
71
+ if (names.has("physics3d")) app.addPlugin(physics3dPlugin());
72
+ if (names.has("physics2d")) app.addPlugin(physics2dPlugin());
49
73
  if (names.has("input")) app.addPlugin(inputPlugin({ source: options.inputSource }));
74
+ if (names.has("gestures")) app.addPlugin(gesturesPlugin);
75
+ if (names.has("controls")) app.addPlugin(controlsPlugin);
50
76
  if (names.has("audio")) app.addPlugin(audioPlugin({ backend: options.audio }));
77
+ if (names.has("audio/events")) app.addPlugin(audioEventsPlugin());
78
+ if (names.has("audio/mixer")) app.addPlugin(audioMixerPlugin());
79
+ if (names.has("audio/acoustics")) app.addPlugin(audioAcousticsPlugin());
80
+ if (names.has("audio/music")) app.addPlugin(audioMusicPlugin());
51
81
  if (names.has("ui")) app.addPlugin(uiPlugin);
52
82
  if (names.has("nav")) app.addPlugin(navPlugin);
53
83
  else if (names.has("nav/grid")) app.addPlugin(navGridPlugin);
54
- if (names.has("terrain")) app.addPlugin(terrainPlugin({ workers: options.platform?.workers }));
84
+ if (names.has("terrain"))
85
+ app.addPlugin(terrainPlugin({ workers: options.platform?.workers, fs: options.platform?.fs }));
86
+ if (names.has("scatter")) app.addPlugin(scatterPlugin({ workers: options.platform?.workers }));
55
87
  app.addPlugin(noisePlugin);
56
88
  app.addPlugin(ScenePlugin);
89
+ if (names.has("effects")) app.addPlugin(effectsPlugin);
57
90
  app.addPlugin(
58
91
  procgenPlugin({
59
92
  fs: options.platform?.fs,
@@ -18,6 +18,9 @@ export declare const Manifest: import("@aethervtt/shard-core").ComponentDef<{
18
18
  readonly procgen: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
19
19
  cacheSize: import("@aethervtt/shard-core").FieldType<number, "u32">;
20
20
  }>, "object">;
21
+ readonly graphics: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
22
+ baseline: import("@aethervtt/shard-core").FieldType<"optional" | "required", "u8">;
23
+ }>, "object">;
21
24
  readonly plugins: import("@aethervtt/shard-core").FieldType<string[], "object">;
22
25
  }>;
23
26
  export type ManifestValue = Infer<typeof Manifest>;
@@ -25,6 +28,7 @@ export type ManifestValue = Infer<typeof Manifest>;
25
28
  export declare function validateManifest(json: unknown): ShardError[];
26
29
  /** Built-in engine plugins a manifest can name, with what they bring. */
27
30
  export declare const BUILTIN_PLUGINS: {
31
+ readonly effects: 'Prepared effect sequences: coordinated properties, animation, particles, audio and markers; add the feature plugins used by tracks';
28
32
  readonly render: 'GPU device, render graph, views, shaders';
29
33
  readonly 'render/forward': 'cameras, meshes, standard material, lights (includes render and core/transform)';
30
34
  readonly sprite: 'sprites, atlases, frame animation, tilemaps (includes render/forward)';
@@ -34,11 +38,19 @@ export declare const BUILTIN_PLUGINS: {
34
38
  readonly physics3d: 'Rapier 3D rigid bodies, colliders, joints, raycasts (includes core/transform)';
35
39
  readonly physics2d: 'Rapier 2D physics in the XY plane (includes core/transform)';
36
40
  readonly input: 'keyboard, mouse, gamepad, touch, action maps';
41
+ readonly gestures: 'taps, long presses, drags, pinches, twists and the wheel as Gesture events, with claims and Escape to cancel (includes input)';
42
+ readonly controls: 'OrbitControls and MapControls on cameras, syncViews, PlaneDrag to drag entities along a plane, controls.describe (includes gestures, render/forward)';
37
43
  readonly audio: 'AudioSource, AudioListener, playSound, buses and ducking, spatial audio, voice limits (includes core/transform)';
44
+ readonly 'audio/events': 'Authored layered events, parameters, collections and prepared voice lifetimes (includes audio)';
45
+ readonly 'audio/mixer': 'Hierarchical effects, sends and composable snapshots (includes audio/events)';
46
+ readonly 'audio/acoustics': 'Listener reverb zones and bounded injected obstruction queries (includes audio/mixer)';
47
+ readonly 'audio/music': 'Prepared stems, beat/bar/marker transitions and stingers (includes audio/events)';
38
48
  readonly ui: 'UiNode trees under a UiRoot: flexbox layout, text, images, buttons, sliders, text fields, world-anchored markers, themes (includes core/transform)';
39
49
  readonly nav: 'navigation: NavGrid A* (2D), NavMesh baked by Recast from NavSource geometry, OffMeshLink, path queries, NavAgent steering (includes core/transform)';
40
50
  readonly 'nav/grid': 'navigation on NavGrids only, without loading the Recast WASM (includes core/transform)';
41
51
  readonly terrain: 'planets: Planet cube-sphere terrain from noise graphs with LOD, geomorphing, oceans, biomes, collider chunks around characters, navmeshes on the surface; terrain.* methods (includes core/transform; add render/forward to draw it, physics3d for colliders)';
52
+ readonly scatter: 'props and foliage from ScatterSet rules (*.scatter.json) on planets (Planet.scatter, Biome.scatter) and ScatterSurface meshes: props as entities around cameras and anchors, GPU foliage near cameras; scatter.* methods; the engine generators shard/Rock, Tree, Bush, GrassClump and Crystal make their meshes (includes core/transform; add terrain for planets, render/forward to draw)';
53
+ readonly dice: 'the dice table (0054): dice skins, face layouts and effect recipes as data, dice.play and dice.describe, dice/RollRequest in scenes; tracks record on the calling thread (includes render/forward)';
42
54
  readonly 'core/transform': 'Transform and hierarchy propagation';
43
55
  };
44
56
  export declare function manifestJsonSchema(): JsonSchema;
package/dist/manifest.js CHANGED
@@ -39,9 +39,18 @@ const Manifest = defineSchema(
39
39
  },
40
40
  { description: "Generators (0042): output cache limits." }
41
41
  ),
42
+ graphics: t.struct(
43
+ {
44
+ baseline: t.enum(["optional", "required"], {
45
+ default: "optional",
46
+ description: "required: the project must run at the baseline tier (WebGPU compatibility mode, WebGL2), and shard validate checks it. optional: what the tier can't run is reported when it runs."
47
+ })
48
+ },
49
+ { description: "Graphics tiers (0064)." }
50
+ ),
42
51
  plugins: t.list(t.string, {
43
52
  default: ["render/forward", "input"],
44
- description: "Engine plugins to enable, by name: render, render/forward, sprite, text, particles, animation, physics3d, physics2d, input, audio, ui, nav, nav/grid, core/transform."
53
+ description: "Engine plugins to enable, by name: render, render/forward, sprite, text, particles, animation, physics3d, physics2d, input, gestures, controls, audio, ui, nav, nav/grid, terrain, scatter, dice, core/transform."
45
54
  })
46
55
  },
47
56
  { description: "shard.json: the project manifest." }
@@ -80,6 +89,7 @@ function validateManifest(json) {
80
89
  return errors;
81
90
  }
82
91
  const BUILTIN_PLUGINS = {
92
+ effects: "Prepared effect sequences: coordinated properties, animation, particles, audio and markers; add the feature plugins used by tracks",
83
93
  render: "GPU device, render graph, views, shaders",
84
94
  "render/forward": "cameras, meshes, standard material, lights (includes render and core/transform)",
85
95
  sprite: "sprites, atlases, frame animation, tilemaps (includes render/forward)",
@@ -89,11 +99,19 @@ const BUILTIN_PLUGINS = {
89
99
  physics3d: "Rapier 3D rigid bodies, colliders, joints, raycasts (includes core/transform)",
90
100
  physics2d: "Rapier 2D physics in the XY plane (includes core/transform)",
91
101
  input: "keyboard, mouse, gamepad, touch, action maps",
102
+ gestures: "taps, long presses, drags, pinches, twists and the wheel as Gesture events, with claims and Escape to cancel (includes input)",
103
+ controls: "OrbitControls and MapControls on cameras, syncViews, PlaneDrag to drag entities along a plane, controls.describe (includes gestures, render/forward)",
92
104
  audio: "AudioSource, AudioListener, playSound, buses and ducking, spatial audio, voice limits (includes core/transform)",
105
+ "audio/events": "Authored layered events, parameters, collections and prepared voice lifetimes (includes audio)",
106
+ "audio/mixer": "Hierarchical effects, sends and composable snapshots (includes audio/events)",
107
+ "audio/acoustics": "Listener reverb zones and bounded injected obstruction queries (includes audio/mixer)",
108
+ "audio/music": "Prepared stems, beat/bar/marker transitions and stingers (includes audio/events)",
93
109
  ui: "UiNode trees under a UiRoot: flexbox layout, text, images, buttons, sliders, text fields, world-anchored markers, themes (includes core/transform)",
94
110
  nav: "navigation: NavGrid A* (2D), NavMesh baked by Recast from NavSource geometry, OffMeshLink, path queries, NavAgent steering (includes core/transform)",
95
111
  "nav/grid": "navigation on NavGrids only, without loading the Recast WASM (includes core/transform)",
96
112
  terrain: "planets: Planet cube-sphere terrain from noise graphs with LOD, geomorphing, oceans, biomes, collider chunks around characters, navmeshes on the surface; terrain.* methods (includes core/transform; add render/forward to draw it, physics3d for colliders)",
113
+ scatter: "props and foliage from ScatterSet rules (*.scatter.json) on planets (Planet.scatter, Biome.scatter) and ScatterSurface meshes: props as entities around cameras and anchors, GPU foliage near cameras; scatter.* methods; the engine generators shard/Rock, Tree, Bush, GrassClump and Crystal make their meshes (includes core/transform; add terrain for planets, render/forward to draw)",
114
+ dice: "the dice table (0054): dice skins, face layouts and effect recipes as data, dice.play and dice.describe, dice/RollRequest in scenes; tracks record on the calling thread (includes render/forward)",
97
115
  "core/transform": "Transform and hierarchy propagation"
98
116
  };
99
117
  function manifestJsonSchema() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aethervtt/shard-project",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -8,6 +8,10 @@
8
8
  ".": {
9
9
  "types": "./dist/index.d.ts",
10
10
  "default": "./dist/index.js"
11
+ },
12
+ "./manifest": {
13
+ "types": "./dist/manifest.d.ts",
14
+ "default": "./dist/manifest.js"
11
15
  }
12
16
  },
13
17
  "files": [
@@ -16,29 +20,35 @@
16
20
  "README.md"
17
21
  ],
18
22
  "dependencies": {
19
- "@aethervtt/shard-animation": "0.0.1",
20
- "@aethervtt/shard-assets": "0.0.1",
21
- "@aethervtt/shard-audio": "0.0.1",
22
- "@aethervtt/shard-core": "0.0.1",
23
- "@aethervtt/shard-gltf": "0.0.1",
24
- "@aethervtt/shard-gpu": "0.0.1",
25
- "@aethervtt/shard-input": "0.0.1",
26
- "@aethervtt/shard-nav": "0.0.1",
27
- "@aethervtt/shard-noise": "0.0.1",
28
- "@aethervtt/shard-particles": "0.0.1",
29
- "@aethervtt/shard-physics": "0.0.1",
30
- "@aethervtt/shard-platform": "0.0.1",
31
- "@aethervtt/shard-procgen": "0.0.1",
32
- "@aethervtt/shard-protocol": "0.0.1",
33
- "@aethervtt/shard-render": "0.0.1",
34
- "@aethervtt/shard-runtime": "0.0.1",
35
- "@aethervtt/shard-save": "0.0.1",
36
- "@aethervtt/shard-scene": "0.0.1",
37
- "@aethervtt/shard-sprite": "0.0.1",
38
- "@aethervtt/shard-terrain": "0.0.1",
39
- "@aethervtt/shard-text": "0.0.1",
40
- "@aethervtt/shard-transform": "0.0.1",
41
- "@aethervtt/shard-ui": "0.0.1"
23
+ "@aethervtt/shard-animation": "0.2.0",
24
+ "@aethervtt/shard-assets": "0.2.0",
25
+ "@aethervtt/shard-audio": "0.2.0",
26
+ "@aethervtt/shard-controls": "0.2.0",
27
+ "@aethervtt/shard-core": "0.2.0",
28
+ "@aethervtt/shard-dice": "0.2.0",
29
+ "@aethervtt/shard-fog": "0.2.0",
30
+ "@aethervtt/shard-gltf": "0.2.0",
31
+ "@aethervtt/shard-gpu": "0.2.0",
32
+ "@aethervtt/shard-input": "0.2.0",
33
+ "@aethervtt/shard-nav": "0.2.0",
34
+ "@aethervtt/shard-noise": "0.2.0",
35
+ "@aethervtt/shard-particles": "0.2.0",
36
+ "@aethervtt/shard-physics": "0.2.0",
37
+ "@aethervtt/shard-platform": "0.2.0",
38
+ "@aethervtt/shard-procgen": "0.2.0",
39
+ "@aethervtt/shard-protocol": "0.2.0",
40
+ "@aethervtt/shard-render": "0.2.0",
41
+ "@aethervtt/shard-runtime": "0.2.0",
42
+ "@aethervtt/shard-save": "0.2.0",
43
+ "@aethervtt/shard-scatter": "0.2.0",
44
+ "@aethervtt/shard-scene": "0.2.0",
45
+ "@aethervtt/shard-sprite": "0.2.0",
46
+ "@aethervtt/shard-terrain": "0.2.0",
47
+ "@aethervtt/shard-text": "0.2.0",
48
+ "@aethervtt/shard-transform": "0.2.0",
49
+ "@aethervtt/shard-ui": "0.2.0",
50
+ "@aethervtt/shard-verify": "0.2.0",
51
+ "@aethervtt/shard-effects": "0.2.0"
42
52
  },
43
53
  "repository": {
44
54
  "type": "git",
@@ -50,6 +60,6 @@
50
60
  "provenance": true
51
61
  },
52
62
  "shard": {
53
- "buildSha": "d6c5c6aef4675575c878b02cf2c8f738b4190229"
63
+ "buildSha": "32c22a3944ed23f5d1baa90bf7cc9fae724f5fd8"
54
64
  }
55
65
  }