@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.
- package/LICENSE +21 -0
- package/README.md +2 -2
- package/dist/attachments.d.ts +79 -0
- package/dist/attachments.js +43 -0
- package/dist/audio.d.ts +19 -0
- package/dist/audio.js +187 -0
- package/dist/builtins.d.ts +25 -0
- package/dist/builtins.js +335 -0
- package/dist/cells.d.ts +33 -0
- package/dist/cells.js +53 -0
- package/dist/components.d.ts +14 -0
- package/dist/components.js +27 -0
- package/dist/definition-asset.d.ts +19 -0
- package/dist/definition-asset.js +60 -0
- package/dist/definition.d.ts +98 -0
- package/dist/definition.js +320 -0
- package/dist/dice-worker.d.ts +1 -0
- package/dist/dice-worker.js +3 -0
- package/dist/effects.d.ts +99 -0
- package/dist/effects.js +200 -0
- package/dist/entrances.d.ts +48 -0
- package/dist/entrances.js +54 -0
- package/dist/families.d.ts +17 -0
- package/dist/families.js +131 -0
- package/dist/glyphs.d.ts +29 -0
- package/dist/glyphs.js +270 -0
- package/dist/golden.json +17 -0
- package/dist/hull.d.ts +32 -0
- package/dist/hull.js +154 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +324 -0
- package/dist/landing.d.ts +29 -0
- package/dist/landing.js +93 -0
- package/dist/layout.d.ts +75 -0
- package/dist/layout.js +137 -0
- package/dist/marks.d.ts +54 -0
- package/dist/marks.js +543 -0
- package/dist/material.d.ts +80 -0
- package/dist/material.js +343 -0
- package/dist/math.d.ts +39 -0
- package/dist/math.js +136 -0
- package/dist/mesh.d.ts +9 -0
- package/dist/mesh.js +105 -0
- package/dist/methods.d.ts +2 -0
- package/dist/methods.js +42 -0
- package/dist/numerals.d.ts +9 -0
- package/dist/numerals.js +47 -0
- package/dist/plugin.d.ts +41 -0
- package/dist/plugin.js +176 -0
- package/dist/preview.d.ts +32 -0
- package/dist/preview.js +135 -0
- package/dist/quality.d.ts +12 -0
- package/dist/quality.js +18 -0
- package/dist/resources.d.ts +85 -0
- package/dist/resources.js +191 -0
- package/dist/roll.d.ts +68 -0
- package/dist/roll.js +97 -0
- package/dist/settle.d.ts +53 -0
- package/dist/settle.js +96 -0
- package/dist/skin.d.ts +64 -0
- package/dist/skin.js +313 -0
- package/dist/sound.d.ts +33 -0
- package/dist/sound.js +296 -0
- package/dist/studio.d.ts +3 -0
- package/dist/studio.js +48 -0
- package/dist/table.d.ts +227 -0
- package/dist/table.js +1818 -0
- package/dist/test-worker.d.ts +1 -0
- package/dist/test-worker.js +12 -0
- package/dist/track.d.ts +70 -0
- package/dist/track.js +304 -0
- package/dist/tray.d.ts +2 -0
- package/dist/tray.js +43 -0
- package/dist/variants.d.ts +9 -0
- package/dist/variants.js +46 -0
- package/dist/windows.d.ts +25 -0
- package/dist/windows.js +77 -0
- package/dist/worker.d.ts +6 -0
- package/dist/worker.js +9 -0
- package/package.json +47 -4
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { GpuAssetsResource, MaterialAsset, Materials, Meshes } from "@aethervtt/shard-render";
|
|
2
|
+
import { Texture, Textures } from "@aethervtt/shard-texture";
|
|
3
|
+
import { bakeMarks } from "./marks.js";
|
|
4
|
+
import { findDiceFamily } from "./material.js";
|
|
5
|
+
import { dieMesh } from "./mesh.js";
|
|
6
|
+
import { skinVariant } from "./skin.js";
|
|
7
|
+
const RELEASE_AFTER_MS = 8e3;
|
|
8
|
+
const lookKey = (l) => `${l.blended ? "b" : "o"}${l.dropped ? "d" : ""}${l.entrance === void 0 ? "" : `@${l.entrance}`}`;
|
|
9
|
+
class DiceResources {
|
|
10
|
+
entries = /* @__PURE__ */ new Map();
|
|
11
|
+
world;
|
|
12
|
+
/** The earliest release due (Infinity when none): checked every frame, so kept, not searched. */
|
|
13
|
+
due = Number.POSITIVE_INFINITY;
|
|
14
|
+
constructor(world) {
|
|
15
|
+
this.world = world;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The resources for one kind of die in one skin, made on first use. Each acquire needs a
|
|
19
|
+
* release. Throws `dice/unknown-family` for a skin whose family isn't defined.
|
|
20
|
+
*/
|
|
21
|
+
acquire(geometry, skinKey, skin, kind, layouts, bake) {
|
|
22
|
+
const key = `${geometry.hash.toString(16)}|${skinKey}|${kind}`;
|
|
23
|
+
let entry = this.entries.get(key);
|
|
24
|
+
if (!entry) {
|
|
25
|
+
const family = findDiceFamily(skin.family);
|
|
26
|
+
if (!family) throw new Error(`unknown family ${skin.family}`);
|
|
27
|
+
const variant = skinVariant(skin, kind, geometry.definition.id, layouts);
|
|
28
|
+
const atlas = bakeMarks(geometry, variant.layout, bake);
|
|
29
|
+
const texture = this.world.resource(Textures).add(
|
|
30
|
+
Texture.create({
|
|
31
|
+
width: atlas.width,
|
|
32
|
+
height: atlas.height,
|
|
33
|
+
format: "rgba8unorm",
|
|
34
|
+
usage: "data",
|
|
35
|
+
mips: [atlas.pixels],
|
|
36
|
+
cpu: true
|
|
37
|
+
}),
|
|
38
|
+
`dice:marks/${geometry.definition.id}`
|
|
39
|
+
);
|
|
40
|
+
const mesh = this.world.resource(Meshes).add(
|
|
41
|
+
dieMesh(geometry, variant.bevel),
|
|
42
|
+
`dice:mesh/${geometry.definition.id}`
|
|
43
|
+
);
|
|
44
|
+
entry = {
|
|
45
|
+
key,
|
|
46
|
+
geometry,
|
|
47
|
+
kind,
|
|
48
|
+
family,
|
|
49
|
+
refs: 0,
|
|
50
|
+
releaseAt: Number.POSITIVE_INFINITY,
|
|
51
|
+
mesh,
|
|
52
|
+
atlas,
|
|
53
|
+
texture,
|
|
54
|
+
params: variant.params,
|
|
55
|
+
materials: /* @__PURE__ */ new Map(),
|
|
56
|
+
own: /* @__PURE__ */ new Map()
|
|
57
|
+
};
|
|
58
|
+
this.entries.set(key, entry);
|
|
59
|
+
}
|
|
60
|
+
entry.refs++;
|
|
61
|
+
entry.releaseAt = Number.POSITIVE_INFINITY;
|
|
62
|
+
return entry;
|
|
63
|
+
}
|
|
64
|
+
/** The entry's material for a look, made on first use. */
|
|
65
|
+
material(entry, look) {
|
|
66
|
+
const k = lookKey(look);
|
|
67
|
+
const store = look.entrance === void 0 ? entry.materials : entry.own;
|
|
68
|
+
const existing = store.get(k);
|
|
69
|
+
if (existing) return existing;
|
|
70
|
+
const family = entry.family;
|
|
71
|
+
const schema = family.type.schema;
|
|
72
|
+
const params = {};
|
|
73
|
+
for (const [name, v] of Object.entries(entry.params))
|
|
74
|
+
if (name in schema.fields) params[name] = v;
|
|
75
|
+
const value = schema.deserialize(params);
|
|
76
|
+
value.marks = entry.texture;
|
|
77
|
+
value.dropped = look.dropped ? 1 : 0;
|
|
78
|
+
value.fade = 1;
|
|
79
|
+
value.result = 0;
|
|
80
|
+
if (look.blended && !family.type.blend) value.alphaMode = "premultiplied";
|
|
81
|
+
const ref = this.world.resource(Materials).add(
|
|
82
|
+
new MaterialAsset(value, family.type),
|
|
83
|
+
`dice:material/${entry.key}/${k}`
|
|
84
|
+
);
|
|
85
|
+
store.set(k, ref);
|
|
86
|
+
return ref;
|
|
87
|
+
}
|
|
88
|
+
/** Whether dice of this entry need blending in their normal look (glass, see-through resin). */
|
|
89
|
+
blendedByNature(entry) {
|
|
90
|
+
return this.fixedBlend(entry) || (entry.family.translucent?.(entry.params) ?? false);
|
|
91
|
+
}
|
|
92
|
+
/** Whether the family's type fixes a blend: its dice blend in every tier, large pools too. */
|
|
93
|
+
fixedBlend(entry) {
|
|
94
|
+
const blend = entry.family.type.blend;
|
|
95
|
+
return blend === "alpha" || blend === "premultiplied" || blend === "additive";
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Sets a field on every material of these entries (result, its time, fade); entrance dice's own
|
|
99
|
+
* (in `own`) follow their own landing.
|
|
100
|
+
*/
|
|
101
|
+
setAll(entries, field, value) {
|
|
102
|
+
const materials = this.world.resource(Materials);
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
for (const ref of entry.materials.values()) {
|
|
105
|
+
const m = materials.get(ref);
|
|
106
|
+
if (m && m.value[field] !== value) m.set({ [field]: value });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
release(entry, now) {
|
|
111
|
+
entry.refs = Math.max(0, entry.refs - 1);
|
|
112
|
+
if (entry.refs === 0) {
|
|
113
|
+
entry.releaseAt = now + RELEASE_AFTER_MS;
|
|
114
|
+
this.due = Math.min(this.due, entry.releaseAt);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** The earliest release due, or Infinity. */
|
|
118
|
+
nextRelease() {
|
|
119
|
+
return this.due;
|
|
120
|
+
}
|
|
121
|
+
/** Frees entries unused for 8 s. Returns how many. */
|
|
122
|
+
sweep(now) {
|
|
123
|
+
let n = 0;
|
|
124
|
+
this.due = Number.POSITIVE_INFINITY;
|
|
125
|
+
for (const entry of [...this.entries.values()]) {
|
|
126
|
+
if (entry.refs > 0) continue;
|
|
127
|
+
if (entry.releaseAt > now) {
|
|
128
|
+
this.due = Math.min(this.due, entry.releaseAt);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
this.free(entry);
|
|
132
|
+
n++;
|
|
133
|
+
}
|
|
134
|
+
return n;
|
|
135
|
+
}
|
|
136
|
+
free(entry) {
|
|
137
|
+
const world = this.world;
|
|
138
|
+
const gpu = world.tryResource(GpuAssetsResource);
|
|
139
|
+
const meshes = world.resource(Meshes);
|
|
140
|
+
const materials = world.resource(Materials);
|
|
141
|
+
const textures = world.resource(Textures);
|
|
142
|
+
const mesh = meshes.get(entry.mesh);
|
|
143
|
+
if (mesh) gpu?.releaseMesh(mesh);
|
|
144
|
+
meshes.delete(entry.mesh.guid);
|
|
145
|
+
for (const ref of [...entry.materials.values(), ...entry.own.values()]) {
|
|
146
|
+
const m = materials.get(ref);
|
|
147
|
+
if (m) gpu?.releaseMaterial(m);
|
|
148
|
+
materials.delete(ref.guid);
|
|
149
|
+
}
|
|
150
|
+
const texture = textures.get(entry.texture);
|
|
151
|
+
if (texture) gpu?.releaseTexture(texture);
|
|
152
|
+
textures.delete(entry.texture.guid);
|
|
153
|
+
this.entries.delete(entry.key);
|
|
154
|
+
}
|
|
155
|
+
/** Frees everything, used or not (teardown). */
|
|
156
|
+
dispose() {
|
|
157
|
+
for (const entry of [...this.entries.values()]) this.free(entry);
|
|
158
|
+
this.due = Number.POSITIVE_INFINITY;
|
|
159
|
+
}
|
|
160
|
+
stats() {
|
|
161
|
+
let used = 0;
|
|
162
|
+
let materials = 0;
|
|
163
|
+
for (const e of this.entries.values()) {
|
|
164
|
+
if (e.refs > 0) used++;
|
|
165
|
+
materials += e.materials.size + e.own.size;
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
entries: this.entries.size,
|
|
169
|
+
used,
|
|
170
|
+
meshes: this.entries.size,
|
|
171
|
+
atlases: this.entries.size,
|
|
172
|
+
materials
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
describe() {
|
|
176
|
+
return [...this.entries.values()].map((e) => ({
|
|
177
|
+
key: e.key,
|
|
178
|
+
die: e.geometry.definition.id,
|
|
179
|
+
kind: e.kind,
|
|
180
|
+
family: e.family.name,
|
|
181
|
+
refs: e.refs,
|
|
182
|
+
releaseInMs: e.refs > 0 ? null : e.releaseAt,
|
|
183
|
+
materials: [...e.materials.keys(), ...e.own.keys()],
|
|
184
|
+
atlas: [e.atlas.width, e.atlas.height]
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export {
|
|
189
|
+
DiceResources,
|
|
190
|
+
RELEASE_AFTER_MS
|
|
191
|
+
};
|
package/dist/roll.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type AssetRef } from '@aethervtt/shard-core';
|
|
2
|
+
import { type DieKind } from './builtins.js';
|
|
3
|
+
import type { DiceTray } from './settle.js';
|
|
4
|
+
import { type DiceTrackRequest, type LaunchEdge } from './track.js';
|
|
5
|
+
/** A roll the host decided: what to show, never what to decide (0054). */
|
|
6
|
+
export interface DiceRoll {
|
|
7
|
+
/** The host's entry id; also the default seed. */
|
|
8
|
+
id: string;
|
|
9
|
+
dice: DiceRollDie[];
|
|
10
|
+
total?: number;
|
|
11
|
+
source?: {
|
|
12
|
+
mine: boolean;
|
|
13
|
+
gm: boolean;
|
|
14
|
+
};
|
|
15
|
+
/** Host semantic tags, at most 8, for effect conditions. */
|
|
16
|
+
tags?: string[];
|
|
17
|
+
seed?: string;
|
|
18
|
+
/** A fixed tray every viewer shares; omitted, the tray follows this viewport. */
|
|
19
|
+
tray?: DiceTray;
|
|
20
|
+
/** The tray edge the dice come in from; the seed picks one when omitted. */
|
|
21
|
+
launch?: LaunchEdge;
|
|
22
|
+
quality?: DiceQualityPreference;
|
|
23
|
+
motion?: 'full' | 'reduced';
|
|
24
|
+
effects?: boolean;
|
|
25
|
+
/** 0..1. */
|
|
26
|
+
soundGain?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface DiceRollDie {
|
|
29
|
+
kind: DieKind;
|
|
30
|
+
value: number;
|
|
31
|
+
/** A DiceSkin. */
|
|
32
|
+
skin: AssetRef<'dice/DiceSkin'> | AssetRef;
|
|
33
|
+
dropped?: boolean;
|
|
34
|
+
high?: boolean;
|
|
35
|
+
low?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export type DiceQualityPreference = 'auto' | 'full' | 'balanced' | 'large-pool';
|
|
38
|
+
/** One body on the table: a roll's die, or half of a percentile. */
|
|
39
|
+
export interface PhysicalDie {
|
|
40
|
+
/** Index into `roll.dice`. */
|
|
41
|
+
source: number;
|
|
42
|
+
kind: DieKind;
|
|
43
|
+
definition: string;
|
|
44
|
+
/** The definition's value to show (a percentile's tens die shows 1..10). */
|
|
45
|
+
value: number;
|
|
46
|
+
/** What the host rolled for the roll die (a percentile's 1..100). */
|
|
47
|
+
rolled: number;
|
|
48
|
+
/** The printed mark on top. */
|
|
49
|
+
label: string;
|
|
50
|
+
part: 'tens' | 'units' | undefined;
|
|
51
|
+
skin: AssetRef;
|
|
52
|
+
dropped: boolean;
|
|
53
|
+
high: boolean;
|
|
54
|
+
low: boolean;
|
|
55
|
+
/** World scale of the unit-radius die. */
|
|
56
|
+
scale: number;
|
|
57
|
+
}
|
|
58
|
+
/** Presentation scale by the number of bodies, as Aether steps it. */
|
|
59
|
+
export declare function presentationScale(count: number): number;
|
|
60
|
+
/** The tray that follows a viewport: as wide as its aspect allows, 3.15 deep (Aether's). */
|
|
61
|
+
export declare function viewportTray(aspect: number): DiceTray;
|
|
62
|
+
/**
|
|
63
|
+
* The bodies a roll puts on the table, in order: percentile plays a tens and a units d10. Values
|
|
64
|
+
* the dice can't show fail with `dice/invalid-value` naming the die; nothing is played.
|
|
65
|
+
*/
|
|
66
|
+
export declare function expandRoll(roll: DiceRoll, kinds?: Readonly<Record<DieKind, readonly string[]>>): PhysicalDie[];
|
|
67
|
+
/** The track request of a roll's bodies: its seed (default the id), tray, and dice. */
|
|
68
|
+
export declare function rollTrackRequest(roll: DiceRoll, dice: readonly PhysicalDie[], tray: DiceTray): DiceTrackRequest;
|
package/dist/roll.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
import { DIE_KINDS, KIND_DEFINITIONS, kindSides, percentileValues } from "./builtins.js";
|
|
3
|
+
import { dieGeometry, frameOf, labelOf, physicalScale, requireDie } from "./definition.js";
|
|
4
|
+
import { MAX_DICE } from "./track.js";
|
|
5
|
+
const MAX_TAGS = 8;
|
|
6
|
+
function invalidValue(message, path) {
|
|
7
|
+
return new ShardError("dice/invalid-value", message, {
|
|
8
|
+
path,
|
|
9
|
+
hint: "The host decides the result; pass one the die can show."
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function presentationScale(count) {
|
|
13
|
+
if (count <= 2) return 0.68;
|
|
14
|
+
if (count <= 8) return 0.56;
|
|
15
|
+
if (count <= 16) return 0.46;
|
|
16
|
+
return 0.38;
|
|
17
|
+
}
|
|
18
|
+
function viewportTray(aspect) {
|
|
19
|
+
const a = Number.isFinite(aspect) && aspect > 0 ? aspect : 16 / 9;
|
|
20
|
+
return { halfWidth: Math.max(2.35, Math.min(5.4, 3.15 * a)), halfDepth: 3.15 };
|
|
21
|
+
}
|
|
22
|
+
function expandRoll(roll, kinds = KIND_DEFINITIONS) {
|
|
23
|
+
if (typeof roll.id !== "string" || roll.id === "") {
|
|
24
|
+
throw new ShardError("dice/invalid-roll", "A roll needs an id", { path: "id" });
|
|
25
|
+
}
|
|
26
|
+
if ((roll.tags?.length ?? 0) > MAX_TAGS) {
|
|
27
|
+
throw new ShardError("dice/invalid-roll", `A roll has at most ${MAX_TAGS} tags`, {
|
|
28
|
+
path: "tags"
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const out = [];
|
|
32
|
+
roll.dice.forEach((die, i) => {
|
|
33
|
+
const path = `dice[${i}]`;
|
|
34
|
+
if (!DIE_KINDS.includes(die.kind)) {
|
|
35
|
+
throw new ShardError("dice/invalid-roll", `${path}.kind "${die.kind}" isn't a die kind`, {
|
|
36
|
+
path: `${path}.kind`,
|
|
37
|
+
hint: `Kinds: ${DIE_KINDS.join(", ")}.`
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const sides = kindSides(die.kind);
|
|
41
|
+
if (!Number.isInteger(die.value) || die.value < 1 || die.value > sides) {
|
|
42
|
+
throw invalidValue(
|
|
43
|
+
`${path} is a ${die.kind} showing ${die.value}; it shows 1 to ${sides}`,
|
|
44
|
+
`${path}.value`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const defs = kinds[die.kind];
|
|
48
|
+
const values = die.kind === "percentile" ? percentileValues(die.value) : [die.value];
|
|
49
|
+
defs.forEach((id, k) => {
|
|
50
|
+
const def = requireDie(id);
|
|
51
|
+
const value = values[k] ?? die.value;
|
|
52
|
+
frameOf(dieGeometry(def), value);
|
|
53
|
+
out.push({
|
|
54
|
+
source: i,
|
|
55
|
+
kind: die.kind,
|
|
56
|
+
definition: id,
|
|
57
|
+
value,
|
|
58
|
+
rolled: die.value,
|
|
59
|
+
label: labelOf(def, value),
|
|
60
|
+
part: die.kind === "percentile" ? k === 0 ? "tens" : "units" : void 0,
|
|
61
|
+
skin: die.skin,
|
|
62
|
+
dropped: die.dropped ?? false,
|
|
63
|
+
high: die.high ?? false,
|
|
64
|
+
low: die.low ?? false,
|
|
65
|
+
scale: 0
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
if (out.length === 0 || out.length > MAX_DICE) {
|
|
70
|
+
throw new ShardError(
|
|
71
|
+
"dice/invalid-roll",
|
|
72
|
+
`A roll puts 1 to ${MAX_DICE} dice on the table, not ${out.length}`,
|
|
73
|
+
{
|
|
74
|
+
path: "dice"
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const base = presentationScale(out.length);
|
|
79
|
+
const reference = dieGeometry(requireDie("d20"));
|
|
80
|
+
for (const d of out)
|
|
81
|
+
d.scale = base * physicalScale(dieGeometry(requireDie(d.definition)), reference);
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
function rollTrackRequest(roll, dice, tray) {
|
|
85
|
+
return {
|
|
86
|
+
seed: roll.seed ?? roll.id,
|
|
87
|
+
tray: { halfWidth: tray.halfWidth, halfDepth: tray.halfDepth },
|
|
88
|
+
dice: dice.map((d) => ({ definition: d.definition, scale: d.scale, dropped: d.dropped })),
|
|
89
|
+
launch: roll.launch
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export {
|
|
93
|
+
expandRoll,
|
|
94
|
+
presentationScale,
|
|
95
|
+
rollTrackRequest,
|
|
96
|
+
viewportTray
|
|
97
|
+
};
|
package/dist/settle.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { SettleRule } from '@aethervtt/shard-physics/track';
|
|
2
|
+
export interface DiceTray {
|
|
3
|
+
/** Half the tray's width (x) and depth (z), in world units. */
|
|
4
|
+
halfWidth: number;
|
|
5
|
+
halfDepth: number;
|
|
6
|
+
}
|
|
7
|
+
/** What `dice-settle` needs about each body. */
|
|
8
|
+
export interface DiceSettleParams {
|
|
9
|
+
tray: DiceTray;
|
|
10
|
+
bodies: {
|
|
11
|
+
/** Unit directions (x y z each) whose value reads when up: face normals, or the d4's vertices. */
|
|
12
|
+
directions: number[];
|
|
13
|
+
/** Collider points, scaled (hulls), for floor clearance. */
|
|
14
|
+
points?: number[];
|
|
15
|
+
/** Ball radius. */
|
|
16
|
+
radius?: number;
|
|
17
|
+
}[];
|
|
18
|
+
}
|
|
19
|
+
export declare const DICE_SETTLE_RULE = "dice-settle";
|
|
20
|
+
/** The step a track still not settled runs its one cleanup phase. */
|
|
21
|
+
export declare const DICE_CLEANUP_STEP = 240;
|
|
22
|
+
/** A ball's damping after its first bounce: angular ramps up over `steps`, linear jumps. */
|
|
23
|
+
export declare const BALL_DAMPING: {
|
|
24
|
+
from: number;
|
|
25
|
+
to: number;
|
|
26
|
+
steps: number;
|
|
27
|
+
linear: number;
|
|
28
|
+
};
|
|
29
|
+
/** Collision groups: dice touch dice, walls and the floor; walls and floor touch dice. */
|
|
30
|
+
export declare const DICE_GROUPS: {
|
|
31
|
+
dice: {
|
|
32
|
+
layers: number;
|
|
33
|
+
mask: number;
|
|
34
|
+
};
|
|
35
|
+
walls: {
|
|
36
|
+
layers: number;
|
|
37
|
+
mask: number;
|
|
38
|
+
};
|
|
39
|
+
floor: {
|
|
40
|
+
layers: number;
|
|
41
|
+
mask: number;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
/** Outside the tray for good: below the floor, or past a wall. */
|
|
45
|
+
export declare function outsideTray(p: ArrayLike<number>, tray: DiceTray): boolean;
|
|
46
|
+
/** A body at rest reading a value: flat (a face, or the d4's vertex, up) and on the floor. */
|
|
47
|
+
export declare function restingFlat(body: DiceSettleParams['bodies'][number], pos: ArrayLike<number>, rot: ArrayLike<number>): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Done once every die in the tray sleeps resting flat (dice that left the tray are the placed
|
|
50
|
+
* fallback's). Every body asleep with one cocked, or step 240 unsettled, runs the cleanup phase
|
|
51
|
+
* once. A ball's angular damping ramps up after its first bounce, so it settles when the others do.
|
|
52
|
+
*/
|
|
53
|
+
export declare const diceSettle: SettleRule;
|
package/dist/settle.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const DICE_SETTLE_RULE = "dice-settle";
|
|
2
|
+
const DICE_CLEANUP_STEP = 240;
|
|
3
|
+
const FLAT_ALIGNMENT = 0.995;
|
|
4
|
+
const FLOOR_CLEARANCE = 0.035;
|
|
5
|
+
const MIN_SETTLE_STEP = 45;
|
|
6
|
+
const BALL_DAMPING = { from: 0.2, to: 8, steps: 20, linear: 2.4 };
|
|
7
|
+
const DICE_GROUPS = {
|
|
8
|
+
dice: { layers: 1, mask: 1 | 2 | 4 },
|
|
9
|
+
walls: { layers: 2, mask: 1 },
|
|
10
|
+
floor: { layers: 4, mask: 1 }
|
|
11
|
+
};
|
|
12
|
+
const CLEANUP = {
|
|
13
|
+
groups: { dice: { layers: 1, mask: 4 } },
|
|
14
|
+
disableGroups: ["walls"],
|
|
15
|
+
wake: true
|
|
16
|
+
};
|
|
17
|
+
function outsideTray(p, tray) {
|
|
18
|
+
return p[1] < -0.5 || Math.abs(p[0]) > tray.halfWidth + 0.3 || Math.abs(p[2]) > tray.halfDepth + 0.3;
|
|
19
|
+
}
|
|
20
|
+
function rotatedY(q, x, y, z) {
|
|
21
|
+
const qx = q[0];
|
|
22
|
+
const qy = q[1];
|
|
23
|
+
const qz = q[2];
|
|
24
|
+
const qw = q[3];
|
|
25
|
+
const tx = 2 * (qy * z - qz * y);
|
|
26
|
+
const tz = 2 * (qx * y - qy * x);
|
|
27
|
+
return y + qw * (2 * (qz * x - qx * z)) + (qz * tx - qx * tz);
|
|
28
|
+
}
|
|
29
|
+
function restingFlat(body, pos, rot) {
|
|
30
|
+
if (body.radius !== void 0) return pos[1] - body.radius <= FLOOR_CLEARANCE;
|
|
31
|
+
let up = Number.NEGATIVE_INFINITY;
|
|
32
|
+
const d = body.directions;
|
|
33
|
+
for (let i = 0; i < d.length; i += 3)
|
|
34
|
+
up = Math.max(up, rotatedY(rot, d[i], d[i + 1], d[i + 2]));
|
|
35
|
+
if (up < FLAT_ALIGNMENT) return false;
|
|
36
|
+
let low = Number.POSITIVE_INFINITY;
|
|
37
|
+
const p = body.points;
|
|
38
|
+
for (let i = 0; i < p.length; i += 3) {
|
|
39
|
+
low = Math.min(low, rotatedY(rot, p[i], p[i + 1], p[i + 2]));
|
|
40
|
+
}
|
|
41
|
+
return pos[1] + low <= FLOOR_CLEARANCE;
|
|
42
|
+
}
|
|
43
|
+
const diceSettle = (raw) => {
|
|
44
|
+
const params = raw;
|
|
45
|
+
const n = params.bodies.length;
|
|
46
|
+
const pos = new Float64Array(3);
|
|
47
|
+
const rot = new Float64Array(4);
|
|
48
|
+
const bounced = new Uint8Array(n);
|
|
49
|
+
const damping = new Float64Array(n);
|
|
50
|
+
let cleaned = false;
|
|
51
|
+
return (view) => {
|
|
52
|
+
let phase;
|
|
53
|
+
for (let i = 0; i < n; i++) {
|
|
54
|
+
const body = params.bodies[i];
|
|
55
|
+
if (body.radius === void 0) continue;
|
|
56
|
+
view.pose(i, pos, rot);
|
|
57
|
+
if (!bounced[i] && pos[1] - body.radius < 0.02) {
|
|
58
|
+
bounced[i] = 1;
|
|
59
|
+
damping[i] = BALL_DAMPING.from;
|
|
60
|
+
}
|
|
61
|
+
if (bounced[i] && damping[i] < BALL_DAMPING.to) {
|
|
62
|
+
damping[i] = Math.min(
|
|
63
|
+
BALL_DAMPING.to,
|
|
64
|
+
damping[i] + (BALL_DAMPING.to - BALL_DAMPING.from) / BALL_DAMPING.steps
|
|
65
|
+
);
|
|
66
|
+
phase ??= { damping: [] };
|
|
67
|
+
phase.damping.push({ body: i, angular: damping[i], linear: BALL_DAMPING.linear });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (view.step >= MIN_SETTLE_STEP) {
|
|
71
|
+
let asleep = true;
|
|
72
|
+
let flat = true;
|
|
73
|
+
for (let i = 0; i < n && asleep; i++) {
|
|
74
|
+
view.pose(i, pos, rot);
|
|
75
|
+
if (outsideTray(pos, params.tray)) continue;
|
|
76
|
+
if (view.sleeping[i] === 0) asleep = false;
|
|
77
|
+
else if (flat && !restingFlat(params.bodies[i], pos, rot)) flat = false;
|
|
78
|
+
}
|
|
79
|
+
if (asleep && flat) return "done";
|
|
80
|
+
if ((asleep || view.step >= DICE_CLEANUP_STEP) && !cleaned) {
|
|
81
|
+
cleaned = true;
|
|
82
|
+
phase = { ...phase, ...CLEANUP };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return phase ?? "continue";
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
export {
|
|
89
|
+
BALL_DAMPING,
|
|
90
|
+
DICE_CLEANUP_STEP,
|
|
91
|
+
DICE_GROUPS,
|
|
92
|
+
DICE_SETTLE_RULE,
|
|
93
|
+
diceSettle,
|
|
94
|
+
outsideTray,
|
|
95
|
+
restingFlat
|
|
96
|
+
};
|
package/dist/skin.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type AssetRef, type FieldValue, type Infer, ShardError } from '@aethervtt/shard-core';
|
|
2
|
+
import { type DieKind } from './builtins.js';
|
|
3
|
+
import type { DiceEffectRecipeValue } from './effects.js';
|
|
4
|
+
import { type FaceLayoutValue } from './layout.js';
|
|
5
|
+
declare const VARIANT: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
|
|
6
|
+
layout: import("@aethervtt/shard-core").FieldType<AssetRef<"dice/FaceLayout"> | null, "object">;
|
|
7
|
+
bevel: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
8
|
+
params: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").JsonValue, "object">;
|
|
9
|
+
}>, "object">;
|
|
10
|
+
export declare const IMPACT_SOUNDS: readonly ['resin', 'metal', 'wood', 'glass'];
|
|
11
|
+
export type ImpactSound = (typeof IMPACT_SOUNDS)[number];
|
|
12
|
+
/** A dice skin: `data/**\/*.dice-skin.json`. */
|
|
13
|
+
export declare const DiceSkin: import("@aethervtt/shard-assets").DataType<{
|
|
14
|
+
readonly version: import("@aethervtt/shard-core").FieldType<number, "u8">;
|
|
15
|
+
readonly id: import("@aethervtt/shard-core").FieldType<string, "object">;
|
|
16
|
+
readonly family: import("@aethervtt/shard-core").FieldType<string, "object">;
|
|
17
|
+
readonly params: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").JsonValue, "object">;
|
|
18
|
+
readonly variants: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<Record<"d10" | "d100" | "d12" | "d20" | "d4" | "d6" | "d8" | "percentile", import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
|
|
19
|
+
layout: import("@aethervtt/shard-core").FieldType<AssetRef<"dice/FaceLayout"> | null, "object">;
|
|
20
|
+
bevel: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
21
|
+
params: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").JsonValue, "object">;
|
|
22
|
+
}>, "object">>>, "object">;
|
|
23
|
+
readonly sounds: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
|
|
24
|
+
impact: import("@aethervtt/shard-core").FieldType<"glass" | "metal" | "resin" | "wood", "u8">;
|
|
25
|
+
impactEvent: import("@aethervtt/shard-core").FieldType<AssetRef<"audio/AudioEvent"> | null, "object">;
|
|
26
|
+
entranceEvent: import("@aethervtt/shard-core").FieldType<AssetRef<"audio/AudioEvent"> | null, "object">;
|
|
27
|
+
resultEvent: import("@aethervtt/shard-core").FieldType<AssetRef<"audio/AudioEvent"> | null, "object">;
|
|
28
|
+
accent: import("@aethervtt/shard-core").FieldType<"" | "arcane-spark" | "resin-chime" | "void-whump", "u8">;
|
|
29
|
+
}>, "object">;
|
|
30
|
+
readonly effects: import("@aethervtt/shard-core").FieldType<(AssetRef<"dice/DiceEffectRecipe"> | null)[], "object">;
|
|
31
|
+
}, "dice/DiceSkin">;
|
|
32
|
+
export type DiceSkinValue = Infer<typeof DiceSkin>;
|
|
33
|
+
export type DiceSkinVariant = FieldValue<typeof VARIANT>;
|
|
34
|
+
export declare const MAX_SKIN_EFFECTS = 4;
|
|
35
|
+
export interface SkinContext {
|
|
36
|
+
layout?: (ref: AssetRef) => FaceLayoutValue | undefined;
|
|
37
|
+
recipe?: (ref: AssetRef) => DiceEffectRecipeValue | undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Everything wrong with a skin, each error with a path into it: an unknown family
|
|
41
|
+
* (`dice/unknown-family`), parameters its family doesn't have, a layout that can't show a value
|
|
42
|
+
* of its kind (`dice/layout-missing-value`), more than 4 recipes, or a recipe over its bounds
|
|
43
|
+
* (`dice/recipe-bounds`). Layouts and recipes resolve through `ctx`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function validateDiceSkin(skin: DiceSkinValue, ctx?: SkinContext): ShardError[];
|
|
46
|
+
/** A skin from its JSON form (files, or objects written like them), with defaults filled in. */
|
|
47
|
+
export declare function diceSkin(json: Record<string, unknown>): DiceSkinValue;
|
|
48
|
+
/** The layout a skin prints on a kind, and its bevel. */
|
|
49
|
+
export declare function skinVariant(skin: DiceSkinValue, kind: DieKind, definition: string, layouts: (ref: AssetRef) => FaceLayoutValue | undefined): {
|
|
50
|
+
layout: FaceLayoutValue;
|
|
51
|
+
bevel: number | undefined;
|
|
52
|
+
params: Record<string, unknown>;
|
|
53
|
+
};
|
|
54
|
+
/** Skins the package ships, one or two per family, for previews, tests and quick starts. */
|
|
55
|
+
export declare const DICE_SKINS: {
|
|
56
|
+
readonly ivory: AssetRef<"dice/DiceSkin">;
|
|
57
|
+
readonly obsidian: AssetRef<"dice/DiceSkin">;
|
|
58
|
+
readonly teal: AssetRef<"dice/DiceSkin">;
|
|
59
|
+
readonly ember: AssetRef<"dice/DiceSkin">;
|
|
60
|
+
readonly brass: AssetRef<"dice/DiceSkin">;
|
|
61
|
+
readonly frost: AssetRef<"dice/DiceSkin">;
|
|
62
|
+
};
|
|
63
|
+
export declare const BUILTIN_SKINS: Readonly<Record<keyof typeof DICE_SKINS, Record<string, unknown>>>;
|
|
64
|
+
export {};
|