@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,60 @@
1
+ import { defineDataType } from "@aethervtt/shard-assets";
2
+ import { t } from "@aethervtt/shard-core";
3
+ const DieDefinitionAsset = defineDataType(
4
+ "dice/DieDefinition",
5
+ {
6
+ version: t.u8({ default: 1, min: 1, max: 1, description: "Format version: 1." }),
7
+ id: t.string({ description: "'d20', 'd10-tens', 'd100-ball'." }),
8
+ sides: t.u8({ min: 2, max: 100, description: "How many values it shows." }),
9
+ vertices: t.list(t.f32(), {
10
+ description: "Canonical points, x y z each, unit radius, in a fixed order: vertex ids index them."
11
+ }),
12
+ read: t.enum(["face", "vertex"], {
13
+ description: "The face on top reads, or (d4) the vertex on top."
14
+ }),
15
+ values: t.json({
16
+ default: {},
17
+ description: "Value by sorted vertex ids of a face ('0:1:4:5') or by a vertex id ('3')."
18
+ }),
19
+ labels: t.json({
20
+ default: {},
21
+ description: "The printed mark per value where it isn't the value ('00')."
22
+ }),
23
+ collider: t.enum(["hull", "ball"], { description: "A hull of the vertices, or a ball." }),
24
+ sizeMm: t.f32({ default: 16, min: 1, unit: "mm", description: "Landed footprint diameter." }),
25
+ bevel: t.f32({
26
+ default: 0.082,
27
+ min: 0,
28
+ max: 0.3,
29
+ description: "Chamfer toward each face center."
30
+ }),
31
+ markScale: t.f32({
32
+ default: 0.4,
33
+ min: 0.05,
34
+ max: 1,
35
+ description: "Mark height, of a face's size."
36
+ }),
37
+ oppositesSum: t.bool({ description: "Opposite faces must sum to sides + 1." })
38
+ },
39
+ { extension: "die", description: "A die: its shape, numbering and printed labels." }
40
+ );
41
+ function dieDefinitionOf(value) {
42
+ return {
43
+ version: 1,
44
+ id: value.id,
45
+ sides: value.sides,
46
+ vertices: Float32Array.from(value.vertices),
47
+ read: value.read,
48
+ values: value.values,
49
+ labels: value.labels,
50
+ collider: value.collider,
51
+ sizeMm: value.sizeMm,
52
+ bevel: value.bevel,
53
+ markScale: value.markScale,
54
+ oppositesSum: value.oppositesSum
55
+ };
56
+ }
57
+ export {
58
+ DieDefinitionAsset,
59
+ dieDefinitionOf
60
+ };
@@ -0,0 +1,98 @@
1
+ import { ShardError } from '@aethervtt/shard-core';
2
+ import { type Face } from './hull.js';
3
+ import { type Q4, type V3 } from './math.js';
4
+ /**
5
+ * A die's shape and numbering, as data (0054). Built-ins cover d4 to d20, the two percentile d10s
6
+ * and a d100 ball; a host can register more with `defineDie`.
7
+ */
8
+ export interface DieDefinition {
9
+ version: 1;
10
+ /** 'd20', 'd10-tens', 'd100-ball'. */
11
+ id: string;
12
+ sides: number;
13
+ /**
14
+ * Canonical points, unit radius, in a fixed order: vertex ids are indices into it. For a ball,
15
+ * each point is a cell's center (its normal).
16
+ */
17
+ vertices: Float32Array;
18
+ /** The face on top reads, or (d4) the vertex on top. */
19
+ read: 'face' | 'vertex';
20
+ /**
21
+ * Value by sorted vertex ids of a face (`'0:1:4:5'`), or by a vertex id (`'3'`) for vertex dice
22
+ * and ball cells. Keyed by ids, so the numbering doesn't depend on the order a hull returns.
23
+ */
24
+ values: Record<string, number>;
25
+ /** The printed mark per value, where it isn't the value: `'00'` … `'90'`, or 10 → `'0'`. */
26
+ labels?: Record<number, string>;
27
+ /** A hull of the vertices, or a ball (every rotation is a symmetry). */
28
+ collider: 'hull' | 'ball';
29
+ /** Footprint diameter of a landed die, before the presentation scale. */
30
+ sizeMm: number;
31
+ /** Chamfer: how far each face corner moves toward its face's center (0..0.3). */
32
+ bevel: number;
33
+ /** Mark height as a fraction of a face's size (the atlas cell's inner square). */
34
+ markScale?: number;
35
+ /** Opposite faces must sum to `sides + 1` (checked by the validator). */
36
+ oppositesSum?: boolean;
37
+ }
38
+ /** A value's reading frame on the die: where it points, and which way its mark reads up. */
39
+ export interface ValueFrame {
40
+ value: number;
41
+ /** The direction that's up when this value shows: a face normal, or the d4's vertex. */
42
+ normal: V3;
43
+ /** Across the mark, left to right. */
44
+ tangent: V3;
45
+ /** The mark's up. `tangent × bitangent = normal`. */
46
+ bitangent: V3;
47
+ }
48
+ /** What the engine derives from a definition, once per content hash. */
49
+ export interface DieGeometry {
50
+ definition: DieDefinition;
51
+ hash: number;
52
+ /** Unit-radius points (a ball's cell centers). */
53
+ points: V3[];
54
+ /** The polytope that's drawn and (for hulls) collided: faces with ordered vertices. */
55
+ polytope: {
56
+ points: V3[];
57
+ faces: Face[];
58
+ };
59
+ /** The value each polytope face carries (face dice), or undefined (d4: marks per corner). */
60
+ faceValues: (number | undefined)[];
61
+ /** d4: the value at each point (vertex id → value). */
62
+ vertexValues: number[] | undefined;
63
+ /** Frames by value (index value − 1). */
64
+ frames: ValueFrame[];
65
+ /** Rotations mapping the die onto itself; empty for a ball (every rotation is one). */
66
+ symmetries: Q4[];
67
+ /** Landed footprint diameter at unit radius: the size a die of this definition shows. */
68
+ footprint: number;
69
+ /** Distance from the center to the floor when resting on a face (1 for a ball). */
70
+ restHeight: number;
71
+ }
72
+ /** 32-bit FNV-1a over the definition's canonical content. */
73
+ export declare function dieHash(def: DieDefinition): number;
74
+ /**
75
+ * Checks a definition and returns every problem, with a path to the field: every face (or vertex,
76
+ * or cell) has exactly one value, values are 1..sides with no duplicates, labels name real values,
77
+ * and opposite faces sum to `sides + 1` where the definition opts in.
78
+ */
79
+ export declare function validateDieDefinition(def: DieDefinition): ShardError[];
80
+ /**
81
+ * Derives a definition's faces, value frames and symmetry group, cached by its content hash.
82
+ * Throws `dice/invalid-definition` naming the first problem.
83
+ */
84
+ export declare function dieGeometry(def: DieDefinition): DieGeometry;
85
+ /** The value's frame, or `dice/invalid-value` if the die can't show it. */
86
+ export declare function frameOf(geometry: DieGeometry, value: number): ValueFrame;
87
+ /** The printed mark of a value: its label, or the value itself. */
88
+ export declare function labelOf(def: DieDefinition, value: number): string;
89
+ /**
90
+ * Registers a definition by id so skins and rolls can name it. Registering the same content again
91
+ * is a no-op; a different definition under a taken id throws `dice/registry-conflict`.
92
+ */
93
+ export declare function defineDie(def: DieDefinition): DieDefinition;
94
+ export declare function findDie(id: string): DieDefinition | undefined;
95
+ export declare function requireDie(id: string): DieDefinition;
96
+ export declare function allDice(): DieDefinition[];
97
+ /** Scale of a die of `def` at presentation scale 1: its footprint is `sizeMm` relative to the d20's 18 mm. */
98
+ export declare function physicalScale(geometry: DieGeometry, reference: DieGeometry): number;
@@ -0,0 +1,320 @@
1
+ import { ShardError } from "@aethervtt/shard-core";
2
+ import { convexFaces, pointsOf, symmetryGroup, tangentPolytope } from "./hull.js";
3
+ import {
4
+ cross,
5
+ dot,
6
+ EPSILON,
7
+ length,
8
+ normalize,
9
+ qKey,
10
+ qRotate,
11
+ round,
12
+ scale,
13
+ stableTangent,
14
+ sub,
15
+ tangentOf
16
+ } from "./math.js";
17
+ function invalid(message, path, hint) {
18
+ return new ShardError("dice/invalid-definition", message, {
19
+ path,
20
+ hint: hint ?? "See DieDefinition in @aethervtt/shard-dice."
21
+ });
22
+ }
23
+ function dieHash(def) {
24
+ const canonical = JSON.stringify([
25
+ def.version,
26
+ def.id,
27
+ def.sides,
28
+ Array.from(def.vertices),
29
+ def.read,
30
+ Object.keys(def.values).sort().map((k) => [k, def.values[k]]),
31
+ Object.keys(def.labels ?? {}).map(Number).sort((a, b) => a - b).map((k) => [k, def.labels[k]]),
32
+ def.collider,
33
+ def.sizeMm,
34
+ def.bevel,
35
+ def.markScale ?? null,
36
+ def.oppositesSum ?? false
37
+ ]);
38
+ let h = 2166136261;
39
+ for (let i = 0; i < canonical.length; i++) {
40
+ h ^= canonical.charCodeAt(i);
41
+ h = Math.imul(h, 16777619);
42
+ }
43
+ return h >>> 0;
44
+ }
45
+ const faceKey = (face) => [...face.vertices].sort((a, b) => a - b).join(":");
46
+ function validateDieDefinition(def) {
47
+ const errors = [];
48
+ if (def.version !== 1) errors.push(invalid(`Version ${def.version} isn't 1`, "/version"));
49
+ if (typeof def.id !== "string" || !/^[a-z0-9][a-z0-9._-]*$/.test(def.id))
50
+ errors.push(invalid(`id "${def.id}" isn't a lowercase id`, "/id"));
51
+ if (!Number.isInteger(def.sides) || def.sides < 2 || def.sides > 100)
52
+ errors.push(invalid("sides must be an integer from 2 to 100", "/sides"));
53
+ const v = def.vertices;
54
+ if (!(v instanceof Float32Array) || v.length < 12 || v.length % 3 !== 0) {
55
+ errors.push(invalid("vertices needs 4 or more x y z points in a Float32Array", "/vertices"));
56
+ return errors;
57
+ }
58
+ const points = pointsOf(v);
59
+ const radius = Math.max(...points.map(length));
60
+ if (Math.abs(radius - 1) > 1e-3)
61
+ errors.push(invalid(`vertices reach radius ${radius.toFixed(4)}, not 1`, "/vertices"));
62
+ if (def.collider === "ball") {
63
+ points.forEach((p, i) => {
64
+ if (Math.abs(length(p) - 1) > 1e-3)
65
+ errors.push(invalid(`Ball cell ${i} isn't on the unit sphere`, `/vertices/${i * 3}`));
66
+ });
67
+ }
68
+ if (!(def.sizeMm > 0)) errors.push(invalid("sizeMm must be positive", "/sizeMm"));
69
+ if (!(def.bevel >= 0 && def.bevel <= 0.3))
70
+ errors.push(invalid("bevel must be from 0 to 0.3", "/bevel"));
71
+ if (errors.length > 0) return errors;
72
+ const keys = [];
73
+ if (def.read === "vertex" || def.collider === "ball") {
74
+ for (let i = 0; i < points.length; i++) keys.push(String(i));
75
+ } else {
76
+ for (const face of convexFaces(points)) keys.push(faceKey(face));
77
+ }
78
+ if (keys.length !== def.sides) {
79
+ errors.push(
80
+ invalid(
81
+ `The shape has ${keys.length} ${def.read === "vertex" ? "vertices" : "faces"}, not ${def.sides}`,
82
+ "/sides"
83
+ )
84
+ );
85
+ }
86
+ const seen = /* @__PURE__ */ new Map();
87
+ for (const key of keys) {
88
+ const value = def.values[key];
89
+ if (value === void 0) {
90
+ errors.push(
91
+ invalid(`Face ${key} has no value`, `/values/${key}`, `Give it one of 1..${def.sides}.`)
92
+ );
93
+ continue;
94
+ }
95
+ if (!Number.isInteger(value) || value < 1 || value > def.sides) {
96
+ errors.push(invalid(`Face ${key} has value ${value}, not 1..${def.sides}`, `/values/${key}`));
97
+ continue;
98
+ }
99
+ const other = seen.get(value);
100
+ if (other !== void 0)
101
+ errors.push(invalid(`Faces ${other} and ${key} both have value ${value}`, `/values/${key}`));
102
+ seen.set(value, key);
103
+ }
104
+ for (const key of Object.keys(def.values)) {
105
+ if (!keys.includes(key))
106
+ errors.push(invalid(`values names ${key}, which isn't a face of the shape`, `/values/${key}`));
107
+ }
108
+ for (const key of Object.keys(def.labels ?? {})) {
109
+ const value = Number(key);
110
+ if (!seen.has(value))
111
+ errors.push(invalid(`labels names value ${key}, which no face has`, `/labels/${key}`));
112
+ }
113
+ if (def.oppositesSum && def.read === "face" && def.collider === "hull") {
114
+ const faces = convexFaces(points);
115
+ for (const face of faces) {
116
+ const opposite = faces.find((f) => dot(f.normal, face.normal) < -1 + 1e-4);
117
+ if (!opposite) {
118
+ errors.push(invalid(`Face ${faceKey(face)} has no opposite face`, "/oppositesSum"));
119
+ continue;
120
+ }
121
+ const a = def.values[faceKey(face)];
122
+ const b = def.values[faceKey(opposite)];
123
+ if (a !== void 0 && b !== void 0 && a + b !== def.sides + 1) {
124
+ errors.push(
125
+ invalid(
126
+ `Opposite faces ${faceKey(face)} (${a}) and ${faceKey(opposite)} (${b}) don't sum to ${def.sides + 1}`,
127
+ `/values/${faceKey(face)}`
128
+ )
129
+ );
130
+ }
131
+ }
132
+ }
133
+ return errors;
134
+ }
135
+ function markUp(points, face) {
136
+ const d = face.vertices.map((i) => length(sub(points[i], face.center)));
137
+ const far = Math.max(...d);
138
+ const farthest = face.vertices.filter((_, k) => d[k] > far - 1e-4);
139
+ let toward;
140
+ if (farthest.length === 1) toward = points[farthest[0]];
141
+ else if (face.vertices.length % 2 === 1) toward = points[Math.min(...face.vertices)];
142
+ else {
143
+ const vs = face.vertices;
144
+ let best = [vs[0], vs[1]];
145
+ let bestKey = Number.POSITIVE_INFINITY;
146
+ for (let k = 0; k < vs.length; k++) {
147
+ const a = vs[k];
148
+ const b = vs[(k + 1) % vs.length];
149
+ const key = Math.min(a, b) * 1e3 + Math.max(a, b);
150
+ if (key < bestKey) {
151
+ bestKey = key;
152
+ best = [a, b];
153
+ }
154
+ }
155
+ toward = scale(add3(points[best[0]], points[best[1]]), 0.5);
156
+ }
157
+ return tangentOf(sub(toward, face.center), face.normal);
158
+ }
159
+ const add3 = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
160
+ function footprintOf(points, down) {
161
+ let r = 0;
162
+ for (const p of points) {
163
+ const along = dot(p, down);
164
+ const x = sub(p, scale(down, along));
165
+ r = Math.max(r, length(x));
166
+ }
167
+ return r * 2;
168
+ }
169
+ const cache = /* @__PURE__ */ new Map();
170
+ const byObject = /* @__PURE__ */ new WeakMap();
171
+ function dieGeometry(def) {
172
+ const known = byObject.get(def);
173
+ if (known) return known;
174
+ const hash = dieHash(def);
175
+ const cached = cache.get(hash);
176
+ if (cached) {
177
+ byObject.set(def, cached);
178
+ return cached;
179
+ }
180
+ const errors = validateDieDefinition(def);
181
+ if (errors.length > 0) throw errors[0];
182
+ const points = pointsOf(def.vertices).map((p) => p.map(round));
183
+ const ball = def.collider === "ball";
184
+ const polytope = ball ? tangentPolytope(points.map(normalize)) : { points, faces: convexFaces(points) };
185
+ const symmetries = ball ? [] : symmetryGroup(points);
186
+ const directions = [];
187
+ let faceValues;
188
+ let vertexValues;
189
+ if (def.read === "vertex") {
190
+ vertexValues = points.map((_, i) => def.values[String(i)]);
191
+ faceValues = polytope.faces.map(() => void 0);
192
+ points.forEach((p, i) => {
193
+ directions.push({ value: vertexValues[i], normal: normalize(p), face: void 0 });
194
+ });
195
+ } else if (ball) {
196
+ faceValues = points.map((_, i) => def.values[String(i)]);
197
+ polytope.faces.forEach((face, i) => {
198
+ directions.push({ value: faceValues[i], normal: face.normal, face });
199
+ });
200
+ } else {
201
+ faceValues = polytope.faces.map((f) => def.values[faceKey(f)]);
202
+ polytope.faces.forEach((face, i) => {
203
+ directions.push({ value: faceValues[i], normal: face.normal, face });
204
+ });
205
+ }
206
+ directions.sort((a, b) => a.value - b.value);
207
+ const frames = [];
208
+ if (ball) {
209
+ for (const d of directions) {
210
+ const n = d.normal;
211
+ const pole = Math.abs(n[1]) > 0.999 ? [0, 0, n[1] > 0 ? -1 : 1] : [0, 1, 0];
212
+ const b = tangentOf(pole, n);
213
+ frames.push({ value: d.value, normal: n, tangent: cross(b, n), bitangent: b });
214
+ }
215
+ } else {
216
+ const ref = directions[0];
217
+ let refUp;
218
+ if (def.read === "vertex") {
219
+ const vertex = points.findIndex((_, i) => vertexValues[i] === ref.value);
220
+ const adjacent = polytope.faces.filter((f) => f.vertices.includes(vertex)).sort((a, b) => Math.min(...a.vertices) - Math.min(...b.vertices))[0];
221
+ refUp = adjacent ? tangentOf(sub(points[vertex], adjacent.center), ref.normal) : stableTangent(ref.normal);
222
+ } else {
223
+ refUp = markUp(points, ref.face);
224
+ }
225
+ for (const d of directions) {
226
+ const candidates = symmetries.filter((q2) => length(sub(qRotate(q2, ref.normal), d.normal)) < EPSILON * 10).sort((a, b2) => qKey(a).localeCompare(qKey(b2)));
227
+ const q = candidates[0];
228
+ if (!q) {
229
+ throw invalid(
230
+ `No symmetry of ${def.id} carries value ${ref.value}'s face to value ${d.value}'s`,
231
+ "/vertices",
232
+ "Dice need a symmetry group that moves every face to every other (fair dice have one)."
233
+ );
234
+ }
235
+ const b = normalize(qRotate(q, refUp));
236
+ frames.push({ value: d.value, normal: d.normal, tangent: cross(b, d.normal), bitangent: b });
237
+ }
238
+ }
239
+ const up = frames[0].normal;
240
+ const down = scale(up, -1);
241
+ let rest = 0;
242
+ for (const p of polytope.points) rest = Math.max(rest, dot(p, down));
243
+ const geometry = {
244
+ definition: def,
245
+ hash,
246
+ points,
247
+ polytope,
248
+ faceValues,
249
+ vertexValues,
250
+ frames,
251
+ symmetries,
252
+ footprint: footprintOf(ball ? polytope.points : points, down),
253
+ restHeight: ball ? 1 : rest
254
+ };
255
+ cache.set(hash, geometry);
256
+ byObject.set(def, geometry);
257
+ return geometry;
258
+ }
259
+ function frameOf(geometry, value) {
260
+ const frame = Number.isInteger(value) ? geometry.frames[value - 1] : void 0;
261
+ if (!frame || frame.value !== value) {
262
+ throw new ShardError(
263
+ "dice/invalid-value",
264
+ `A ${geometry.definition.id} can't show ${value}: its values are 1 to ${geometry.frames.length}`,
265
+ { hint: "The host decides the result; pass one the die has." }
266
+ );
267
+ }
268
+ return frame;
269
+ }
270
+ function labelOf(def, value) {
271
+ return def.labels?.[value] ?? String(value);
272
+ }
273
+ const definitions = /* @__PURE__ */ new Map();
274
+ function defineDie(def) {
275
+ const existing = definitions.get(def.id);
276
+ if (existing) {
277
+ if (dieHash(existing) === dieHash(def)) return existing;
278
+ throw new ShardError(
279
+ "dice/registry-conflict",
280
+ `Die "${def.id}" is already defined, differently`,
281
+ {
282
+ hint: "Give the new definition its own id."
283
+ }
284
+ );
285
+ }
286
+ const errors = validateDieDefinition(def);
287
+ if (errors.length > 0) throw errors[0];
288
+ definitions.set(def.id, def);
289
+ return def;
290
+ }
291
+ function findDie(id) {
292
+ return definitions.get(id);
293
+ }
294
+ function requireDie(id) {
295
+ const def = definitions.get(id);
296
+ if (!def) {
297
+ throw new ShardError("dice/unknown-die", `No die definition "${id}"`, {
298
+ hint: `Defined: ${[...definitions.keys()].join(", ")}. Register others with defineDie.`
299
+ });
300
+ }
301
+ return def;
302
+ }
303
+ function allDice() {
304
+ return [...definitions.values()];
305
+ }
306
+ function physicalScale(geometry, reference) {
307
+ return geometry.definition.sizeMm / reference.definition.sizeMm * (reference.footprint / geometry.footprint);
308
+ }
309
+ export {
310
+ allDice,
311
+ defineDie,
312
+ dieGeometry,
313
+ dieHash,
314
+ findDie,
315
+ frameOf,
316
+ labelOf,
317
+ physicalScale,
318
+ requireDie,
319
+ validateDieDefinition
320
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { serveTracks } from "@aethervtt/shard-physics/worker";
2
+ import { DICE_SETTLE_RULE, diceSettle } from "./settle.js";
3
+ serveTracks({ rules: { [DICE_SETTLE_RULE]: diceSettle } });
@@ -0,0 +1,97 @@
1
+ import { type FieldValue, type Infer, ShardError } from '@aethervtt/shard-core';
2
+ import type { DiceRoll, PhysicalDie } from './roll.js';
3
+ declare const CONDITION: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
4
+ kind: import("@aethervtt/shard-core").FieldType<"die" | "source" | "tag" | "total", "u8">;
5
+ die: import("@aethervtt/shard-core").FieldType<"any" | "d10" | "d100" | "d12" | "d20" | "d4" | "d6" | "d8" | "percentile", "u8">;
6
+ value: import("@aethervtt/shard-core").FieldType<number, "u16">;
7
+ face: import("@aethervtt/shard-core").FieldType<string, "object">;
8
+ state: import("@aethervtt/shard-core").FieldType<"any" | "dropped" | "kept", "u8">;
9
+ rank: import("@aethervtt/shard-core").FieldType<"any" | "high" | "low", "u8">;
10
+ compare: import("@aethervtt/shard-core").FieldType<"eq" | "gte" | "lte", "u8">;
11
+ total: import("@aethervtt/shard-core").FieldType<number, "f32">;
12
+ source: import("@aethervtt/shard-core").FieldType<"gm" | "mine" | "other", "u8">;
13
+ tag: import("@aethervtt/shard-core").FieldType<string, "object">;
14
+ }>, "object">;
15
+ declare const EFFECT: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
16
+ kind: import("@aethervtt/shard-core").FieldType<"attachment" | "entrance" | "lens-pulse" | "light-pulse" | "particle-burst" | "sound-accent", "u8">;
17
+ color: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
18
+ colors: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color[], "object">;
19
+ intensity: import("@aethervtt/shard-core").FieldType<number, "f32">;
20
+ count: import("@aethervtt/shard-core").FieldType<number, "u8">;
21
+ durationMs: import("@aethervtt/shard-core").FieldType<number, "u16">;
22
+ cue: import("@aethervtt/shard-core").FieldType<"arcane-spark" | "resin-chime" | "void-whump", "u8">;
23
+ gain: import("@aethervtt/shard-core").FieldType<number, "f32">;
24
+ radius: import("@aethervtt/shard-core").FieldType<number, "f32">;
25
+ strength: import("@aethervtt/shard-core").FieldType<number, "f32">;
26
+ attachment: import("@aethervtt/shard-core").FieldType<string, "object">;
27
+ entrance: import("@aethervtt/shard-core").FieldType<string, "object">;
28
+ params: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").JsonValue, "object">;
29
+ }>, "object">;
30
+ /** An effect recipe: `data/**\/*.dice-effect.json`. */
31
+ export declare const DiceEffectRecipe: import("@aethervtt/shard-assets").DataType<{
32
+ readonly version: import("@aethervtt/shard-core").FieldType<number, "u8">;
33
+ readonly id: import("@aethervtt/shard-core").FieldType<string, "object">;
34
+ readonly match: import("@aethervtt/shard-core").FieldType<"all" | "any", "u8">;
35
+ readonly conditions: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
36
+ kind: import("@aethervtt/shard-core").FieldType<"die" | "source" | "tag" | "total", "u8">;
37
+ die: import("@aethervtt/shard-core").FieldType<"any" | "d10" | "d100" | "d12" | "d20" | "d4" | "d6" | "d8" | "percentile", "u8">;
38
+ value: import("@aethervtt/shard-core").FieldType<number, "u16">;
39
+ face: import("@aethervtt/shard-core").FieldType<string, "object">;
40
+ state: import("@aethervtt/shard-core").FieldType<"any" | "dropped" | "kept", "u8">;
41
+ rank: import("@aethervtt/shard-core").FieldType<"any" | "high" | "low", "u8">;
42
+ compare: import("@aethervtt/shard-core").FieldType<"eq" | "gte" | "lte", "u8">;
43
+ total: import("@aethervtt/shard-core").FieldType<number, "f32">;
44
+ source: import("@aethervtt/shard-core").FieldType<"gm" | "mine" | "other", "u8">;
45
+ tag: import("@aethervtt/shard-core").FieldType<string, "object">;
46
+ }>[], "object">;
47
+ readonly effects: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
48
+ kind: import("@aethervtt/shard-core").FieldType<"attachment" | "entrance" | "lens-pulse" | "light-pulse" | "particle-burst" | "sound-accent", "u8">;
49
+ color: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
50
+ colors: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color[], "object">;
51
+ intensity: import("@aethervtt/shard-core").FieldType<number, "f32">;
52
+ count: import("@aethervtt/shard-core").FieldType<number, "u8">;
53
+ durationMs: import("@aethervtt/shard-core").FieldType<number, "u16">;
54
+ cue: import("@aethervtt/shard-core").FieldType<"arcane-spark" | "resin-chime" | "void-whump", "u8">;
55
+ gain: import("@aethervtt/shard-core").FieldType<number, "f32">;
56
+ radius: import("@aethervtt/shard-core").FieldType<number, "f32">;
57
+ strength: import("@aethervtt/shard-core").FieldType<number, "f32">;
58
+ attachment: import("@aethervtt/shard-core").FieldType<string, "object">;
59
+ entrance: import("@aethervtt/shard-core").FieldType<string, "object">;
60
+ params: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").JsonValue, "object">;
61
+ }>[], "object">;
62
+ }, "dice/DiceEffectRecipe">;
63
+ export type DiceEffectRecipeValue = Infer<typeof DiceEffectRecipe>;
64
+ export type DiceEffect = FieldValue<typeof EFFECT>;
65
+ export type DiceCondition = FieldValue<typeof CONDITION>;
66
+ export declare const RECIPE_LIMITS: {
67
+ conditions: number;
68
+ effects: number;
69
+ particles: number;
70
+ colors: number;
71
+ minDurationMs: number;
72
+ maxDurationMs: number;
73
+ recipesPerRoll: number;
74
+ params: number;
75
+ };
76
+ /** A recipe from its JSON form, with defaults filled in. */
77
+ export declare function diceEffectRecipe(json: Record<string, unknown>): DiceEffectRecipeValue;
78
+ /** A recipe's problems, each with a path into it. */
79
+ export declare function recipeProblems(recipe: DiceEffectRecipeValue): ShardError[];
80
+ /** A recipe that matched, and the dice its effects play on. */
81
+ export interface MatchedRecipe {
82
+ recipe: DiceEffectRecipeValue;
83
+ /**
84
+ * Kept dice its effects anchor to: those meeting a die condition, or every kept die when it has
85
+ * none. Dropped dice never anchor effects, though conditions can match `state: 'dropped'`.
86
+ */
87
+ anchors: number[];
88
+ }
89
+ /**
90
+ * The recipes that play for a roll: each distinct one (by id) whose conditions match, at most 4.
91
+ * `degraded` says a bound refused more.
92
+ */
93
+ export declare function matchRecipes(recipes: readonly DiceEffectRecipeValue[], roll: DiceRoll, dice: readonly PhysicalDie[]): {
94
+ matched: MatchedRecipe[];
95
+ degraded: boolean;
96
+ };
97
+ export {};