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 +21 -0
- package/README.md +174 -0
- package/SPEC.md +160 -0
- package/dist/aether-dice.esm.js +19565 -0
- package/dist/aether-dice.esm.js.map +1 -0
- package/package.json +59 -0
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).
|