@aethervtt/shard-dice 0.0.0-stage → 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.
Files changed (80) 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/audio.d.ts +19 -0
  6. package/dist/audio.js +187 -0
  7. package/dist/builtins.d.ts +25 -0
  8. package/dist/builtins.js +335 -0
  9. package/dist/cells.d.ts +33 -0
  10. package/dist/cells.js +53 -0
  11. package/dist/components.d.ts +14 -0
  12. package/dist/components.js +27 -0
  13. package/dist/definition-asset.d.ts +19 -0
  14. package/dist/definition-asset.js +60 -0
  15. package/dist/definition.d.ts +98 -0
  16. package/dist/definition.js +320 -0
  17. package/dist/dice-worker.d.ts +1 -0
  18. package/dist/dice-worker.js +3 -0
  19. package/dist/effects.d.ts +99 -0
  20. package/dist/effects.js +200 -0
  21. package/dist/entrances.d.ts +48 -0
  22. package/dist/entrances.js +54 -0
  23. package/dist/families.d.ts +17 -0
  24. package/dist/families.js +131 -0
  25. package/dist/glyphs.d.ts +29 -0
  26. package/dist/glyphs.js +270 -0
  27. package/dist/golden.json +17 -0
  28. package/dist/hull.d.ts +32 -0
  29. package/dist/hull.js +154 -0
  30. package/dist/index.d.ts +32 -0
  31. package/dist/index.js +324 -0
  32. package/dist/landing.d.ts +29 -0
  33. package/dist/landing.js +93 -0
  34. package/dist/layout.d.ts +75 -0
  35. package/dist/layout.js +137 -0
  36. package/dist/marks.d.ts +54 -0
  37. package/dist/marks.js +543 -0
  38. package/dist/material.d.ts +80 -0
  39. package/dist/material.js +343 -0
  40. package/dist/math.d.ts +39 -0
  41. package/dist/math.js +136 -0
  42. package/dist/mesh.d.ts +9 -0
  43. package/dist/mesh.js +105 -0
  44. package/dist/methods.d.ts +2 -0
  45. package/dist/methods.js +42 -0
  46. package/dist/numerals.d.ts +9 -0
  47. package/dist/numerals.js +47 -0
  48. package/dist/plugin.d.ts +41 -0
  49. package/dist/plugin.js +176 -0
  50. package/dist/preview.d.ts +32 -0
  51. package/dist/preview.js +135 -0
  52. package/dist/quality.d.ts +12 -0
  53. package/dist/quality.js +18 -0
  54. package/dist/resources.d.ts +85 -0
  55. package/dist/resources.js +191 -0
  56. package/dist/roll.d.ts +68 -0
  57. package/dist/roll.js +97 -0
  58. package/dist/settle.d.ts +53 -0
  59. package/dist/settle.js +96 -0
  60. package/dist/skin.d.ts +64 -0
  61. package/dist/skin.js +313 -0
  62. package/dist/sound.d.ts +33 -0
  63. package/dist/sound.js +296 -0
  64. package/dist/studio.d.ts +3 -0
  65. package/dist/studio.js +48 -0
  66. package/dist/table.d.ts +227 -0
  67. package/dist/table.js +1818 -0
  68. package/dist/test-worker.d.ts +1 -0
  69. package/dist/test-worker.js +12 -0
  70. package/dist/track.d.ts +70 -0
  71. package/dist/track.js +304 -0
  72. package/dist/tray.d.ts +2 -0
  73. package/dist/tray.js +43 -0
  74. package/dist/variants.d.ts +9 -0
  75. package/dist/variants.js +46 -0
  76. package/dist/windows.d.ts +25 -0
  77. package/dist/windows.js +77 -0
  78. package/dist/worker.d.ts +6 -0
  79. package/dist/worker.js +9 -0
  80. package/package.json +47 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patrick Dias
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,3 @@
1
- # Temporary Holding Version
1
+ # @aethervtt/shard-dice
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Part of [Shard](https://github.com/aethervtt/shard), a WebGPU-first 2D/3D game engine in TypeScript. See the repository for documentation.
@@ -0,0 +1,79 @@
1
+ import type { AudioClipAsset, PlaySoundOptions } from '@aethervtt/shard-audio';
2
+ import { type AssetRef, type Entity, type World } from '@aethervtt/shard-core';
3
+ import type { ScreenEffectParams } from '@aethervtt/shard-render';
4
+ import type { DieKind } from './builtins.js';
5
+ import type { AccentCue } from './sound.js';
6
+ export declare const MAX_ATTACHMENTS = 4;
7
+ /**
8
+ * What the registries hold scenes to (0065): vertices an attachment or an entrance may declare, and
9
+ * an entrance's longest run. Module-wide, like the registries: set them before defining.
10
+ */
11
+ export declare const DICE_BUDGETS: {
12
+ attachmentVertices: number;
13
+ entranceVertices: number;
14
+ entranceMs: number;
15
+ };
16
+ /** Raises (or lowers) the budgets; call it before defining attachments and entrances. */
17
+ export declare function setDiceBudgets(budgets: Partial<typeof DICE_BUDGETS>): void;
18
+ /** What a host scene on a die (an attachment, or an entrance, 0065) can reach and do. */
19
+ export interface DiceSceneContext {
20
+ world: World;
21
+ /** The die: parent what you spawn to it (ChildOf) so it follows the die. */
22
+ die: Entity;
23
+ kind: DieKind;
24
+ value: number;
25
+ label: string;
26
+ /** The die's world scale. */
27
+ scale: number;
28
+ /** From the recipe effect that named the scene, so one scene serves many skins. */
29
+ params: Record<string, unknown>;
30
+ /** The dice camera: face it (spawnDiceWindow). */
31
+ camera: Entity;
32
+ /** From the roll and the die: variation that's the same for every viewer. */
33
+ seed: number;
34
+ /** The roll's sound gain, 0..1 (`sound` applies it). */
35
+ soundGain: number;
36
+ /** Where a world point (default: the die) is in the dice view's CSS pixels; null behind the camera. */
37
+ screen(point?: ArrayLike<number>): [number, number] | null;
38
+ /**
39
+ * Publishes (or refreshes) a lens field (0063) centered on the die, in the dice view's CSS
40
+ * pixels. False when the table doesn't allow fields (reduced motion, effects off, large pools)
41
+ * or 4 others are live.
42
+ */
43
+ lens(field: {
44
+ radius: number;
45
+ strength: number;
46
+ ttlMs?: number;
47
+ }): boolean;
48
+ /**
49
+ * Publishes (or refreshes) a screen effect (0065) of this kind, centered on the die (or `at`),
50
+ * for the host's table to draw (`fire`). False when the table doesn't allow effects or 8 are live.
51
+ */
52
+ effect(effect: {
53
+ kind: string;
54
+ radius: number;
55
+ ttlMs?: number;
56
+ params?: ScreenEffectParams;
57
+ at?: ArrayLike<number>;
58
+ }): boolean;
59
+ /** Plays a clip at the roll's sound gain (a range scales too); stopped when the roll ends. -1 without audio. */
60
+ sound(clip: AssetRef | AudioClipAsset, options?: PlaySoundOptions): number;
61
+ /** Plays one of the package's accent cues. */
62
+ cue(cue: AccentCue, gain?: number): void;
63
+ }
64
+ export type DiceAttachmentContext = DiceSceneContext;
65
+ export interface DiceAttachmentDef {
66
+ /** Vertices it draws, within `DICE_BUDGETS.attachmentVertices`: declared, and held to. */
67
+ vertices: number;
68
+ /** Spawns the scene. Returns what it spawned (the package despawns it). */
69
+ spawn(ctx: DiceAttachmentContext): Entity[];
70
+ /**
71
+ * Runs each frame while it's alive, with seconds since it spawned; frames keep coming while any
72
+ * attachment has one. Return true to keep it, false when it's done (it's despawned).
73
+ */
74
+ update?(ctx: DiceAttachmentContext, seconds: number, entities: readonly Entity[]): boolean;
75
+ }
76
+ /** Registers a scene recipes can attach to landed dice by name. */
77
+ export declare function defineDiceAttachment(name: string, def: DiceAttachmentDef): DiceAttachmentDef;
78
+ export declare function findDiceAttachment(name: string): DiceAttachmentDef | undefined;
79
+ export declare function allDiceAttachments(): string[];
@@ -0,0 +1,43 @@
1
+ import { ShardError } from "@aethervtt/shard-core";
2
+ const MAX_ATTACHMENTS = 4;
3
+ const DICE_BUDGETS = {
4
+ attachmentVertices: 12e3,
5
+ entranceVertices: 6e4,
6
+ entranceMs: 8e3
7
+ };
8
+ function setDiceBudgets(budgets) {
9
+ Object.assign(DICE_BUDGETS, budgets);
10
+ }
11
+ const attachments = /* @__PURE__ */ new Map();
12
+ function defineDiceAttachment(name, def) {
13
+ const budget = DICE_BUDGETS.attachmentVertices;
14
+ if (!(def.vertices > 0) || def.vertices > budget) {
15
+ throw new ShardError(
16
+ "dice/attachment-budget",
17
+ `Attachment "${name}" declares ${def.vertices} vertices; the budget is ${budget}`,
18
+ { hint: "Draw fewer vertices, split the effect, or raise it with setDiceBudgets first." }
19
+ );
20
+ }
21
+ const existing = attachments.get(name);
22
+ if (existing && existing !== def) {
23
+ throw new ShardError("dice/registry-conflict", `Attachment "${name}" is already defined`, {
24
+ hint: "Give the new attachment its own name."
25
+ });
26
+ }
27
+ attachments.set(name, def);
28
+ return def;
29
+ }
30
+ function findDiceAttachment(name) {
31
+ return attachments.get(name);
32
+ }
33
+ function allDiceAttachments() {
34
+ return [...attachments.keys()].sort();
35
+ }
36
+ export {
37
+ DICE_BUDGETS,
38
+ MAX_ATTACHMENTS,
39
+ allDiceAttachments,
40
+ defineDiceAttachment,
41
+ findDiceAttachment,
42
+ setDiceBudgets
43
+ };
@@ -0,0 +1,19 @@
1
+ import { type AssetRef, type World } from '@aethervtt/shard-core';
2
+ /** Default category for dice, including procedural fallback sounds. */
3
+ export declare const DICE_AUDIO_BUS = "dice";
4
+ /** Preserve host controls; a configured mixer must explicitly include its dice route. */
5
+ export declare function ensureDiceAudioBus(world: World): void;
6
+ /** A roll owns preparation and instances, independently of rendering and the host's storage. */
7
+ export declare class DiceAudioSession {
8
+ private readonly bindings;
9
+ private readonly leases;
10
+ private readonly instances;
11
+ private closed;
12
+ private readonly world;
13
+ private readonly gain;
14
+ constructor(world: World, gain: number);
15
+ prepare(refs: readonly AssetRef[]): Promise<void>;
16
+ /** Returns true for configured bindings, even when silent, so defaults do not also play. */
17
+ play(ref: AssetRef | null, seed: number, strength: number, pitch: number, value: number, high: boolean, low: boolean, count: number, contact?: number, gain?: number): boolean;
18
+ dispose(): void;
19
+ }
package/dist/audio.js ADDED
@@ -0,0 +1,187 @@
1
+ import {
2
+ AudioBuses,
3
+ AudioEvent,
4
+ AudioEventsState,
5
+ AudioMixState,
6
+ AudioState,
7
+ audioEvents,
8
+ playAudioEvent,
9
+ prepareAudio,
10
+ stopAudioEvent
11
+ } from "@aethervtt/shard-audio";
12
+ import { ShardError } from "@aethervtt/shard-core";
13
+ const DICE_AUDIO_BUS = "dice";
14
+ const INPUTS = ["strength", "pitch", "value", "high", "low", "count", "contact"];
15
+ function ensureDiceAudioBus(world) {
16
+ if (!world.hasResource(AudioState)) return;
17
+ const graph = world.tryResource(AudioMixState)?.graph;
18
+ if (graph && !graph.buses.some((bus) => bus.id === DICE_AUDIO_BUS && !bus.return))
19
+ throw new ShardError("dice/audio-bus", "The active mixer needs an ordinary dice bus", {
20
+ hint: "Add dice under master in the host mixer before rolling.",
21
+ path: "/buses"
22
+ });
23
+ const buses = world.resource(AudioBuses);
24
+ buses[DICE_AUDIO_BUS] ??= { volume: 1, muted: false, parent: "master" };
25
+ if (buses[DICE_AUDIO_BUS].parent !== "master" || buses[DICE_AUDIO_BUS].userDomain === false)
26
+ throw new ShardError(
27
+ "dice/audio-bus",
28
+ "Dice must be an ordinary category directly under master",
29
+ {
30
+ hint: "Independent dice user controls require their own category, including effect tails.",
31
+ path: "/buses/dice"
32
+ }
33
+ );
34
+ }
35
+ function key(ref) {
36
+ return ref.guid ?? ref.path ?? "";
37
+ }
38
+ class DiceAudioSession {
39
+ bindings = /* @__PURE__ */ new Map();
40
+ leases = [];
41
+ instances = [];
42
+ closed = false;
43
+ world;
44
+ gain;
45
+ constructor(world, gain) {
46
+ this.world = world;
47
+ this.gain = gain;
48
+ }
49
+ async prepare(refs) {
50
+ if (this.closed || this.gain <= 0 || !this.world.hasResource(AudioState)) return;
51
+ ensureDiceAudioBus(this.world);
52
+ if (refs.length && !this.world.hasResource(AudioEventsState))
53
+ throw new ShardError(
54
+ "dice/audio-events-missing",
55
+ "Authored dice sounds need audioEventsPlugin",
56
+ {
57
+ hint: "Install audioEventsPlugin alongside audioPlugin, or remove the event bindings."
58
+ }
59
+ );
60
+ try {
61
+ for (const ref of refs) {
62
+ if (this.closed) return;
63
+ if (this.bindings.has(key(ref))) continue;
64
+ const lease = await prepareAudio(this.world, ref);
65
+ if (this.closed) {
66
+ lease.release();
67
+ return;
68
+ }
69
+ this.leases.push(lease);
70
+ const plan = lease.events[0];
71
+ if (!plan || lease.events.length !== 1)
72
+ throw new ShardError("dice/audio-event", "Dice bindings require one audio event");
73
+ const definition = plan.definition;
74
+ if (definition.mode !== "oneshot" || definition.layers.some((layer) => layer.spatial))
75
+ throw new ShardError(
76
+ "dice/audio-event",
77
+ "Dice presentation events must be flat one-shots"
78
+ );
79
+ const buses = this.world.resource(AudioBuses);
80
+ for (const layer of definition.layers) {
81
+ let bus = layer.bus || definition.bus;
82
+ let depth = 0;
83
+ while (bus && bus !== DICE_AUDIO_BUS && depth++ < 64) bus = buses[bus]?.parent ?? "";
84
+ if (bus !== DICE_AUDIO_BUS)
85
+ throw new ShardError("dice/audio-bus", "Dice event layers must route through dice", {
86
+ path: `/layers/${layer.id}/bus`,
87
+ hint: "Set the event bus to dice or a child bus in the host mixer."
88
+ });
89
+ }
90
+ const inputs = [];
91
+ for (const parameter of definition.parameters) {
92
+ if (!INPUTS.includes(parameter.name)) continue;
93
+ if (parameter.readonly || parameter.kind === "label" || ["strength", "pitch"].includes(parameter.name) && parameter.kind !== "continuous")
94
+ throw new ShardError("dice/audio-parameter", `Invalid dice input ${parameter.name}`, {
95
+ path: `/parameters/${parameter.name}`,
96
+ hint: "Use writable numeric inputs; strength and pitch must be continuous."
97
+ });
98
+ inputs.push(parameter.name);
99
+ }
100
+ this.bindings.set(key(ref), { ref: plan.source ?? ref, inputs, revision: plan.revision });
101
+ }
102
+ } catch (error) {
103
+ this.dispose();
104
+ throw error;
105
+ }
106
+ }
107
+ /** Returns true for configured bindings, even when silent, so defaults do not also play. */
108
+ play(ref, seed, strength, pitch, value, high, low, count, contact = 0, gain = 1) {
109
+ if (!ref) return false;
110
+ if (this.closed || this.gain <= 0 || !this.world.hasResource(AudioState)) return true;
111
+ const binding = this.bindings.get(key(ref));
112
+ if (!binding)
113
+ throw new ShardError("dice/audio-not-prepared", "Prepare the dice event before playback");
114
+ const current = this.world.resource(AudioEvent.store).get(binding.ref);
115
+ if (!current || JSON.stringify(AudioEvent.serialize(current)) !== binding.revision)
116
+ throw new ShardError("dice/audio-changed", "A dice event changed during its roll", {
117
+ hint: "Start a new roll to prepare the changed event and validate its routing and inputs."
118
+ });
119
+ const parameters = {};
120
+ for (let i = 0; i < binding.inputs.length; i++) {
121
+ const input = binding.inputs[i];
122
+ switch (input) {
123
+ case "strength":
124
+ parameters[input] = strength;
125
+ break;
126
+ case "pitch":
127
+ parameters[input] = pitch;
128
+ break;
129
+ case "value":
130
+ parameters[input] = value;
131
+ break;
132
+ case "high":
133
+ parameters[input] = high ? 1 : 0;
134
+ break;
135
+ case "low":
136
+ parameters[input] = low ? 1 : 0;
137
+ break;
138
+ case "count":
139
+ parameters[input] = count;
140
+ break;
141
+ case "contact":
142
+ parameters[input] = contact;
143
+ break;
144
+ }
145
+ }
146
+ const state = audioEvents(this.world);
147
+ let keep = 0;
148
+ for (let i = 0; i < this.instances.length; i++) {
149
+ const id = this.instances[i];
150
+ for (let j = 0; j < state.instances.length; j++) {
151
+ const instance = state.instances[j];
152
+ if (instance?.id === id && instance.state !== "ended" && instance.state !== "failed") {
153
+ this.instances[keep++] = id;
154
+ break;
155
+ }
156
+ }
157
+ }
158
+ this.instances.length = keep;
159
+ try {
160
+ this.instances.push(
161
+ playAudioEvent(this.world, binding.ref, {
162
+ seed: seed >>> 0,
163
+ volume: this.gain * gain,
164
+ parameters
165
+ })
166
+ );
167
+ } catch (error) {
168
+ if (!(error instanceof ShardError) || error.code !== "audio/capacity") throw error;
169
+ }
170
+ return true;
171
+ }
172
+ dispose() {
173
+ if (this.closed) return;
174
+ this.closed = true;
175
+ if (this.world.hasResource(AudioEventsState))
176
+ for (const id of this.instances) stopAudioEvent(this.world, id, { release: 0 });
177
+ for (const lease of this.leases) lease.release();
178
+ this.instances.length = 0;
179
+ this.leases.length = 0;
180
+ this.bindings.clear();
181
+ }
182
+ }
183
+ export {
184
+ DICE_AUDIO_BUS,
185
+ DiceAudioSession,
186
+ ensureDiceAudioBus
187
+ };
@@ -0,0 +1,25 @@
1
+ import { type DieDefinition } from './definition.js';
2
+ export declare const D4: DieDefinition;
3
+ export declare const D6: DieDefinition;
4
+ export declare const D8: DieDefinition;
5
+ /** A d10 printing 1–9 and 0 (the value 10). Opposite digits sum to 9, so it doesn't opt in. */
6
+ export declare const D10: DieDefinition;
7
+ /** A percentile's tens die: the d10's shape and numbering, printing 10–90 and 00 (the value 10). */
8
+ export declare const D10_TENS: DieDefinition;
9
+ export declare const D12: DieDefinition;
10
+ export declare const D20: DieDefinition;
11
+ export declare const D100_BALL: DieDefinition;
12
+ export declare const BUILTIN_DICE: readonly DieDefinition[];
13
+ /** Registers the built-ins (idempotent). The dice plugin calls it; tests and tools can too. */
14
+ export declare function registerBuiltinDice(): void;
15
+ /** The kinds a roll names, and the definitions each plays (percentile plays two). */
16
+ export declare const DIE_KINDS: readonly ['d4', 'd6', 'd8', 'd10', 'd12', 'd20', 'd100', 'percentile'];
17
+ export type DieKind = (typeof DIE_KINDS)[number];
18
+ export declare const KIND_DEFINITIONS: Readonly<Record<DieKind, readonly string[]>>;
19
+ /** The values a kind can show: percentile 1..100 over its two dice. */
20
+ export declare function kindSides(kind: DieKind): number;
21
+ /**
22
+ * A percentile value as its two dice's values: tens then units. 100 shows 00 and 0, and 7 shows 00
23
+ * and 7. Each d10 value 10 prints as its zero ('00' on the tens die, '0' on the units die).
24
+ */
25
+ export declare function percentileValues(value: number): [number, number];