aether-dice 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AetherDice contributors
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 ADDED
@@ -0,0 +1,174 @@
1
+ # AetherDice
2
+
3
+ A standalone, framework-agnostic **3D dice roller** for the browser. Renders
4
+ `d4 · d6 · d8 · d10 · d12 · d20 · d100` as a physics-driven, transparent canvas overlay
5
+ — and **settles each die on a value you supply**, so the visual always matches whatever
6
+ your app's logic already decided.
7
+
8
+ Built to be dropped into any web app (including no-build, vanilla-ESM ones) as a single
9
+ prebuilt module + assets.
10
+
11
+ > AetherDice is a **presentation layer**. It does not roll randomly or do dice math —
12
+ > your app owns the RNG and modifiers; AetherDice shows the given face values with a
13
+ > believable tumble.
14
+
15
+ ## Features
16
+
17
+ - All standard polyhedral dice + `d100` (as tens/units d10s, with percentile mode).
18
+ - **Deterministic results** — pass the exact value each die must land on.
19
+ - Physics-based tumble (Three.js + cannon-es) with a guaranteed final face.
20
+ - Mixed throws in one roll (e.g. `1d20 + 2d6`).
21
+ - Themeable with **real HDR edge bloom** — `aether` (glowing cyan default), `aether-neon`,
22
+ `aether-molten`, `classic`, or your own.
23
+ - Respects `prefers-reduced-motion`; instant/skip mode; WebGL-absent fallback signal.
24
+ - Ships as one ESM bundle + assets; no framework dependency.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ npm install aether-dice
30
+ ```
31
+
32
+ Or load directly (no build step) via CDN:
33
+
34
+ ```js
35
+ import { AetherDice } from 'https://cdn.jsdelivr.net/npm/aether-dice/dist/aether-dice.esm.js';
36
+ ```
37
+
38
+ ## Quick start
39
+
40
+ ```js
41
+ import { AetherDice } from 'aether-dice';
42
+
43
+ const dice = new AetherDice({
44
+ container: document.getElementById('dice-overlay'), // a positioned element
45
+ assetPath: '/assets/aether-dice/', // where textures/wasm live
46
+ theme: 'aether',
47
+ });
48
+
49
+ await dice.init();
50
+
51
+ // The outcome is decided by YOUR app; AetherDice just renders it:
52
+ const result = await dice.roll(
53
+ [{ sides: 20 }, { sides: 6 }, { sides: 6 }],
54
+ { results: [17, 4, 2] },
55
+ );
56
+ // → { dice: [{sides:20,value:17}, {sides:6,value:4}, {sides:6,value:2}], total: 23 }
57
+ ```
58
+
59
+ ## API
60
+
61
+ ### `new AetherDice(options)`
62
+
63
+ | Option | Type | Default | Description |
64
+ |---|---|---|---|
65
+ | `container` | `HTMLElement` | — | Element the canvas overlay mounts into (required). |
66
+ | `assetPath` | `string` | — | Base URL for textures/wasm assets. |
67
+ | `theme` | `string \| object` | `'aether'` | Built-in theme name or a theme object. |
68
+ | `scale` | `number` | `6` | Die size. |
69
+ | `gravity` | `number` | `40` | Physics gravity. |
70
+ | `throwForce` | `number` | `8` | Initial impulse strength. |
71
+ | `reducedMotion` | `boolean` | auto | `true` skips the tumble and shows the result instantly. Defaults to the user's `prefers-reduced-motion`. |
72
+ | `maxDice` | `number` | `20` | Safety cap on dice per roll. |
73
+
74
+ ### Methods
75
+
76
+ - `await dice.init()` — load the engine + assets; resolves when ready.
77
+ - `await dice.roll(dice, opts)` — render a roll. `dice` is a notation string
78
+ (`'2d8'`) or an array (`[{ sides: 20 }, …]`). `opts.results` is the array of values
79
+ each die must land on (**always pass this from a game**). `opts.instant` skips the
80
+ tumble for this call. Resolves to `{ dice: [{ sides, value }], total }`.
81
+ - `dice.clear()` — remove dice from the scene.
82
+ - `dice.destroy()` — dispose all GPU resources (call on unmount).
83
+ - `dice.on(event, handler)` / `dice.off(event, handler)`.
84
+
85
+ ### Events
86
+
87
+ | Event | Payload | When |
88
+ |---|---|---|
89
+ | `rollStart` | `{ dice }` | A roll begins. |
90
+ | `rollComplete` | `{ dice, total, text }` | Dice settle. `text` is the readable result (use it for logs / screen readers). |
91
+ | `webglUnavailable` | `{}` | WebGL can't initialize — fall back to a 2D presentation. |
92
+
93
+ ## Determinism (the important part)
94
+
95
+ If you omit `results`, AetherDice may roll randomly and return the landed values — handy
96
+ for demos. **In a real game, always pass `results`** so the rendered dice match your
97
+ authoritative roll. Internally the tumble runs with a random throw, then a corrective
98
+ rotation guarantees the requested face ends up. The result is *also* delivered as text
99
+ via `rollComplete`, so the visual is never the only source of truth.
100
+
101
+ ## Theming
102
+
103
+ Pass a built-in theme name or a theme object. A theme object is merged over the `aether`
104
+ default, so you only specify what you want to change:
105
+
106
+ ```js
107
+ new AetherDice({ theme: 'aether-neon' }); // built-in name
108
+
109
+ new AetherDice({ theme: { // custom (merged over aether)
110
+ dieColor: '#10131a', // body color
111
+ edgeGlow: '#39d0ff', // edge color (base hue of the glow)
112
+ numberColor: '#eaf6ff', // numeral color
113
+ numberFont: '600 220px "Trebuchet MS", system-ui, sans-serif',
114
+ metalness: 0.2,
115
+ roughness: 0.35,
116
+ opacity: 0.92, // <1 → translucent body
117
+ ambient: '#22303a', // ambient light color
118
+ keyLight: '#ffffff', // key (directional) light color
119
+ bloom: true, // enable the edge bloom post-process
120
+ edgeIntensity: 2.2, // HDR edge multiplier (>1 → brighter, punchier glow)
121
+ bloomStrength: 1.5, // overall bloom amount
122
+ bloomRadius: 0.6, // bloom spread
123
+ bloomThreshold:0, // luminance cutoff (0 = bloom all edge light)
124
+ }});
125
+ ```
126
+
127
+ ### Built-in themes
128
+
129
+ | Preview | Name | Look |
130
+ |---|---|---|
131
+ | <img src="https://raw.githubusercontent.com/BeastPhoenix/aether-dice/v0.1.0/docs/images/theme-aether.png" width="150" alt="aether theme"> | `aether` (default) | Dark dice, glowing cyan edges, light numerals. |
132
+ | <img src="https://raw.githubusercontent.com/BeastPhoenix/aether-dice/v0.1.0/docs/images/theme-aether-neon.png" width="150" alt="aether-neon theme"> | `aether-neon` | Deep violet body, searing magenta-purple edge glow. |
133
+ | <img src="https://raw.githubusercontent.com/BeastPhoenix/aether-dice/v0.1.0/docs/images/theme-aether-molten.png" width="150" alt="aether-molten theme"> | `aether-molten` | Dark ember body, hot orange edge glow, warm numerals. |
134
+ | <img src="https://raw.githubusercontent.com/BeastPhoenix/aether-dice/v0.1.0/docs/images/theme-classic.png" width="150" alt="classic theme"> | `classic` | Ivory/black, no glow. |
135
+
136
+ ### Edge bloom (the glow is real)
137
+
138
+ The edge glow is a true **bloom** post-process, not a flat outline. It uses *selective*
139
+ bloom — only the die edges glow, never the body or numerals — and the edge color is pushed
140
+ into **HDR** (`edgeIntensity` > 1) so the brightest part of each edge blows out to a
141
+ white-hot core with a saturated colored halo, like neon. Two knobs shape it:
142
+
143
+ - `edgeIntensity` — how over-bright the edge *source* is (character of the glow).
144
+ - `bloomStrength` / `bloomRadius` — how much the glow *spreads* (amount and softness).
145
+
146
+ Set `bloom: false` (as `classic` does) to skip the post-processing pipeline entirely — the
147
+ edges then render as a plain line, or are omitted if `edgeGlow` is transparent.
148
+
149
+ ## Supported dice
150
+
151
+ `d4, d6, d8, d10, d12, d20, d100`. The `d4` is read by its apex/bottom convention; `d100`
152
+ renders as a tens die (`00–90`) + a units die (`0–9`) and can combine to a percentile.
153
+
154
+ ## Development
155
+
156
+ ```bash
157
+ npm install
158
+ npm run dev # demo page with live reload
159
+ npm test # unit tests (value resolution, d4/d100, corrective orientation)
160
+ npm run build # emits dist/aether-dice.esm.js + assets/
161
+ ```
162
+
163
+ The `/demo` page rolls every die type, mixed throws, and lets you force a predetermined
164
+ result to verify determinism.
165
+
166
+ ## Distribution
167
+
168
+ Published to npm and consumable via jsDelivr. The public API above is the semver
169
+ stability contract. Ship `dist/aether-dice.esm.js` + `assets/`; consumers point
170
+ `assetPath` at the hosted assets.
171
+
172
+ ## License
173
+
174
+ MIT.
package/SPEC.md ADDED
@@ -0,0 +1,160 @@
1
+ # AetherDice — Technical Specification
2
+
3
+ A standalone, framework-agnostic **3D dice roller** for the browser, built to be
4
+ developed in its own GitHub repo and consumed by Chronicles of Aethermoor (and any
5
+ other web app) as a prebuilt ES module.
6
+
7
+ > **Suggested identity:** repo `aether-dice`, npm package `aether-dice`, default
8
+ > theme `aether` (glowing edges to match Aethermoor's sci-fi/fantasy look).
9
+
10
+ ---
11
+
12
+ ## 1. Purpose & non-goals
13
+
14
+ **Purpose:** render a physics-driven 3D dice roll as a transparent overlay, and
15
+ **settle each die on a caller-supplied value** (the consuming app owns the RNG and
16
+ math; the library only *shows* results).
17
+
18
+ **Non-goals:**
19
+ - No dice-notation math (no `+3` modifiers, no advantage logic). The app computes the
20
+ result; the library renders the given face values. This keeps the library a pure
21
+ presentation layer.
22
+ - No game logic, no UI chrome beyond the canvas.
23
+ - No framework coupling (no React/Vue dependency).
24
+
25
+ ---
26
+
27
+ ## 2. The critical requirement: deterministic results
28
+
29
+ This is the feature that justifies building it and the part most DIY dice fail.
30
+
31
+ - The caller passes the exact value(s) each die must end on: `roll(dice, { results })`.
32
+ - The library runs a physics tumble for *feel*, then **guarantees** the final upward
33
+ face matches the requested value.
34
+
35
+ **Recommended technique:** run the physics simulation with a randomized throw; when a
36
+ die comes to rest, read its landed top face; if it differs from the target, apply a
37
+ short corrective quaternion tween that rotates the die so the **target face's normal
38
+ points to world-up** (about the resting vertical axis to keep it natural). This yields
39
+ believable tumbling with a guaranteed outcome.
40
+
41
+ Each die geometry ships with a **face → value map** and per-face outward normals. Note
42
+ the **d4 special case**: a tetrahedron is read from the bottom/apex convention, not a
43
+ top face — handle its value resolution separately.
44
+
45
+ WebGL unavailable, or `reducedMotion`/`instant` requested → **skip the tumble** and
46
+ present the result immediately (or emit an event so the app can fall back to 2D).
47
+
48
+ ---
49
+
50
+ ## 3. Supported dice
51
+
52
+ `d4, d6, d8, d10, d12, d20`, plus `d100` (rendered as two d10s: a tens die `00–90` and
53
+ a units die `0–9`, with a percentile-combine mode). Mixed rolls in one throw are
54
+ supported (e.g. `1d20 + 2d6`).
55
+
56
+ ---
57
+
58
+ ## 4. Tech stack (recommended)
59
+
60
+ | Concern | Choice | Notes |
61
+ |---|---|---|
62
+ | Renderer | **Three.js** (ESM) | Well-documented, tree-shakeable. Babylon.js is a valid alternative. |
63
+ | Physics | **cannon-es** | Lightweight, ESM, maintained. (rapier-wasm if more accuracy is wanted.) |
64
+ | Build | **Vite** (library mode) or Rollup | Emits a single `dist/aether-dice.esm.js` + assets. |
65
+ | Geometry | Procedural polyhedra + UV-mapped number textures | Generate at build or runtime; cache. |
66
+
67
+ The library owns its bundler — that is the whole point of the separate repo. The game
68
+ never sees Three.js as a dependency; it loads one built ESM file.
69
+
70
+ ---
71
+
72
+ ## 5. Public API (framework-agnostic, promise-based)
73
+
74
+ ```js
75
+ import { AetherDice } from 'aether-dice';
76
+
77
+ const dice = new AetherDice({
78
+ container: document.getElementById('dice-overlay'), // a positioned element
79
+ assetPath: 'https://cdn.example/aether-dice/assets/',
80
+ theme: 'aether', // built-in theme name or a theme object
81
+ scale: 6, // die size
82
+ gravity: 40,
83
+ throwForce: 8,
84
+ reducedMotion:false, // true → no tumble, show result instantly
85
+ maxDice: 20, // safety cap
86
+ });
87
+
88
+ await dice.init(); // loads engine + assets; resolves when ready
89
+
90
+ // Render a roll whose outcome is already decided by the caller:
91
+ const result = await dice.roll(
92
+ [{ sides: 20 }, { sides: 6 }, { sides: 6 }],
93
+ { results: [17, 4, 2] } // each die settles on these faces, in order
94
+ );
95
+ // result === { dice: [{sides:20,value:17}, {sides:6,value:4}, {sides:6,value:2}], total: 23 }
96
+
97
+ dice.roll('2d8', { results: [5, 8] }); // dice-notation string form also accepted
98
+ dice.clear(); // remove dice from the scene
99
+ dice.on('rollStart' | 'rollComplete' | 'webglUnavailable', handler);
100
+ dice.destroy(); // dispose geometries, materials, renderer
101
+ ```
102
+
103
+ Behavioral contract:
104
+ - If `results` is omitted, the library may roll randomly (useful for demos) and return
105
+ the landed values — but the **consuming game must always pass `results`**.
106
+ - `instant: true` per-call, or global `reducedMotion`, skips animation.
107
+ - All GPU resources are disposed on `destroy()`; one renderer instance per `AetherDice`.
108
+
109
+ ---
110
+
111
+ ## 6. Theming
112
+
113
+ A theme defines: die material/color, edge glow, number (pip) color/font, environment
114
+ lighting, and optional custom textures (via `assetPath`). Ship at least:
115
+ - `aether` (default) — dark dice, glowing cyan edges, light numerals.
116
+ - `classic` — ivory/black for a traditional look.
117
+
118
+ Custom themes are plain objects passed to the constructor.
119
+
120
+ ---
121
+
122
+ ## 7. Accessibility & performance
123
+
124
+ - Honor `prefers-reduced-motion` automatically (overridable).
125
+ - Emit the textual result via `rollComplete` so the app can log it / announce it to
126
+ screen readers — **the visual is never the only channel for the result**.
127
+ - Pause/idle the render loop when no dice are active; cap dice count (`maxDice`).
128
+ - Lazy-initializable: `init()` is async so the consuming app can defer the heavy load
129
+ until 3D dice are actually enabled.
130
+
131
+ ---
132
+
133
+ ## 8. Distribution & versioning
134
+
135
+ - Publish to **npm** as `aether-dice` and/or attach a built bundle to GitHub releases
136
+ consumable via **jsDelivr** (e.g. `https://cdn.jsdelivr.net/npm/aether-dice/dist/aether-dice.esm.js`).
137
+ - Ship `dist/aether-dice.esm.js` (single file) + an `assets/` folder (textures, any
138
+ wasm). Document the `assetPath` the consumer must point at.
139
+ - Semantic versioning. The **public API in §5 is the stability contract**.
140
+
141
+ ---
142
+
143
+ ## 9. Deliverables (definition of done)
144
+
145
+ - The library + Vite/Rollup library-mode build emitting the ESM bundle + assets.
146
+ - A **standalone demo page** (`/demo`) rolling every die type and mixed throws, with a
147
+ field to force a predetermined result (proves §2).
148
+ - Unit tests for the value-resolution math (face→value, d4 convention, d100 combine)
149
+ and the corrective-orientation logic.
150
+ - Docs: `README.md` (install + the §5 API), and a `CLAUDE.md`/`AGENTS.md` for the repo.
151
+ - MIT license.
152
+
153
+ ## 10. Acceptance criteria
154
+
155
+ 1. Every die type renders and **always settles on the requested value** across 100+
156
+ randomized throws (automated check on the resolution logic).
157
+ 2. `reducedMotion`/`instant` and WebGL-absent paths present the result with no tumble.
158
+ 3. Consumable from a no-build host via a single ESM import + `assetPath` (proven by the
159
+ game's integration — see `INTEGRATION.md`).
160
+ 4. `destroy()` leaks no GPU resources (verify in a mount/unmount loop).