@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
package/dist/index.js ADDED
@@ -0,0 +1,322 @@
1
+ import {
2
+ allDiceAttachments,
3
+ DICE_BUDGETS,
4
+ defineDiceAttachment,
5
+ findDiceAttachment,
6
+ MAX_ATTACHMENTS,
7
+ setDiceBudgets
8
+ } from "./attachments.js";
9
+ import {
10
+ BUILTIN_DICE,
11
+ D4,
12
+ D6,
13
+ D8,
14
+ D10,
15
+ D10_TENS,
16
+ D12,
17
+ D20,
18
+ D100_BALL,
19
+ DIE_KINDS,
20
+ KIND_DEFINITIONS,
21
+ kindSides,
22
+ percentileValues,
23
+ registerBuiltinDice
24
+ } from "./builtins.js";
25
+ import { CELL_PAD, cellLayout, MARK_RANGE } from "./cells.js";
26
+ import { DiceDie, DiceRollRequest } from "./components.js";
27
+ import {
28
+ allDice,
29
+ defineDie,
30
+ dieGeometry,
31
+ dieHash,
32
+ findDie,
33
+ frameOf,
34
+ labelOf,
35
+ physicalScale,
36
+ requireDie,
37
+ validateDieDefinition
38
+ } from "./definition.js";
39
+ import {
40
+ DieDefinitionAsset,
41
+ dieDefinitionOf
42
+ } from "./definition-asset.js";
43
+ import {
44
+ DiceEffectRecipe,
45
+ diceEffectRecipe,
46
+ matchRecipes,
47
+ RECIPE_LIMITS,
48
+ recipeProblems
49
+ } from "./effects.js";
50
+ import {
51
+ allDiceEntrances,
52
+ defineDiceEntrance,
53
+ findDiceEntrance,
54
+ MAX_ENTRANCES
55
+ } from "./entrances.js";
56
+ import { BUILTIN_FAMILIES, GLASS, METAL, RESIN, SOLID } from "./families.js";
57
+ import {
58
+ allDiceGlyphs,
59
+ BUILTIN_GLYPHS,
60
+ defineDiceGlyph,
61
+ findDiceGlyph,
62
+ parseSvgPath,
63
+ registerBuiltinGlyphs
64
+ } from "./glyphs.js";
65
+ import {
66
+ landingCorrection,
67
+ markAngle,
68
+ naturalValue,
69
+ restingHeight,
70
+ restRotation,
71
+ topAlignment
72
+ } from "./landing.js";
73
+ import {
74
+ ambiguousLabel,
75
+ defaultLayoutFor,
76
+ FaceLayout,
77
+ layoutProblems,
78
+ NUMBERS_LAYOUT,
79
+ PIPS_LAYOUT,
80
+ resolveMark
81
+ } from "./layout.js";
82
+ import {
83
+ bakeMarks,
84
+ clearMarkCache,
85
+ MARK_MARGIN,
86
+ markAtlasKey,
87
+ markPlacements
88
+ } from "./marks.js";
89
+ import {
90
+ allDiceFamilies,
91
+ DICE_FIELDS,
92
+ DICE_SHADERS,
93
+ defineDiceFamily,
94
+ findDiceFamily,
95
+ isDiceMaterial
96
+ } from "./material.js";
97
+ import { dieMesh, floorQuad } from "./mesh.js";
98
+ import { diceMethods } from "./methods.js";
99
+ import { NUMERAL_HEIGHT } from "./numerals.js";
100
+ import { dicePlugin, TOP_DOWN } from "./plugin.js";
101
+ import { renderDiceThumbnail } from "./preview.js";
102
+ import { resolveDiceQuality } from "./quality.js";
103
+ import {
104
+ DiceResources,
105
+ RELEASE_AFTER_MS
106
+ } from "./resources.js";
107
+ import {
108
+ expandRoll,
109
+ presentationScale,
110
+ rollTrackRequest,
111
+ viewportTray
112
+ } from "./roll.js";
113
+ import {
114
+ BALL_DAMPING,
115
+ DICE_CLEANUP_STEP,
116
+ DICE_GROUPS,
117
+ DICE_SETTLE_RULE,
118
+ diceSettle,
119
+ outsideTray,
120
+ restingFlat
121
+ } from "./settle.js";
122
+ import {
123
+ BUILTIN_SKINS,
124
+ DICE_SKINS,
125
+ DiceSkin,
126
+ diceSkin,
127
+ IMPACT_SOUNDS,
128
+ MAX_SKIN_EFFECTS,
129
+ skinVariant,
130
+ validateDiceSkin
131
+ } from "./skin.js";
132
+ import {
133
+ ACCENT_CUES,
134
+ accentSamples,
135
+ DiceSoundBank,
136
+ IMPACT_LAYERS,
137
+ IMPACT_VARIATIONS,
138
+ impactLayer,
139
+ impactSamples,
140
+ impactStrength,
141
+ wavBytes
142
+ } from "./sound.js";
143
+ import { studioEnvironment } from "./studio.js";
144
+ import {
145
+ ANIMATED_DEMAND,
146
+ DiceTable,
147
+ DiceTableState,
148
+ ENTRANCE_DEMAND,
149
+ ENTRANCE_WAIT_MS,
150
+ PRESENTATION_DEMAND,
151
+ PREVIEW_TRAY
152
+ } from "./table.js";
153
+ import {
154
+ DICE_CONTACTS,
155
+ DICE_GRAVITY,
156
+ DICE_MAX_STEPS,
157
+ DICE_STEP,
158
+ diceSettleParams,
159
+ diceTrackScene,
160
+ LAUNCH_EDGES,
161
+ laneLayout,
162
+ launchEdgeOf,
163
+ MAX_DICE,
164
+ PLACED_DROP_STEPS,
165
+ placeDie,
166
+ placedFrom,
167
+ placementSpots,
168
+ seedOf,
169
+ unlandedDice
170
+ } from "./track.js";
171
+ import { DiceTray, TRAY_SHADERS } from "./tray.js";
172
+ import { diceVariants } from "./variants.js";
173
+ import { spawnDiceWindow } from "./windows.js";
174
+ export {
175
+ ACCENT_CUES,
176
+ ANIMATED_DEMAND,
177
+ BALL_DAMPING,
178
+ BUILTIN_DICE,
179
+ BUILTIN_FAMILIES,
180
+ BUILTIN_GLYPHS,
181
+ BUILTIN_SKINS,
182
+ CELL_PAD,
183
+ D10,
184
+ D100_BALL,
185
+ D10_TENS,
186
+ D12,
187
+ D20,
188
+ D4,
189
+ D6,
190
+ D8,
191
+ DICE_BUDGETS,
192
+ DICE_CLEANUP_STEP,
193
+ DICE_CONTACTS,
194
+ DICE_FIELDS,
195
+ DICE_GRAVITY,
196
+ DICE_GROUPS,
197
+ DICE_MAX_STEPS,
198
+ DICE_SETTLE_RULE,
199
+ DICE_SHADERS,
200
+ DICE_SKINS,
201
+ DICE_STEP,
202
+ DIE_KINDS,
203
+ DiceDie,
204
+ DiceEffectRecipe,
205
+ DiceResources,
206
+ DiceRollRequest,
207
+ DiceSkin,
208
+ DiceSoundBank,
209
+ DiceTable,
210
+ DiceTableState,
211
+ DiceTray as DiceTrayMaterial,
212
+ DieDefinitionAsset,
213
+ ENTRANCE_DEMAND,
214
+ ENTRANCE_WAIT_MS,
215
+ FaceLayout,
216
+ GLASS,
217
+ IMPACT_LAYERS,
218
+ IMPACT_SOUNDS,
219
+ IMPACT_VARIATIONS,
220
+ KIND_DEFINITIONS,
221
+ LAUNCH_EDGES,
222
+ MARK_MARGIN,
223
+ MARK_RANGE,
224
+ MAX_ATTACHMENTS,
225
+ MAX_DICE,
226
+ MAX_ENTRANCES,
227
+ MAX_SKIN_EFFECTS,
228
+ METAL,
229
+ NUMBERS_LAYOUT,
230
+ NUMERAL_HEIGHT,
231
+ PIPS_LAYOUT,
232
+ PLACED_DROP_STEPS,
233
+ PRESENTATION_DEMAND,
234
+ PREVIEW_TRAY,
235
+ RECIPE_LIMITS,
236
+ RELEASE_AFTER_MS,
237
+ RESIN,
238
+ SOLID,
239
+ TOP_DOWN,
240
+ TRAY_SHADERS,
241
+ accentSamples,
242
+ allDice,
243
+ allDiceAttachments,
244
+ allDiceEntrances,
245
+ allDiceFamilies,
246
+ allDiceGlyphs,
247
+ ambiguousLabel,
248
+ bakeMarks,
249
+ cellLayout,
250
+ clearMarkCache,
251
+ defaultLayoutFor,
252
+ defineDiceAttachment,
253
+ defineDiceEntrance,
254
+ defineDiceFamily,
255
+ defineDiceGlyph,
256
+ defineDie,
257
+ diceEffectRecipe,
258
+ diceMethods,
259
+ dicePlugin,
260
+ diceSettle,
261
+ diceSettleParams,
262
+ diceSkin,
263
+ diceTrackScene,
264
+ diceVariants,
265
+ dieDefinitionOf,
266
+ dieGeometry,
267
+ dieHash,
268
+ dieMesh,
269
+ expandRoll,
270
+ findDiceAttachment,
271
+ findDiceEntrance,
272
+ findDiceFamily,
273
+ findDiceGlyph,
274
+ findDie,
275
+ floorQuad,
276
+ frameOf,
277
+ impactLayer,
278
+ impactSamples,
279
+ impactStrength,
280
+ isDiceMaterial,
281
+ kindSides,
282
+ labelOf,
283
+ landingCorrection,
284
+ laneLayout,
285
+ launchEdgeOf,
286
+ layoutProblems,
287
+ markAngle,
288
+ markAtlasKey,
289
+ markPlacements,
290
+ matchRecipes,
291
+ naturalValue,
292
+ outsideTray,
293
+ parseSvgPath,
294
+ percentileValues,
295
+ physicalScale,
296
+ placeDie,
297
+ placedFrom,
298
+ placementSpots,
299
+ presentationScale,
300
+ recipeProblems,
301
+ registerBuiltinDice,
302
+ registerBuiltinGlyphs,
303
+ renderDiceThumbnail,
304
+ requireDie,
305
+ resolveDiceQuality,
306
+ resolveMark,
307
+ restRotation,
308
+ restingFlat,
309
+ restingHeight,
310
+ rollTrackRequest,
311
+ seedOf,
312
+ setDiceBudgets,
313
+ skinVariant,
314
+ spawnDiceWindow,
315
+ studioEnvironment,
316
+ topAlignment,
317
+ unlandedDice,
318
+ validateDiceSkin,
319
+ validateDieDefinition,
320
+ viewportTray,
321
+ wavBytes
322
+ };
@@ -0,0 +1,29 @@
1
+ import { type DieDefinition, type DieGeometry } from './definition.js';
2
+ import { type Q4, type V3 } from './math.js';
3
+ /** Screen-up as a direction on the floor (the camera's up, flattened). */
4
+ export declare function floorUp(screenUp: ArrayLike<number>): V3;
5
+ /** The value on top for a body rotation: the one whose direction points most nearly up. */
6
+ export declare function naturalValue(die: DieDefinition | DieGeometry, rotation: ArrayLike<number>): number;
7
+ /** How nearly the top value's direction points up: 1 when it's exactly vertical. */
8
+ export declare function topAlignment(die: DieDefinition | DieGeometry, rotation: ArrayLike<number>): number;
9
+ /**
10
+ * The angle (radians) between a value's mark-up on screen and screen-up, both flattened onto the
11
+ * floor, for a die shown at `rotation`.
12
+ */
13
+ export declare function markAngle(die: DieDefinition | DieGeometry, rotation: ArrayLike<number>, value: number, screenUp?: ArrayLike<number>): number;
14
+ /**
15
+ * The correction for a die that came to rest at `finalRotation`: a symmetry C of its collider,
16
+ * post-multiplied onto every sampled rotation (shown = rotation · C), that puts `target` where the
17
+ * natural value is. Among the symmetries that do, it's the one whose mark reads closest to
18
+ * screen-up. A ball's group is every rotation, so its target ends exactly up and upright. Throws
19
+ * `dice/invalid-value` for a value the die can't show.
20
+ */
21
+ export declare function landingCorrection(die: DieDefinition | DieGeometry, finalRotation: ArrayLike<number>, target: number, screenUp?: ArrayLike<number>): Q4;
22
+ /**
23
+ * A rotation that rests `target` on top with its mark exactly toward screen-up: reduced motion,
24
+ * placed dice and thumbnails. The face opposite rests on the floor (a d4 rests on the face
25
+ * opposite its vertex).
26
+ */
27
+ export declare function restRotation(die: DieDefinition | DieGeometry, target: number, screenUp?: ArrayLike<number>): Q4;
28
+ /** The height of the die's center above the floor at `rotation`: its lowest point touches it. */
29
+ export declare function restingHeight(die: DieDefinition | DieGeometry, rotation: ArrayLike<number>, scale: number): number;
@@ -0,0 +1,93 @@
1
+ import { dieGeometry, frameOf } from "./definition.js";
2
+ import {
3
+ angleBetween,
4
+ dot,
5
+ EPSILON,
6
+ length,
7
+ normalize,
8
+ qConj,
9
+ qFromFrames,
10
+ qMul,
11
+ qNormalize,
12
+ qRotate,
13
+ sub
14
+ } from "./math.js";
15
+ const UP = [0, 1, 0];
16
+ const geometryOf = (die) => "frames" in die ? die : dieGeometry(die);
17
+ const quat = (q) => qNormalize([q[0], q[1], q[2], q[3]]);
18
+ function floorUp(screenUp) {
19
+ const flat = [screenUp[0], 0, screenUp[2]];
20
+ const l = length(flat);
21
+ return l > 1e-6 ? [flat[0] / l, 0, flat[2] / l] : [0, 0, -1];
22
+ }
23
+ function naturalValue(die, rotation) {
24
+ const g = geometryOf(die);
25
+ const q = quat(rotation);
26
+ let best = g.frames[0].value;
27
+ let height = Number.NEGATIVE_INFINITY;
28
+ for (const frame of g.frames) {
29
+ const y = qRotate(q, frame.normal)[1];
30
+ if (y > height) {
31
+ height = y;
32
+ best = frame.value;
33
+ }
34
+ }
35
+ return best;
36
+ }
37
+ function topAlignment(die, rotation) {
38
+ const g = geometryOf(die);
39
+ if (g.definition.collider === "ball") return 1;
40
+ const q = quat(rotation);
41
+ let height = Number.NEGATIVE_INFINITY;
42
+ for (const frame of g.frames) height = Math.max(height, qRotate(q, frame.normal)[1]);
43
+ return height;
44
+ }
45
+ function markAngle(die, rotation, value, screenUp = [0, 0, -1]) {
46
+ const g = geometryOf(die);
47
+ const b = qRotate(quat(rotation), frameOf(g, value).bitangent);
48
+ return angleBetween(normalize([b[0], 0, b[2]]), floorUp(screenUp));
49
+ }
50
+ function landingCorrection(die, finalRotation, target, screenUp = [0, 0, -1]) {
51
+ const g = geometryOf(die);
52
+ const frame = frameOf(g, target);
53
+ const r = quat(finalRotation);
54
+ const up = floorUp(screenUp);
55
+ if (g.definition.collider === "ball") {
56
+ return qNormalize(qMul(qConj(r), qFromFrames(frame.normal, frame.bitangent, UP, up)));
57
+ }
58
+ const natural = frameOf(g, naturalValue(g, r));
59
+ let best;
60
+ let score = Number.NEGATIVE_INFINITY;
61
+ for (const c of g.symmetries) {
62
+ if (length(sub(qRotate(c, frame.normal), natural.normal)) > EPSILON * 10) continue;
63
+ const b = qRotate(r, qRotate(c, frame.bitangent));
64
+ const s = dot(normalize([b[0], 0, b[2]]), up);
65
+ if (s > score + 1e-9) {
66
+ score = s;
67
+ best = c;
68
+ }
69
+ }
70
+ return best ?? [0, 0, 0, 1];
71
+ }
72
+ function restRotation(die, target, screenUp = [0, 0, -1]) {
73
+ const g = geometryOf(die);
74
+ const frame = frameOf(g, target);
75
+ return qFromFrames(frame.normal, frame.bitangent, UP, floorUp(screenUp));
76
+ }
77
+ function restingHeight(die, rotation, scale) {
78
+ const g = geometryOf(die);
79
+ if (g.definition.collider === "ball") return scale;
80
+ const q = quat(rotation);
81
+ let low = 0;
82
+ for (const p of g.points) low = Math.min(low, qRotate(q, p)[1]);
83
+ return -low * scale;
84
+ }
85
+ export {
86
+ floorUp,
87
+ landingCorrection,
88
+ markAngle,
89
+ naturalValue,
90
+ restRotation,
91
+ restingHeight,
92
+ topAlignment
93
+ };
@@ -0,0 +1,75 @@
1
+ import { type AssetRef, type FieldValue, type Infer, ShardError } from '@aethervtt/shard-core';
2
+ import { type DieDefinition } from './definition.js';
3
+ export declare const FaceMarkSchema: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
4
+ kind: import("@aethervtt/shard-core").FieldType<"blank" | "glyph" | "pips" | "text", "u8">;
5
+ text: import("@aethervtt/shard-core").FieldType<string, "object">;
6
+ font: import("@aethervtt/shard-core").FieldType<AssetRef<"Font"> | null, "object">;
7
+ size: import("@aethervtt/shard-core").FieldType<number, "f32">;
8
+ weight: import("@aethervtt/shard-core").FieldType<number, "f32">;
9
+ underline: import("@aethervtt/shard-core").FieldType<"always" | "auto" | "never", "u8">;
10
+ count: import("@aethervtt/shard-core").FieldType<number, "u8">;
11
+ glyph: import("@aethervtt/shard-core").FieldType<string, "object">;
12
+ rotation: import("@aethervtt/shard-core").FieldType<number, "f32">;
13
+ }>, "object">;
14
+ /** A face layout: `data/**\/*.dice-layout.json`. */
15
+ export declare const FaceLayout: import("@aethervtt/shard-assets").DataType<{
16
+ readonly version: import("@aethervtt/shard-core").FieldType<number, "u8">;
17
+ readonly id: import("@aethervtt/shard-core").FieldType<string, "object">;
18
+ readonly default: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
19
+ kind: import("@aethervtt/shard-core").FieldType<"blank" | "glyph" | "pips" | "text", "u8">;
20
+ text: import("@aethervtt/shard-core").FieldType<string, "object">;
21
+ font: import("@aethervtt/shard-core").FieldType<AssetRef<"Font"> | null, "object">;
22
+ size: import("@aethervtt/shard-core").FieldType<number, "f32">;
23
+ weight: import("@aethervtt/shard-core").FieldType<number, "f32">;
24
+ underline: import("@aethervtt/shard-core").FieldType<"always" | "auto" | "never", "u8">;
25
+ count: import("@aethervtt/shard-core").FieldType<number, "u8">;
26
+ glyph: import("@aethervtt/shard-core").FieldType<string, "object">;
27
+ rotation: import("@aethervtt/shard-core").FieldType<number, "f32">;
28
+ }>, "object">;
29
+ readonly overrides: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
30
+ value: import("@aethervtt/shard-core").FieldType<number, "u16">;
31
+ mark: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
32
+ kind: import("@aethervtt/shard-core").FieldType<"blank" | "glyph" | "pips" | "text", "u8">;
33
+ text: import("@aethervtt/shard-core").FieldType<string, "object">;
34
+ font: import("@aethervtt/shard-core").FieldType<AssetRef<"Font"> | null, "object">;
35
+ size: import("@aethervtt/shard-core").FieldType<number, "f32">;
36
+ weight: import("@aethervtt/shard-core").FieldType<number, "f32">;
37
+ underline: import("@aethervtt/shard-core").FieldType<"always" | "auto" | "never", "u8">;
38
+ count: import("@aethervtt/shard-core").FieldType<number, "u8">;
39
+ glyph: import("@aethervtt/shard-core").FieldType<string, "object">;
40
+ rotation: import("@aethervtt/shard-core").FieldType<number, "f32">;
41
+ }>, "object">;
42
+ }>[], "object">;
43
+ }, "dice/FaceLayout">;
44
+ export type FaceLayoutValue = Infer<typeof FaceLayout>;
45
+ export type FaceMark = FieldValue<typeof FaceMarkSchema>;
46
+ /** A mark ready to draw: text resolved to characters, pips to a count. */
47
+ export interface ResolvedMark {
48
+ kind: 'text' | 'pips' | 'glyph' | 'blank';
49
+ text: string;
50
+ font: AssetRef | null;
51
+ size: number;
52
+ weight: number;
53
+ underline: boolean;
54
+ count: number;
55
+ glyph: string;
56
+ /** Radians, clockwise. */
57
+ rotation: number;
58
+ }
59
+ /** Numbers on every face: each value's label, in the built-in numerals. */
60
+ export declare const NUMBERS_LAYOUT: FaceLayoutValue;
61
+ /** Pips on every face (up to 9). */
62
+ export declare const PIPS_LAYOUT: FaceLayoutValue;
63
+ /** The layout a die uses when its skin names none: pips on a d6, numbers everywhere else. */
64
+ export declare function defaultLayoutFor(def: DieDefinition): FaceLayoutValue;
65
+ /** Whether a label reads as another label of the die when the die is turned around. */
66
+ export declare function ambiguousLabel(def: DieDefinition, label: string): boolean;
67
+ /** The mark a value prints under a layout. */
68
+ export declare function resolveMark(layout: FaceLayoutValue, def: DieDefinition, value: number): ResolvedMark;
69
+ /**
70
+ * What a layout can't show on a die, by value: pips past 9, a glyph that isn't registered, text a
71
+ * font can't draw. Paths point into the layout.
72
+ */
73
+ export declare function layoutProblems(layout: FaceLayoutValue, def: DieDefinition, canDraw?: (text: string, font: AssetRef | null) => boolean): ShardError[];
74
+ /** The built-in numerals draw digits only. */
75
+ export declare function builtinCanDraw(text: string, font: AssetRef | null): boolean;
package/dist/layout.js ADDED
@@ -0,0 +1,137 @@
1
+ import { defineDataType } from "@aethervtt/shard-assets";
2
+ import { ShardError, t } from "@aethervtt/shard-core";
3
+ import { labelOf } from "./definition.js";
4
+ import { findDiceGlyph } from "./glyphs.js";
5
+ const MARK_FIELDS = {
6
+ kind: t.enum(["text", "pips", "glyph", "blank"], {
7
+ description: "text prints characters (the value's label unless `text` says otherwise); pips draws dots; glyph draws a registered vector path; blank prints nothing."
8
+ }),
9
+ text: t.string({ description: "Text marks: what to print. Empty prints the value's label." }),
10
+ font: t.handle("Font", {
11
+ description: "Text marks: the font's outlines. Null uses the built-in numerals (digits only)."
12
+ }),
13
+ size: t.f32({ default: 1, min: 0.2, max: 2, description: "Scales the die's mark size." }),
14
+ weight: t.f32({
15
+ default: 0.035,
16
+ min: -0.05,
17
+ max: 0.12,
18
+ unit: "em",
19
+ description: "Thickens (or thins) text and glyph strokes, from the distance field."
20
+ }),
21
+ underline: t.enum(["auto", "always", "never"], {
22
+ description: "auto underlines a label that reads as another of the die\u2019s labels upside down (6 and 9, 16 and 91)."
23
+ }),
24
+ count: t.u8({ max: 9, description: "Pips: how many. 0 draws the value." }),
25
+ glyph: t.string({
26
+ description: "Glyph marks: the name of a registered glyph (defineDiceGlyph)."
27
+ }),
28
+ rotation: t.f32({ unit: "deg", description: "Turns the mark on its face, clockwise." })
29
+ };
30
+ const FaceMarkSchema = t.struct(MARK_FIELDS, { description: "One mark on a face." });
31
+ const FaceLayout = defineDataType(
32
+ "dice/FaceLayout",
33
+ {
34
+ version: t.u8({ default: 1, min: 1, max: 1, description: "Format version: 1." }),
35
+ id: t.string({ description: "A name for tools and errors." }),
36
+ default: FaceMarkSchema,
37
+ overrides: t.list(
38
+ t.struct({
39
+ value: t.u16({ min: 1, max: 100, description: "The value (not the face) it overrides." }),
40
+ mark: FaceMarkSchema
41
+ }),
42
+ { description: "Marks for particular values, by value." }
43
+ )
44
+ },
45
+ {
46
+ extension: "dice-layout",
47
+ description: "What each value of a die prints: a default mark, and overrides by value."
48
+ }
49
+ );
50
+ const mark = (m) => ({
51
+ ...FaceMarkSchema.defaultValue(),
52
+ ...m
53
+ });
54
+ const NUMBERS_LAYOUT = {
55
+ version: 1,
56
+ id: "numbers",
57
+ default: mark({ kind: "text" }),
58
+ overrides: []
59
+ };
60
+ const PIPS_LAYOUT = {
61
+ version: 1,
62
+ id: "pips",
63
+ default: mark({ kind: "pips" }),
64
+ overrides: []
65
+ };
66
+ function defaultLayoutFor(def) {
67
+ return def.id === "d6" ? PIPS_LAYOUT : NUMBERS_LAYOUT;
68
+ }
69
+ const TURNED = { "0": "0", "1": "1", "6": "9", "8": "8", "9": "6" };
70
+ function upsideDown(label) {
71
+ let out = "";
72
+ for (let i = label.length - 1; i >= 0; i--) {
73
+ const c = TURNED[label[i]];
74
+ if (c === void 0) return void 0;
75
+ out += c;
76
+ }
77
+ return out;
78
+ }
79
+ function ambiguousLabel(def, label) {
80
+ const turned = upsideDown(label);
81
+ if (turned === void 0 || turned === label) return false;
82
+ for (let v = 1; v <= def.sides; v++) if (labelOf(def, v) === turned) return true;
83
+ return false;
84
+ }
85
+ function resolveMark(layout, def, value) {
86
+ const m = layout.overrides.find((o) => o.value === value)?.mark ?? layout.default;
87
+ const text = m.kind === "text" ? m.text || labelOf(def, value) : "";
88
+ return {
89
+ kind: m.kind,
90
+ text,
91
+ font: m.font,
92
+ size: m.size,
93
+ weight: m.weight,
94
+ underline: m.kind === "text" && (m.underline === "always" || m.underline === "auto" && ambiguousLabel(def, text)),
95
+ count: m.kind === "pips" ? m.count || value : 0,
96
+ glyph: m.glyph,
97
+ rotation: m.rotation * Math.PI / 180
98
+ };
99
+ }
100
+ function layoutProblems(layout, def, canDraw = builtinCanDraw) {
101
+ const out = [];
102
+ for (let value = 1; value <= def.sides; value++) {
103
+ const index = layout.overrides.findIndex((o) => o.value === value);
104
+ const path = index >= 0 ? `/overrides/${index}/mark` : "/default";
105
+ const m = resolveMark(layout, def, value);
106
+ let problem;
107
+ if (m.kind === "pips" && (m.count < 1 || m.count > 9)) problem = `${m.count} pips (1 to 9 draw)`;
108
+ else if (m.kind === "glyph" && !findDiceGlyph(m.glyph))
109
+ problem = m.glyph ? `glyph "${m.glyph}", which isn't registered` : "a glyph with no name";
110
+ else if (m.kind === "text" && !canDraw(m.text, m.font))
111
+ problem = `"${m.text}", which the font can't draw`;
112
+ if (problem) {
113
+ out.push(
114
+ new ShardError(
115
+ "dice/layout-missing-value",
116
+ `Layout "${layout.id}" can't show ${value} on a ${def.id}: it asks for ${problem}`,
117
+ { path, hint: `Give value ${value} a mark the die can draw, in overrides.` }
118
+ )
119
+ );
120
+ }
121
+ }
122
+ return out;
123
+ }
124
+ function builtinCanDraw(text, font) {
125
+ return font !== null || /^[0-9]+$/.test(text);
126
+ }
127
+ export {
128
+ FaceLayout,
129
+ FaceMarkSchema,
130
+ NUMBERS_LAYOUT,
131
+ PIPS_LAYOUT,
132
+ ambiguousLabel,
133
+ builtinCanDraw,
134
+ defaultLayoutFor,
135
+ layoutProblems,
136
+ resolveMark
137
+ };