@aethervtt/shard-dice 0.0.0-stage → 0.1.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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -2
  3. package/dist/attachments.d.ts +79 -0
  4. package/dist/attachments.js +43 -0
  5. package/dist/builtins.d.ts +25 -0
  6. package/dist/builtins.js +335 -0
  7. package/dist/cells.d.ts +33 -0
  8. package/dist/cells.js +53 -0
  9. package/dist/components.d.ts +14 -0
  10. package/dist/components.js +27 -0
  11. package/dist/definition-asset.d.ts +19 -0
  12. package/dist/definition-asset.js +60 -0
  13. package/dist/definition.d.ts +98 -0
  14. package/dist/definition.js +320 -0
  15. package/dist/dice-worker.d.ts +1 -0
  16. package/dist/dice-worker.js +3 -0
  17. package/dist/effects.d.ts +97 -0
  18. package/dist/effects.js +196 -0
  19. package/dist/entrances.d.ts +48 -0
  20. package/dist/entrances.js +54 -0
  21. package/dist/families.d.ts +17 -0
  22. package/dist/families.js +131 -0
  23. package/dist/glyphs.d.ts +29 -0
  24. package/dist/glyphs.js +270 -0
  25. package/dist/golden.json +17 -0
  26. package/dist/hull.d.ts +32 -0
  27. package/dist/hull.js +154 -0
  28. package/dist/index.d.ts +31 -0
  29. package/dist/index.js +322 -0
  30. package/dist/landing.d.ts +29 -0
  31. package/dist/landing.js +93 -0
  32. package/dist/layout.d.ts +75 -0
  33. package/dist/layout.js +137 -0
  34. package/dist/marks.d.ts +54 -0
  35. package/dist/marks.js +543 -0
  36. package/dist/material.d.ts +80 -0
  37. package/dist/material.js +343 -0
  38. package/dist/math.d.ts +39 -0
  39. package/dist/math.js +136 -0
  40. package/dist/mesh.d.ts +9 -0
  41. package/dist/mesh.js +105 -0
  42. package/dist/methods.d.ts +2 -0
  43. package/dist/methods.js +42 -0
  44. package/dist/numerals.d.ts +9 -0
  45. package/dist/numerals.js +47 -0
  46. package/dist/plugin.d.ts +41 -0
  47. package/dist/plugin.js +174 -0
  48. package/dist/preview.d.ts +32 -0
  49. package/dist/preview.js +123 -0
  50. package/dist/quality.d.ts +12 -0
  51. package/dist/quality.js +18 -0
  52. package/dist/resources.d.ts +85 -0
  53. package/dist/resources.js +191 -0
  54. package/dist/roll.d.ts +68 -0
  55. package/dist/roll.js +97 -0
  56. package/dist/settle.d.ts +53 -0
  57. package/dist/settle.js +96 -0
  58. package/dist/skin.d.ts +61 -0
  59. package/dist/skin.js +303 -0
  60. package/dist/sound.d.ts +33 -0
  61. package/dist/sound.js +296 -0
  62. package/dist/studio.d.ts +3 -0
  63. package/dist/studio.js +48 -0
  64. package/dist/table.d.ts +225 -0
  65. package/dist/table.js +1757 -0
  66. package/dist/test-worker.d.ts +1 -0
  67. package/dist/test-worker.js +12 -0
  68. package/dist/track.d.ts +70 -0
  69. package/dist/track.js +304 -0
  70. package/dist/tray.d.ts +2 -0
  71. package/dist/tray.js +43 -0
  72. package/dist/variants.d.ts +9 -0
  73. package/dist/variants.js +46 -0
  74. package/dist/windows.d.ts +25 -0
  75. package/dist/windows.js +77 -0
  76. package/dist/worker.d.ts +6 -0
  77. package/dist/worker.js +9 -0
  78. package/package.json +47 -4
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { parentPort } from "node:worker_threads";
2
+ import { serveTracks } from "@aethervtt/shard-physics/worker";
3
+ import { DICE_SETTLE_RULE, diceSettle } from "./settle.js";
4
+ const port = parentPort;
5
+ serveTracks({
6
+ rules: { [DICE_SETTLE_RULE]: diceSettle },
7
+ scope: {
8
+ postMessage: (message, transfer) => port.postMessage(message, transfer),
9
+ addEventListener: (_type, listener) => port.on("message", (data) => listener({ data })),
10
+ close: () => port.close()
11
+ }
12
+ });
@@ -0,0 +1,70 @@
1
+ import type { Track, TrackContactOptions, TrackScene } from '@aethervtt/shard-physics/track';
2
+ import { type DieGeometry } from './definition.js';
3
+ import { type Q4, type V3 } from './math.js';
4
+ import { type DiceSettleParams, type DiceTray } from './settle.js';
5
+ export declare const LAUNCH_EDGES: readonly ['left', 'right', 'top', 'bottom'];
6
+ export type LaunchEdge = (typeof LAUNCH_EDGES)[number];
7
+ export interface DiceTrackDie {
8
+ /** A registered definition's id. */
9
+ definition: string;
10
+ /** World scale of the unit-radius die. */
11
+ scale: number;
12
+ /** Dropped dice are heavier on friction and lighter on bounce. */
13
+ dropped?: boolean;
14
+ }
15
+ export interface DiceTrackRequest {
16
+ /** Every random choice (edge, lanes, velocities, spin, starting orientations) derives from it. */
17
+ seed: string | number;
18
+ tray: DiceTray;
19
+ dice: DiceTrackDie[];
20
+ /** The tray edge the dice come in from; chosen from the seed when omitted. */
21
+ launch?: LaunchEdge;
22
+ }
23
+ export declare const DICE_STEP: number;
24
+ export declare const DICE_MAX_STEPS = 480;
25
+ /** The steps a placed die takes to drop into its spot. */
26
+ export declare const PLACED_DROP_STEPS = 20;
27
+ export declare const DICE_GRAVITY = -18.5;
28
+ /** Contacts as the impact synth hears them. */
29
+ export declare const DICE_CONTACTS: TrackContactOptions;
30
+ export declare const MAX_DICE = 32;
31
+ export declare const seedOf: (seed: string | number) => number;
32
+ /** The launch edge a request's seed picks (or the one it names). */
33
+ export declare function launchEdgeOf(request: DiceTrackRequest): LaunchEdge;
34
+ /**
35
+ * The 0053 scene of a throw: a walled tray, and the dice thrown in from one edge in lanes. Every
36
+ * number comes from the seed; the definitions' geometry and scales shape the colliders, so the
37
+ * scene hash covers them and the tray too.
38
+ */
39
+ export declare function diceTrackScene(request: DiceTrackRequest): TrackScene;
40
+ /** The data `dice-settle` needs about a request's dice. */
41
+ export declare function diceSettleParams(request: DiceTrackRequest): DiceSettleParams;
42
+ export type UnlandedReason = 'outside' | 'cocked';
43
+ /**
44
+ * Per die, why the track can't show it landed: it ended outside the tray, or not resting flat
45
+ * (a track that hit maxSteps). Null for a die that landed.
46
+ */
47
+ export declare function unlandedDice(track: Track, request: DiceTrackRequest): (UnlandedReason | null)[];
48
+ /**
49
+ * Where reduced motion puts each die: rows centered in the tray, in roll order, left to right and
50
+ * far to near.
51
+ */
52
+ export declare function laneLayout(request: DiceTrackRequest): V3[];
53
+ /**
54
+ * Free spots for the dice the track can't show: for each, the grid spot nearest to where it
55
+ * ended (clamped into the tray) that no landed or already placed die is near.
56
+ */
57
+ export declare function placementSpots(track: Track, request: DiceTrackRequest, unlanded: readonly (UnlandedReason | null)[]): (V3 | null)[];
58
+ /**
59
+ * Where dice that aren't in the physics land (0065's entrance dice): for each wanted point (tray
60
+ * coordinates), the grid spot nearest it that none of `taken` (where the physical dice ended) and no
61
+ * earlier entrance die is near. `dice` are every die on the tray, for the spacing.
62
+ */
63
+ export declare function entranceSpots(tray: DiceTray, dice: readonly DiceTrackDie[], taken: readonly V3[], wants: readonly V3[]): V3[];
64
+ /**
65
+ * Rewrites a die's last steps so it drops straight down into `spot`, resting at `rotation`: the
66
+ * placed fallback, honest and deterministic. Earlier steps are the physics as recorded.
67
+ */
68
+ export declare function placeDie(track: Track, body: number, spot: V3, rotation: Q4, geometry: DieGeometry, scale: number): void;
69
+ /** The step a placed die starts dropping at. */
70
+ export declare const placedFrom: (track: Track) => number;
package/dist/track.js ADDED
@@ -0,0 +1,304 @@
1
+ import { quat as quatMath, Rng, ShardError } from "@aethervtt/shard-core";
2
+ import { dieGeometry, requireDie } from "./definition.js";
3
+ import { restingHeight } from "./landing.js";
4
+ import { hashString } from "./math.js";
5
+ import {
6
+ BALL_DAMPING,
7
+ DICE_GROUPS,
8
+ outsideTray,
9
+ restingFlat
10
+ } from "./settle.js";
11
+ const LAUNCH_EDGES = ["left", "right", "top", "bottom"];
12
+ const DICE_STEP = 1 / 60;
13
+ const DICE_MAX_STEPS = 480;
14
+ const PLACED_DROP_STEPS = 20;
15
+ const DICE_GRAVITY = -18.5;
16
+ const DICE_CONTACTS = { minForce: 0.24, dedupeSteps: 3, max: 1024 };
17
+ const MAX_DICE = 32;
18
+ function checkRequest(request) {
19
+ const { tray: tray2, dice } = request;
20
+ if (!(tray2.halfWidth >= 1) || !(tray2.halfDepth >= 1)) {
21
+ throw new ShardError("dice/invalid-roll", "The tray is smaller than 1 \xD7 1 half-extents", {
22
+ path: "tray",
23
+ hint: "Pass the tray the camera shows, e.g. { halfWidth: 5.4, halfDepth: 3.15 }."
24
+ });
25
+ }
26
+ if (dice.length < 1 || dice.length > MAX_DICE) {
27
+ throw new ShardError("dice/invalid-roll", `A roll has 1 to ${MAX_DICE} physical dice`, {
28
+ path: "dice"
29
+ });
30
+ }
31
+ dice.forEach((d, i) => {
32
+ if (!(d.scale > 0.05 && d.scale < 4))
33
+ throw new ShardError("dice/invalid-roll", `dice[${i}].scale is out of range`, {
34
+ path: `dice[${i}].scale`
35
+ });
36
+ });
37
+ }
38
+ const seedOf = (seed) => typeof seed === "number" ? seed >>> 0 : hashString(seed);
39
+ const signed = (rng) => rng.float() * 2 - 1;
40
+ function randomRotation(rng) {
41
+ for (; ; ) {
42
+ const x = signed(rng);
43
+ const y = signed(rng);
44
+ const z = signed(rng);
45
+ const w = signed(rng);
46
+ const d = x * x + y * y + z * z + w * w;
47
+ if (d > 1 || d < 0.04) continue;
48
+ const l = Math.sqrt(d);
49
+ return [x / l, y / l, z / l, w / l];
50
+ }
51
+ }
52
+ function spawnFor(index, count, tray2, edge, rng) {
53
+ const alongX = edge === "left" || edge === "right";
54
+ const normalHalf = alongX ? tray2.halfWidth : tray2.halfDepth;
55
+ const tangentHalf = alongX ? tray2.halfDepth : tray2.halfWidth;
56
+ const spacing = count > 16 ? 0.54 : 0.72;
57
+ const capacity = Math.max(2, Math.floor(tangentHalf * 2 * 0.76 / spacing));
58
+ const columns = Math.min(count, capacity);
59
+ const column = index % columns;
60
+ const row = Math.floor(index / columns);
61
+ const sign = edge === "left" || edge === "top" ? -1 : 1;
62
+ const span = Math.min(tangentHalf * 1.48, Math.max(0, columns - 1) * spacing);
63
+ const tangent = columns === 1 ? signed(rng) * 0.18 : -span / 2 + column / (columns - 1) * span;
64
+ const normal = sign * (normalHalf - 0.28 - row * 0.34) + signed(rng) * 0.08;
65
+ const inward = alongX ? 5.8 + rng.float() * 1.7 : 4.25 + rng.float() * 1.45;
66
+ const across = -tangent * (0.34 + rng.float() * 0.16) + signed(rng) * 1.35;
67
+ const y = 1.55 + row * 0.18 + rng.float() * 0.52;
68
+ const vy = 1.65 + rng.float() * 1.7;
69
+ return {
70
+ position: alongX ? [normal, y, tangent] : [tangent, y, normal],
71
+ velocity: alongX ? [-sign * inward, vy, across] : [across, vy, -sign * inward]
72
+ };
73
+ }
74
+ function tray(t) {
75
+ const surface = { friction: 0.62, restitution: 0.24, density: 1 };
76
+ const wall = (x, z, hx, hz) => ({
77
+ shape: "cuboid",
78
+ halfExtents: [hx, 1.35, hz],
79
+ translation: [x, 1.16, z],
80
+ ...surface,
81
+ group: "walls"
82
+ });
83
+ const { halfWidth: hw, halfDepth: hd } = t;
84
+ return [
85
+ {
86
+ shape: "cuboid",
87
+ halfExtents: [hw, 0.16, hd],
88
+ translation: [0, -0.16, 0],
89
+ ...surface,
90
+ group: "floor"
91
+ },
92
+ wall(-hw - 0.12, 0, 0.16, hd),
93
+ wall(hw + 0.12, 0, 0.16, hd),
94
+ wall(0, -hd - 0.12, hw, 0.16),
95
+ wall(0, hd + 0.12, hw, 0.16)
96
+ ];
97
+ }
98
+ function colliderOf(g, scale, dropped) {
99
+ const surface = {
100
+ density: dropped ? 0.82 : 1.1,
101
+ friction: dropped ? 0.72 : 0.54,
102
+ restitution: dropped ? 0.18 : 0.34,
103
+ group: "dice"
104
+ };
105
+ if (g.definition.collider === "ball") return { shape: "ball", radius: scale, ...surface };
106
+ const points = new Float32Array(g.points.length * 3);
107
+ g.points.forEach((p, i) => {
108
+ points[i * 3] = p[0] * scale;
109
+ points[i * 3 + 1] = p[1] * scale;
110
+ points[i * 3 + 2] = p[2] * scale;
111
+ });
112
+ return { shape: "convex", points, ...surface };
113
+ }
114
+ function launchEdgeOf(request) {
115
+ return request.launch ?? new Rng(seedOf(request.seed)).pick(LAUNCH_EDGES);
116
+ }
117
+ function diceTrackScene(request) {
118
+ checkRequest(request);
119
+ const rng = new Rng(seedOf(request.seed));
120
+ const picked = rng.pick(LAUNCH_EDGES);
121
+ const edge = request.launch ?? picked;
122
+ const bodies = request.dice.map((die, i) => {
123
+ const g = dieGeometry(requireDie(die.definition));
124
+ const spawn = spawnFor(i, request.dice.length, request.tray, edge, rng);
125
+ const angular = [
126
+ signed(rng) * (8.5 + rng.float() * 10),
127
+ signed(rng) * (7 + rng.float() * 11),
128
+ signed(rng) * (8.5 + rng.float() * 10)
129
+ ];
130
+ return {
131
+ id: `${i}:${die.definition}`,
132
+ translation: spawn.position,
133
+ rotation: randomRotation(rng),
134
+ linear: spawn.velocity,
135
+ angular,
136
+ colliders: [colliderOf(g, die.scale, die.dropped ?? false)],
137
+ ccd: true,
138
+ linearDamping: 0.16,
139
+ angularDamping: g.definition.collider === "ball" ? BALL_DAMPING.from : 0.2
140
+ };
141
+ });
142
+ return {
143
+ version: 1,
144
+ dim: 3,
145
+ step: DICE_STEP,
146
+ maxSteps: DICE_MAX_STEPS,
147
+ gravity: [0, DICE_GRAVITY, 0],
148
+ groups: DICE_GROUPS,
149
+ fixed: tray(request.tray),
150
+ bodies
151
+ };
152
+ }
153
+ function diceSettleParams(request) {
154
+ return {
155
+ tray: { ...request.tray },
156
+ bodies: request.dice.map((die) => {
157
+ const g = dieGeometry(requireDie(die.definition));
158
+ if (g.definition.collider === "ball") return { directions: [], radius: die.scale };
159
+ return {
160
+ directions: g.frames.flatMap((f) => f.normal),
161
+ points: g.points.flatMap((p) => [p[0] * die.scale, p[1] * die.scale, p[2] * die.scale])
162
+ };
163
+ })
164
+ };
165
+ }
166
+ function unlandedDice(track, request) {
167
+ const params = diceSettleParams(request);
168
+ const n = track.bodyCount;
169
+ const o = track.steps * n;
170
+ return request.dice.map((_, i) => {
171
+ const p = track.positions.subarray((o + i) * 3, (o + i) * 3 + 3);
172
+ const r = track.rotations.subarray((o + i) * 4, (o + i) * 4 + 4);
173
+ if (outsideTray(p, request.tray)) return "outside";
174
+ return restingFlat(params.bodies[i], p, r) ? null : "cocked";
175
+ });
176
+ }
177
+ function laneSpacing(request) {
178
+ let widest = 0;
179
+ for (const d of request.dice) {
180
+ const g = dieGeometry(requireDie(d.definition));
181
+ widest = Math.max(widest, g.footprint * d.scale);
182
+ }
183
+ return widest * 1.18;
184
+ }
185
+ function gridSpots(t, spacing, margin) {
186
+ const spots = [];
187
+ const nx = Math.max(1, Math.floor(2 * (t.halfWidth - margin) / spacing) + 1);
188
+ const nz = Math.max(1, Math.floor(2 * (t.halfDepth - margin) / spacing) + 1);
189
+ for (let iz = 0; iz < nz; iz++) {
190
+ for (let ix = 0; ix < nx; ix++) {
191
+ spots.push([(ix - (nx - 1) / 2) * spacing, 0, (iz - (nz - 1) / 2) * spacing]);
192
+ }
193
+ }
194
+ return spots;
195
+ }
196
+ function laneLayout(request) {
197
+ const spacing = laneSpacing(request);
198
+ const n = request.dice.length;
199
+ const maxColumns = Math.max(1, Math.floor((2 * request.tray.halfWidth - spacing) / spacing) + 1);
200
+ const columns = Math.min(n, maxColumns, Math.max(1, Math.ceil(Math.sqrt(n * 2.2))));
201
+ const rows = Math.ceil(n / columns);
202
+ return request.dice.map((_, i) => {
203
+ const row = Math.floor(i / columns);
204
+ const inRow = row === rows - 1 ? n - row * columns : columns;
205
+ const col = i % columns;
206
+ return [(col - (inRow - 1) / 2) * spacing, 0, (row - (rows - 1) / 2) * spacing];
207
+ });
208
+ }
209
+ function placementSpots(track, request, unlanded) {
210
+ const spacing = laneSpacing(request);
211
+ const t = request.tray;
212
+ const margin = spacing * 0.6;
213
+ const spots = gridSpots(t, spacing, margin);
214
+ const o = track.steps * track.bodyCount;
215
+ const taken = [];
216
+ unlanded.forEach((reason, i) => {
217
+ if (reason) return;
218
+ const p = track.positions;
219
+ taken.push([p[(o + i) * 3], 0, p[(o + i) * 3 + 2]]);
220
+ });
221
+ return unlanded.map((reason, i) => {
222
+ if (!reason) return null;
223
+ const p = track.positions;
224
+ const want = [
225
+ Math.max(-t.halfWidth + margin, Math.min(t.halfWidth - margin, p[(o + i) * 3])),
226
+ 0,
227
+ Math.max(-t.halfDepth + margin, Math.min(t.halfDepth - margin, p[(o + i) * 3 + 2]))
228
+ ];
229
+ const free = spots.filter(
230
+ (s) => taken.every((q) => (s[0] - q[0]) ** 2 + (s[2] - q[2]) ** 2 >= spacing ** 2 * 0.81)
231
+ ).sort(
232
+ (a, b) => (a[0] - want[0]) ** 2 + (a[2] - want[2]) ** 2 - ((b[0] - want[0]) ** 2 + (b[2] - want[2]) ** 2)
233
+ );
234
+ const spot = free[0] ?? want;
235
+ taken.push(spot);
236
+ return spot;
237
+ });
238
+ }
239
+ function entranceSpots(tray2, dice, taken, wants) {
240
+ const spacing = laneSpacing({ dice });
241
+ const margin = spacing * 0.6;
242
+ const spots = gridSpots(tray2, spacing, margin);
243
+ const occupied = taken.slice();
244
+ return wants.map((w) => {
245
+ const want = [
246
+ Math.max(-tray2.halfWidth + margin, Math.min(tray2.halfWidth - margin, w[0])),
247
+ 0,
248
+ Math.max(-tray2.halfDepth + margin, Math.min(tray2.halfDepth - margin, w[2]))
249
+ ];
250
+ let best;
251
+ let bestD = Number.POSITIVE_INFINITY;
252
+ for (const s of spots) {
253
+ if (occupied.some((q) => (s[0] - q[0]) ** 2 + (s[2] - q[2]) ** 2 < spacing ** 2 * 0.81))
254
+ continue;
255
+ const d = (s[0] - want[0]) ** 2 + (s[2] - want[2]) ** 2;
256
+ if (d < bestD) {
257
+ bestD = d;
258
+ best = s;
259
+ }
260
+ }
261
+ const spot = best ?? want;
262
+ occupied.push(spot);
263
+ return spot;
264
+ });
265
+ }
266
+ function placeDie(track, body, spot, rotation, geometry, scale) {
267
+ const n = track.bodyCount;
268
+ const rest = restingHeight(geometry, rotation, scale);
269
+ const from = Math.max(0, track.steps - PLACED_DROP_STEPS);
270
+ const drop = 1.4;
271
+ const q = quatMath.normalize([0, 0, 0, 1], rotation);
272
+ for (let s = from; s <= track.steps; s++) {
273
+ const t = track.steps === from ? 1 : (s - from) / (track.steps - from);
274
+ const o3 = (s * n + body) * 3;
275
+ const o4 = (s * n + body) * 4;
276
+ track.positions[o3] = spot[0];
277
+ track.positions[o3 + 1] = rest + drop * (1 - t * t);
278
+ track.positions[o3 + 2] = spot[2];
279
+ track.rotations[o4] = q[0];
280
+ track.rotations[o4 + 1] = q[1];
281
+ track.rotations[o4 + 2] = q[2];
282
+ track.rotations[o4 + 3] = q[3];
283
+ }
284
+ }
285
+ const placedFrom = (track) => Math.max(0, track.steps - PLACED_DROP_STEPS);
286
+ export {
287
+ DICE_CONTACTS,
288
+ DICE_GRAVITY,
289
+ DICE_MAX_STEPS,
290
+ DICE_STEP,
291
+ LAUNCH_EDGES,
292
+ MAX_DICE,
293
+ PLACED_DROP_STEPS,
294
+ diceSettleParams,
295
+ diceTrackScene,
296
+ entranceSpots,
297
+ laneLayout,
298
+ launchEdgeOf,
299
+ placeDie,
300
+ placedFrom,
301
+ placementSpots,
302
+ seedOf,
303
+ unlandedDice
304
+ };
package/dist/tray.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare const TRAY_SHADERS: Record<string, string>;
2
+ export declare const DiceTray: import("@aethervtt/shard-render").MaterialType;
package/dist/tray.js ADDED
@@ -0,0 +1,43 @@
1
+ import { t } from "@aethervtt/shard-core";
2
+ import { defineMaterial } from "@aethervtt/shard-render";
3
+ const TRAY_SHADERS = {
4
+ "dice::tray": `
5
+ import shard::pbr::types::VertexOutput;
6
+ import shard::mesh::vertex_instance_data;
7
+ import shard::pbr::catcher::shadow_visibility;
8
+ import material::dice_tray::DiceTray;
9
+
10
+ override fn vertex_extra(position: vec3f, normal: vec3f, uv: vec2f) -> vec4f {
11
+ return vec4f(vertex_instance_data().x, 0.0, 0.0, 0.0);
12
+ }
13
+
14
+ override fn shade(in: VertexOutput) -> vec4f {
15
+ let blob = in.extra.x;
16
+ if (blob > 0.0) {
17
+ // A soft round shadow under a die: dense in the middle, gone at the quad's rim.
18
+ let r = length(in.uv * 2.0 - 1.0);
19
+ let a = 1.0 - smoothstep(0.0, 1.0, r);
20
+ return vec4f(0.0, 0.0, 0.0, a * a * blob * DiceTray.blobOpacity);
21
+ }
22
+ return vec4f(0.0, 0.0, 0.0, DiceTray.shadowOpacity * (1.0 - shadow_visibility(in)));
23
+ }`
24
+ };
25
+ const DiceTray = defineMaterial("dice/DiceTray", {
26
+ extends: "none",
27
+ blend: "premultiplied",
28
+ fields: {
29
+ shadowOpacity: t.f32({ default: 0.55, min: 0, max: 1, description: "Alpha of full shadow." }),
30
+ blobOpacity: t.f32({
31
+ default: 0.6,
32
+ min: 0,
33
+ max: 1,
34
+ description: "Alpha of a contact blob under a resting die."
35
+ })
36
+ },
37
+ shader: "dice::tray",
38
+ description: "The dice tray: invisible but for the shadows it catches and the contact blobs under blended dice (and every die in the large-pool tier)."
39
+ });
40
+ export {
41
+ DiceTray,
42
+ TRAY_SHADERS
43
+ };
@@ -0,0 +1,9 @@
1
+ import type { ShaderVariantSource } from '@aethervtt/shard-render';
2
+ import { type DieKind } from './builtins.js';
3
+ export interface DiceVariantEntry {
4
+ /** Skins by path or guid, or "*": every skin loaded or in the catalog. Default "*". */
5
+ skins?: '*' | string[];
6
+ /** Die kinds, or "*". Default "*". */
7
+ kinds?: '*' | DieKind[];
8
+ }
9
+ export declare const diceVariants: ShaderVariantSource;
@@ -0,0 +1,46 @@
1
+ import { assetServer } from "@aethervtt/shard-assets";
2
+ import { DIE_KINDS } from "./builtins.js";
3
+ import { renderDiceThumbnail } from "./preview.js";
4
+ import { DiceSkin } from "./skin.js";
5
+ const diceVariants = {
6
+ name: "dice",
7
+ description: 'Dice skins \xD7 die kinds, opaque and blended: { skins?: "*" | [path|guid], kinds?: "*" | [kind] }.',
8
+ async show(app, entry) {
9
+ const e = entry ?? {};
10
+ const world = app.world;
11
+ const store = world.resource(DiceSkin.store);
12
+ const ref = (guid, path) => ({
13
+ type: DiceSkin.name,
14
+ guid,
15
+ path
16
+ });
17
+ const skins = [];
18
+ if (e.skins === void 0 || e.skins === "*") {
19
+ const server = assetServer(world);
20
+ for (const asset of server.list({ type: DiceSkin.name })) {
21
+ await server.load(ref(asset.guid, asset.path));
22
+ }
23
+ for (const [guid] of store.entries()) skins.push(ref(guid));
24
+ } else {
25
+ for (const s of e.skins) {
26
+ const skin = !store.get(ref(s)) && s.endsWith(".json") ? ref(void 0, s) : ref(s);
27
+ if (!store.get(skin)) await assetServer(world).load(skin);
28
+ skins.push(skin);
29
+ }
30
+ }
31
+ const kinds = e.kinds === void 0 || e.kinds === "*" ? DIE_KINDS : e.kinds;
32
+ let shown = 0;
33
+ for (const skin of skins) {
34
+ for (const kind of kinds) {
35
+ for (const blended of [false, true]) {
36
+ await renderDiceThumbnail(app, { skin, kind, value: 1, size: 32, blended });
37
+ shown++;
38
+ }
39
+ }
40
+ }
41
+ return shown;
42
+ }
43
+ };
44
+ export {
45
+ diceVariants
46
+ };
@@ -0,0 +1,25 @@
1
+ import { type AssetRef, type Entity, type World } from '@aethervtt/shard-core';
2
+ import type { DiceSceneContext } from './attachments.js';
3
+ /** Where a window sits over a die, in die radii. */
4
+ export interface DiceWindow {
5
+ /** Center from the die's center (or `at`): x right, y up on screen. */
6
+ x: number;
7
+ y: number;
8
+ width: number;
9
+ height: number;
10
+ /** Turn on screen, radians counter-clockwise. */
11
+ turn: number;
12
+ /** Toward the camera from the die's center, in die radii (0: at its depth). */
13
+ lift: number;
14
+ /** Where it's centered in the world, instead of the die's position (an entrance's rest spot). */
15
+ at?: ArrayLike<number>;
16
+ }
17
+ /**
18
+ * Spawns a window facing the dice camera over a die, with `material`. A window lifted toward the
19
+ * camera is moved along the line from the camera and scaled by how much nearer it got, so it covers
20
+ * the pixels it would at the die's depth: windows at different depths (a disk's far half behind the
21
+ * die, its near band over it) line up wherever the die is.
22
+ */
23
+ export declare function spawnDiceWindow(ctx: Pick<DiceSceneContext, 'world' | 'die' | 'scale' | 'camera'>, material: AssetRef<'Material'>, window: DiceWindow): Entity;
24
+ /** Vertices the meshes of `entities` draw (their Mesh3d), for holding scenes to their budgets. */
25
+ export declare function meshVertices(world: World, entities: readonly Entity[]): number;
@@ -0,0 +1,77 @@
1
+ import { quat } from "@aethervtt/shard-core";
2
+ import { plane } from "@aethervtt/shard-mesh";
3
+ import {
4
+ Mesh3d,
5
+ Meshes,
6
+ MeshMaterial,
7
+ NotShadowCaster,
8
+ NotShadowReceiver
9
+ } from "@aethervtt/shard-render";
10
+ import { Transform } from "@aethervtt/shard-transform";
11
+ const quads = /* @__PURE__ */ new WeakMap();
12
+ function quadOf(world) {
13
+ let mesh = quads.get(world);
14
+ if (!mesh) {
15
+ mesh = world.resource(Meshes).add(plane({ size: 2 }), "dice:window");
16
+ quads.set(world, mesh);
17
+ }
18
+ return mesh;
19
+ }
20
+ function spawnDiceWindow(ctx, material, window) {
21
+ const w = ctx.world;
22
+ const { rotation: cam, translation: eye } = w.get(ctx.camera, Transform);
23
+ const [qx, qy, qz, qw] = cam;
24
+ const rx = 1 - 2 * (qy * qy + qz * qz);
25
+ const ry = 2 * (qx * qy + qw * qz);
26
+ const rz = 2 * (qx * qz - qw * qy);
27
+ const ux = 2 * (qx * qy - qw * qz);
28
+ const uy = 1 - 2 * (qx * qx + qz * qz);
29
+ const uz = 2 * (qy * qz + qw * qx);
30
+ const at = window.at ?? w.get(ctx.die, Transform).translation;
31
+ const s = ctx.scale;
32
+ const px = at[0] + (rx * window.x + ux * window.y) * s - eye[0];
33
+ const py = at[1] + (ry * window.x + uy * window.y) * s - eye[1];
34
+ const pz = at[2] + (rz * window.x + uz * window.y) * s - eye[2];
35
+ const dx = at[0] - eye[0];
36
+ const dy = at[1] - eye[1];
37
+ const dz = at[2] - eye[2];
38
+ const d = Math.sqrt(dx * dx + dy * dy + dz * dz);
39
+ const f = Math.max(0.05, (d - window.lift * s) / d);
40
+ const face = quat.multiply(
41
+ [0, 0, 0, 1],
42
+ cam,
43
+ quat.multiply(
44
+ [0, 0, 0, 1],
45
+ quat.fromEuler([0, 0, 0, 1], 0, 0, window.turn),
46
+ quat.fromEuler([0, 0, 0, 1], Math.PI / 2, 0, 0)
47
+ )
48
+ );
49
+ return w.spawn(
50
+ [Mesh3d, { mesh: quadOf(w) }],
51
+ [MeshMaterial, { material }],
52
+ NotShadowCaster,
53
+ NotShadowReceiver,
54
+ [
55
+ Transform,
56
+ {
57
+ translation: [eye[0] + px * f, eye[1] + py * f, eye[2] + pz * f],
58
+ rotation: face,
59
+ scale: [window.width / 2 * s * f, 1, window.height / 2 * s * f]
60
+ }
61
+ ]
62
+ );
63
+ }
64
+ function meshVertices(world, entities) {
65
+ const meshes = world.resource(Meshes);
66
+ let n = 0;
67
+ for (const e of entities) {
68
+ const ref = world.isAlive(e) ? world.tryGet(e, Mesh3d)?.mesh : void 0;
69
+ const mesh = ref ? meshes.get(ref) : void 0;
70
+ if (mesh) n += mesh.positions.length / 3;
71
+ }
72
+ return n;
73
+ }
74
+ export {
75
+ meshVertices,
76
+ spawnDiceWindow
77
+ };
@@ -0,0 +1,6 @@
1
+ export { DICE_SETTLE_RULE, diceSettle } from './settle.js';
2
+ /**
3
+ * The dice worker, bundled by the host's bundler (the `new Worker(new URL(…, import.meta.url))`
4
+ * form Vite and webpack recognize): 0053's track server with `dice-settle` registered.
5
+ */
6
+ export declare function diceWorker(): Worker;
package/dist/worker.js ADDED
@@ -0,0 +1,9 @@
1
+ import { DICE_SETTLE_RULE, diceSettle } from "./settle.js";
2
+ function diceWorker() {
3
+ return new Worker(new URL("./dice-worker.js", import.meta.url), { type: "module" });
4
+ }
5
+ export {
6
+ DICE_SETTLE_RULE,
7
+ diceSettle,
8
+ diceWorker
9
+ };
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@aethervtt/shard-dice",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./worker": {
13
+ "types": "./dist/worker.d.ts",
14
+ "default": "./dist/worker.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "LICENSE",
20
+ "README.md"
21
+ ],
22
+ "dependencies": {
23
+ "@aethervtt/shard-assets": "0.1.0",
24
+ "@aethervtt/shard-audio": "0.1.0",
25
+ "@aethervtt/shard-core": "0.1.0",
26
+ "@aethervtt/shard-gpu": "0.1.0",
27
+ "@aethervtt/shard-mesh": "0.1.0",
28
+ "@aethervtt/shard-particles": "0.1.0",
29
+ "@aethervtt/shard-physics": "0.1.0",
30
+ "@aethervtt/shard-protocol": "0.1.0",
31
+ "@aethervtt/shard-render": "0.1.0",
32
+ "@aethervtt/shard-runtime": "0.1.0",
33
+ "@aethervtt/shard-text": "0.1.0",
34
+ "@aethervtt/shard-texture": "0.1.0",
35
+ "@aethervtt/shard-transform": "0.1.0"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/aethervtt/shard.git",
40
+ "directory": "packages/dice"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public",
44
+ "provenance": true
45
+ },
46
+ "shard": {
47
+ "buildSha": "8e14c9896f82a3554b156f1ffdd68f2cc3e7d78a"
48
+ }
49
+ }