@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.
- package/LICENSE +21 -0
- package/README.md +2 -2
- package/dist/attachments.d.ts +79 -0
- package/dist/attachments.js +43 -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 +97 -0
- package/dist/effects.js +196 -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 +31 -0
- package/dist/index.js +322 -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 +174 -0
- package/dist/preview.d.ts +32 -0
- package/dist/preview.js +123 -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 +61 -0
- package/dist/skin.js +303 -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 +225 -0
- package/dist/table.js +1757 -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
package/dist/numerals.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const NUMERAL_HEIGHT = 727.5;
|
|
2
|
+
const DICE_NUMERALS = {
|
|
3
|
+
"0": {
|
|
4
|
+
advance: 630.9,
|
|
5
|
+
path: "M315.4 -9.8Q193.8 -9.8 126.7 87.9Q59.6 185.5 59.6 363.3Q59.6 540 127.2 638.7Q194.8 737.3 315.4 737.3Q436.5 737.3 503.9 638.7Q571.3 540 571.3 363.3Q571.3 185.5 504.4 87.9Q437.5 -9.8 315.4 -9.8ZM315.4 71.3Q395.5 71.3 438.7 148.2Q481.9 225.1 481.9 363.3Q481.9 502.4 438.7 579.6Q395.5 656.7 315.4 656.7Q235.8 656.7 192.4 579.3Q148.9 502 148.9 363.3Q148.9 225.1 192.4 148.2Q235.8 71.3 315.4 71.3Z"
|
|
6
|
+
},
|
|
7
|
+
"1": {
|
|
8
|
+
advance: 406.7,
|
|
9
|
+
path: "M205.1 727.5L318.8 727.5L318.8 0L227.5 0L227.5 641.6L222.7 641.6L46.9 511.2L46.9 610.8L205.1 727.5Z"
|
|
10
|
+
},
|
|
11
|
+
"2": {
|
|
12
|
+
advance: 609.9,
|
|
13
|
+
path: "M538.6 0L75.2 0L75.2 66.9L316.9 329.1Q357.4 373 384.3 406.3Q411.1 439.5 424.8 469.5Q438.5 499.5 438.5 533.2Q438.5 590.3 399.2 623.5Q359.9 656.7 301.8 656.7Q239.7 656.7 202.9 619.6Q166 582.5 166 521L77.1 521Q77.1 585.4 106.9 634.3Q136.7 683.1 188 710.2Q239.3 737.3 304.2 737.3Q369.1 737.3 419.2 710.2Q469.2 683.1 497.8 637.2Q526.4 591.3 526.4 534.2Q526.4 494.6 512.2 457.3Q498 419.9 462.4 372.6Q426.8 325.2 362.3 256.8L203.1 87.4L203.1 81.5L538.6 81.5L538.6 0Z"
|
|
14
|
+
},
|
|
15
|
+
"3": {
|
|
16
|
+
advance: 617.7,
|
|
17
|
+
path: "M310.1 -9.8Q238.8 -9.8 183.6 14.6Q128.4 39.1 95.9 82.5Q63.5 126 61 183.6L154.8 183.6Q158.7 131.8 202.6 101.8Q246.6 71.8 308.6 71.8Q377 71.8 421.6 106.9Q466.3 142.1 466.3 198.7Q466.3 257.8 422.4 295.2Q378.4 332.5 297.4 332.5L238.3 332.5L238.3 413.1L297.4 413.1Q361.3 413.1 401.1 447Q440.9 481 440.9 537.1Q440.9 591.3 405.8 624Q370.6 656.7 311 656.7Q273.9 656.7 241.5 643.3Q209 629.9 188.2 605Q167.5 580.1 166 544.4L76.7 544.4Q78.6 601.6 110.8 645Q143.1 688.5 196 712.9Q249 737.3 313 737.3Q380.9 737.3 429.9 710Q479 682.6 505.4 638.2Q531.7 593.8 531.7 542Q531.7 480.5 499.3 437.5Q466.8 394.5 411.1 377.9L411.1 372.1Q481 360.8 519.5 313.7Q558.1 266.6 558.1 197.8Q558.1 138.2 525.9 91.3Q493.7 44.4 437.5 17.3Q381.3 -9.8 310.1 -9.8Z"
|
|
18
|
+
},
|
|
19
|
+
"4": {
|
|
20
|
+
advance: 646,
|
|
21
|
+
path: "M400.9 147.9L58.6 147.9L58.6 223.6L377 727.5L489.7 727.5L489.7 229.5L588.4 229.5L588.4 147.9L489.7 147.9L489.7 0L400.9 0L400.9 147.9ZM157.7 229.5L401.4 229.5L401.4 611.3L395.5 611.3L157.7 235.4L157.7 229.5Z"
|
|
22
|
+
},
|
|
23
|
+
"5": {
|
|
24
|
+
advance: 607.9,
|
|
25
|
+
path: "M305.7 -9.8Q241.7 -9.8 190.9 15.4Q140.1 40.5 109.9 84.2Q79.6 127.9 77.1 183.6L167 183.6Q171.4 134.8 211.2 103.3Q251 71.8 305.7 71.8Q349.6 71.8 384 92.3Q418.5 112.8 438 148.9Q457.5 185.1 457.5 231Q457.5 277.8 437 314.9Q416.5 352.1 381.1 373.3Q345.7 394.5 299.8 394.5Q266.1 394.5 231.4 383.8Q196.8 373 175.3 356.4L88.4 367.2L131.3 727.5L514.2 727.5L514.2 646L209.5 646L184.6 433.6L188.5 433.6Q210.9 452.1 245.1 464.1Q279.3 476.1 316.9 476.1Q383.8 476.1 436 444.6Q488.3 413.1 518.3 357.9Q548.3 302.7 548.3 232.4Q548.3 162.6 516.8 107.9Q485.4 53.2 430.7 21.7Q376 -9.8 305.7 -9.8Z"
|
|
26
|
+
},
|
|
27
|
+
"6": {
|
|
28
|
+
advance: 620.1,
|
|
29
|
+
path: "M315.4 -9.8Q270 -9.8 224.6 6.8Q179.2 23.4 141.8 62.5Q104.5 101.6 82 168.5Q59.6 235.4 59.6 335.9Q59.6 527.8 131.3 632.6Q203.1 737.3 326.7 737.3Q387.7 737.3 436 712.9Q484.4 688.5 515.1 645Q545.9 601.6 554.7 544.4L463.9 544.4Q451.7 591.8 417.5 623.5Q383.3 655.3 326.7 655.3Q242.7 655.3 195.1 582.8Q147.5 510.3 147.5 378.9L153.3 378.9Q182.6 424.3 229.2 450Q275.9 475.6 332.5 475.6Q395 475.6 446.8 444.8Q498.5 414.1 529.5 359.6Q560.5 305.2 560.5 234.9Q560.5 167 530.3 111.6Q500 56.2 444.8 23.2Q389.6 -9.8 315.4 -9.8ZM315.4 71.8Q359.4 71.8 394.5 93.8Q429.7 115.7 450.2 152.6Q470.7 189.5 470.7 234.9Q470.7 279.3 450.9 315.7Q431.2 352.1 396.5 373.5Q361.8 395 317.9 395Q273.9 395 238.3 372.6Q202.6 350.1 181.6 313.2Q160.6 276.4 160.6 233.9Q160.6 190.9 180.7 154.1Q200.7 117.2 235.8 94.5Q271 71.8 315.4 71.8Z"
|
|
30
|
+
},
|
|
31
|
+
"7": {
|
|
32
|
+
advance: 565.9,
|
|
33
|
+
path: "M195.3 0L97.7 0L420.9 640.1L420.9 646L47.9 646L47.9 727.5L518.1 727.5L518.1 641.1L195.3 0Z"
|
|
34
|
+
},
|
|
35
|
+
"8": {
|
|
36
|
+
advance: 618.7,
|
|
37
|
+
path: "M309.1 -9.8Q235.4 -9.8 179 16.4Q122.6 42.5 91.1 88.4Q59.6 134.3 59.6 193.4Q59.6 239.3 78.1 278.6Q96.7 317.9 128.9 344.2Q161.1 370.6 201.7 377.9L201.7 381.8Q148.4 395.5 117.2 440.9Q85.9 486.3 85.9 543.9Q85.9 599.6 114.7 643.3Q143.6 687 193.8 712.2Q244.1 737.3 309.1 737.3Q373 737.3 423.3 712.2Q473.6 687 502.7 643.3Q531.7 599.6 531.7 543.9Q531.7 486.3 500.2 440.9Q468.8 395.5 417 381.8L417 377.9Q456.5 370.6 488.8 344.2Q521 317.9 540 278.6Q559.1 239.3 559.1 193.4Q559.1 134.3 527.1 88.4Q495.1 42.5 438.7 16.4Q382.3 -9.8 309.1 -9.8ZM309.1 70.8Q381.3 70.8 423.8 105.7Q466.3 140.6 466.3 198.2Q466.3 238.8 445.8 269.8Q425.3 300.8 389.6 318.6Q354 336.4 309.1 336.4Q240.2 336.4 195.6 297.6Q150.9 258.8 150.9 198.2Q150.9 140.6 193.1 105.7Q235.4 70.8 309.1 70.8ZM309.1 414.1Q366.7 414.1 404.3 448Q441.9 481.9 441.9 537.1Q441.9 591.8 405.3 624.5Q368.7 657.2 309.1 657.2Q248.5 657.2 212.2 624.5Q175.8 591.8 175.8 537.1Q175.8 481.9 212.9 448Q250 414.1 309.1 414.1Z"
|
|
38
|
+
},
|
|
39
|
+
"9": {
|
|
40
|
+
advance: 620.1,
|
|
41
|
+
path: "M294.4 -9.8Q232.4 -9.8 184.1 14.6Q135.7 39.1 105 83Q74.2 127 65.4 185.1L157.2 185.1Q168.5 136.7 202.6 104.5Q236.8 72.3 294.4 72.3Q377.4 72.3 425.5 144.8Q473.6 217.3 473.6 349.6L467.8 349.6Q438.5 305.2 392.1 278.8Q345.7 252.4 288.6 252.4Q226.1 252.4 173.8 283.4Q121.6 314.5 90.6 368.9Q59.6 423.3 59.6 493.2Q59.6 561 90.3 616.9Q121.1 672.9 176.5 705.8Q231.9 738.8 306.6 737.3Q351.6 736.8 396.5 720Q441.4 703.1 478.5 664.6Q515.6 626 538.1 559.6Q560.5 493.2 560.5 392.6Q560.5 199.7 489.3 95Q418 -9.8 294.4 -9.8ZM302.2 333.5Q346.7 333.5 382.3 356.2Q418 378.9 439.2 416Q460.4 453.1 460.4 495.6Q460.4 537.6 440.2 574Q419.9 610.4 385 633.1Q350.1 655.8 305.2 655.8Q261.2 655.8 226.3 634Q191.4 612.3 170.9 575.4Q150.4 538.6 150.4 493.7Q150.4 449.2 170.2 412.8Q189.9 376.5 224.1 355Q258.3 333.5 302.2 333.5Z"
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
export {
|
|
45
|
+
DICE_NUMERALS,
|
|
46
|
+
NUMERAL_HEIGHT
|
|
47
|
+
};
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type AssetRef } from '@aethervtt/shard-core';
|
|
2
|
+
import type { TrackWorkerLike } from '@aethervtt/shard-physics/worker';
|
|
3
|
+
import { type Plugin } from '@aethervtt/shard-runtime';
|
|
4
|
+
import { type DieKind } from './builtins.js';
|
|
5
|
+
export interface DicePluginOptions {
|
|
6
|
+
/** The dice camera: a top-down view of the tray, clearing to alpha 0 (0052). */
|
|
7
|
+
camera?: {
|
|
8
|
+
fovY?: number;
|
|
9
|
+
/** A RenderTargets ref; null renders into the app's surface. */
|
|
10
|
+
target?: AssetRef<'RenderTarget'> | null;
|
|
11
|
+
order?: number;
|
|
12
|
+
ev100?: number;
|
|
13
|
+
};
|
|
14
|
+
/** How long dice rest before the presentation finishes, ms. Default 60 s. */
|
|
15
|
+
restMs?: number;
|
|
16
|
+
/** Bodies a roll may put on the table (percentile is two). Default 32. */
|
|
17
|
+
maxDice?: number;
|
|
18
|
+
/** Makes the track worker: `diceWorker` from `@aethervtt/shard-dice/worker`, or 'inline'. */
|
|
19
|
+
worker?: (() => TrackWorkerLike) | 'inline';
|
|
20
|
+
/** The clock rests and releases run on, ms. Default `performance.now`. */
|
|
21
|
+
now?: () => number;
|
|
22
|
+
/** Definitions a kind plays, when not the built-ins. */
|
|
23
|
+
kinds?: Partial<Record<DieKind, readonly string[]>>;
|
|
24
|
+
/** The key light's illuminance, lux. Default 60,000. */
|
|
25
|
+
illuminance?: number;
|
|
26
|
+
/** Image-based light from a small procedural studio. Default true. */
|
|
27
|
+
environment?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* How long a roll waits, past its physics, for an entrance's assets and warm-up before its die
|
|
30
|
+
* drops in instead (0065). Default 1,500 ms.
|
|
31
|
+
*/
|
|
32
|
+
entranceWaitMs?: number;
|
|
33
|
+
}
|
|
34
|
+
/** Where the dice camera looks from: straight down, screen-up toward -z. */
|
|
35
|
+
export declare const TOP_DOWN: [number, number, number, number];
|
|
36
|
+
/**
|
|
37
|
+
* Dice (0054): the table, its camera, key light, image-based light and shadow-catching tray, and
|
|
38
|
+
* the systems that present rolls. Needs `forwardPlugin` (render/forward, the shadow catcher, the
|
|
39
|
+
* environment). Add `particlesPlugin` for particle bursts and `audioPlugin` for sound.
|
|
40
|
+
*/
|
|
41
|
+
export declare function dicePlugin(options?: DicePluginOptions): Plugin;
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { defineSystem, Update } from "@aethervtt/shard-core";
|
|
2
|
+
import {
|
|
3
|
+
addShaderVariantSource,
|
|
4
|
+
Camera3d,
|
|
5
|
+
DirectionalLight,
|
|
6
|
+
EnvironmentMap,
|
|
7
|
+
Exposure,
|
|
8
|
+
GpuDeviceLost,
|
|
9
|
+
InstanceData,
|
|
10
|
+
MaterialAsset,
|
|
11
|
+
Materials,
|
|
12
|
+
Mesh3d,
|
|
13
|
+
Meshes,
|
|
14
|
+
MeshMaterial,
|
|
15
|
+
NotShadowCaster,
|
|
16
|
+
registerShaders,
|
|
17
|
+
Shaders
|
|
18
|
+
} from "@aethervtt/shard-render";
|
|
19
|
+
import { definePlugin } from "@aethervtt/shard-runtime";
|
|
20
|
+
import { Textures } from "@aethervtt/shard-texture";
|
|
21
|
+
import { lookAt, Transform } from "@aethervtt/shard-transform";
|
|
22
|
+
import { KIND_DEFINITIONS, registerBuiltinDice } from "./builtins.js";
|
|
23
|
+
import { DiceDie, DiceRollRequest } from "./components.js";
|
|
24
|
+
import { DieDefinitionAsset } from "./definition-asset.js";
|
|
25
|
+
import { DiceEffectRecipe } from "./effects.js";
|
|
26
|
+
import { BUILTIN_FAMILIES } from "./families.js";
|
|
27
|
+
import { registerBuiltinGlyphs } from "./glyphs.js";
|
|
28
|
+
import { FaceLayout, NUMBERS_LAYOUT, PIPS_LAYOUT } from "./layout.js";
|
|
29
|
+
import { allDiceFamilies, DICE_SHADERS } from "./material.js";
|
|
30
|
+
import { floorQuad } from "./mesh.js";
|
|
31
|
+
import { diceMethods } from "./methods.js";
|
|
32
|
+
import { BUILTIN_SKINS, DICE_SKINS, DiceSkin, diceSkin } from "./skin.js";
|
|
33
|
+
import { studioEnvironment } from "./studio.js";
|
|
34
|
+
import { DiceTable, DiceTableState, ENTRANCE_WAIT_MS } from "./table.js";
|
|
35
|
+
import { DiceTray, TRAY_SHADERS } from "./tray.js";
|
|
36
|
+
import { diceVariants } from "./variants.js";
|
|
37
|
+
const TOP_DOWN = [-Math.SQRT1_2, 0, 0, Math.SQRT1_2];
|
|
38
|
+
const tableFrame = defineSystem({
|
|
39
|
+
name: "dice/table",
|
|
40
|
+
description: "Plays the dice presentation: samples the track, lands, accents, rests, releases.",
|
|
41
|
+
run: (_, world) => world.resource(DiceTable).frame()
|
|
42
|
+
});
|
|
43
|
+
const deviceLoss = defineSystem({
|
|
44
|
+
name: "dice/device-loss",
|
|
45
|
+
description: "Fails a playing roll when the GPU device is lost.",
|
|
46
|
+
setup: (world) => world.reader(GpuDeviceLost),
|
|
47
|
+
run: (reader, world) => {
|
|
48
|
+
const lost = reader.read();
|
|
49
|
+
if (lost.length > 0) world.resource(DiceTable).deviceLost(lost[0].message);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const rollRequests = defineSystem({
|
|
53
|
+
name: "dice/roll-requests",
|
|
54
|
+
description: "Plays the roll of each new dice/RollRequest entity, then removes the request.",
|
|
55
|
+
run: (_, world) => {
|
|
56
|
+
const found = [];
|
|
57
|
+
for (const e of world.query({ with: [DiceRollRequest] }).entities()) found.push(e);
|
|
58
|
+
if (found.length === 0) return;
|
|
59
|
+
const table = world.resource(DiceTable);
|
|
60
|
+
for (const e of found) {
|
|
61
|
+
const request = world.get(e, DiceRollRequest);
|
|
62
|
+
world.remove(e, DiceRollRequest);
|
|
63
|
+
void table.play(request.roll, { replace: request.replace }).catch((err) => {
|
|
64
|
+
table.lastError = err;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
function dicePlugin(options = {}) {
|
|
70
|
+
return definePlugin({
|
|
71
|
+
name: "dice",
|
|
72
|
+
dependencies: ["render/forward", "render/shadow-catcher"],
|
|
73
|
+
provides: [
|
|
74
|
+
DiceTable,
|
|
75
|
+
DiceDie,
|
|
76
|
+
DiceRollRequest,
|
|
77
|
+
DiceSkin,
|
|
78
|
+
FaceLayout,
|
|
79
|
+
DiceEffectRecipe,
|
|
80
|
+
DieDefinitionAsset,
|
|
81
|
+
DiceTray,
|
|
82
|
+
BUILTIN_FAMILIES,
|
|
83
|
+
tableFrame,
|
|
84
|
+
rollRequests
|
|
85
|
+
],
|
|
86
|
+
build(app) {
|
|
87
|
+
registerBuiltinDice();
|
|
88
|
+
registerBuiltinGlyphs();
|
|
89
|
+
const world = app.world;
|
|
90
|
+
addShaderVariantSource(world, diceVariants);
|
|
91
|
+
world.initResource(DiceSkin.store);
|
|
92
|
+
world.initResource(FaceLayout.store);
|
|
93
|
+
world.initResource(DiceEffectRecipe.store);
|
|
94
|
+
const skins = world.resource(DiceSkin.store);
|
|
95
|
+
for (const [name, json] of Object.entries(BUILTIN_SKINS)) {
|
|
96
|
+
skins.set(DICE_SKINS[name].guid, diceSkin(json));
|
|
97
|
+
}
|
|
98
|
+
const layouts = world.resource(FaceLayout.store);
|
|
99
|
+
layouts.set("dice:layout/numbers", NUMBERS_LAYOUT);
|
|
100
|
+
layouts.set("dice:layout/pips", PIPS_LAYOUT);
|
|
101
|
+
world.insertResource(
|
|
102
|
+
DiceTable,
|
|
103
|
+
new DiceTableState(world, {
|
|
104
|
+
restMs: options.restMs ?? 6e4,
|
|
105
|
+
maxDice: options.maxDice ?? 32,
|
|
106
|
+
now: options.now ?? (() => performance.now()),
|
|
107
|
+
kinds: { ...KIND_DEFINITIONS, ...options.kinds },
|
|
108
|
+
worker: options.worker ?? "inline",
|
|
109
|
+
fovY: options.camera?.fovY ?? 35,
|
|
110
|
+
entranceWaitMs: options.entranceWaitMs ?? ENTRANCE_WAIT_MS
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
app.addSystems(Update, tableFrame, rollRequests, deviceLoss);
|
|
114
|
+
app.addMethod(...diceMethods);
|
|
115
|
+
},
|
|
116
|
+
ready(app) {
|
|
117
|
+
const world = app.world;
|
|
118
|
+
const shaders = { ...DICE_SHADERS, ...TRAY_SHADERS };
|
|
119
|
+
for (const family of allDiceFamilies())
|
|
120
|
+
if (family.module) shaders[family.module.path] = family.module.source;
|
|
121
|
+
registerShaders(world.resource(Shaders), shaders);
|
|
122
|
+
const table = world.resource(DiceTable);
|
|
123
|
+
const quad = world.resource(Meshes).add(floorQuad(), "dice:tray");
|
|
124
|
+
const trayMaterial = world.resource(Materials).add(new MaterialAsset({}, DiceTray), "dice:tray");
|
|
125
|
+
table.quad = quad;
|
|
126
|
+
table.trayMaterial = trayMaterial;
|
|
127
|
+
table.floor = world.spawn(
|
|
128
|
+
[Mesh3d, { mesh: quad }],
|
|
129
|
+
[MeshMaterial, { material: trayMaterial }],
|
|
130
|
+
[InstanceData, { x: 0, y: 0 }],
|
|
131
|
+
NotShadowCaster,
|
|
132
|
+
[Transform, { scale: [7.5, 1, 4.4] }]
|
|
133
|
+
);
|
|
134
|
+
table.light = world.spawn(
|
|
135
|
+
[
|
|
136
|
+
DirectionalLight,
|
|
137
|
+
{ illuminance: options.illuminance ?? 6e4, shadows: true, color: [1, 0.97, 0.91, 1] }
|
|
138
|
+
],
|
|
139
|
+
[Transform, { rotation: lightRotation() }]
|
|
140
|
+
);
|
|
141
|
+
const q = lightRotation();
|
|
142
|
+
const fx = -2 * (q[0] * q[2] + q[3] * q[1]);
|
|
143
|
+
const fz = -(1 - 2 * (q[0] * q[0] + q[1] * q[1]));
|
|
144
|
+
const fl = Math.sqrt(fx * fx + fz * fz) || 1;
|
|
145
|
+
table.shadowDir = [fx / fl, fz / fl];
|
|
146
|
+
const cam = options.camera ?? {};
|
|
147
|
+
table.camera = world.spawn(
|
|
148
|
+
[
|
|
149
|
+
Camera3d,
|
|
150
|
+
{
|
|
151
|
+
fovY: cam.fovY ?? 35,
|
|
152
|
+
clearColor: [0, 0, 0, 0],
|
|
153
|
+
...cam.target ? { target: cam.target } : {},
|
|
154
|
+
...cam.order !== void 0 ? { order: cam.order } : {}
|
|
155
|
+
}
|
|
156
|
+
],
|
|
157
|
+
[Exposure, { ev100: cam.ev100 ?? 13 }],
|
|
158
|
+
[Transform, { translation: [0, 14, 0], rotation: TOP_DOWN }]
|
|
159
|
+
);
|
|
160
|
+
if (options.environment !== false) {
|
|
161
|
+
const texture = world.resource(Textures).add(studioEnvironment(), "dice:studio");
|
|
162
|
+
world.add(table.camera, EnvironmentMap, { texture, intensity: 2600 });
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
dispose(app) {
|
|
166
|
+
app.world.tryResource(DiceTable)?.dispose();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const lightRotation = () => lookAt([-3.4, 10, 4.2], [0, 0, 0]);
|
|
171
|
+
export {
|
|
172
|
+
TOP_DOWN,
|
|
173
|
+
dicePlugin
|
|
174
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type AssetRef } from '@aethervtt/shard-core';
|
|
2
|
+
import { type App } from '@aethervtt/shard-runtime';
|
|
3
|
+
import type { DieKind } from './builtins.js';
|
|
4
|
+
export interface DiceThumbnailOptions {
|
|
5
|
+
skin: AssetRef;
|
|
6
|
+
kind: DieKind;
|
|
7
|
+
/** The value on top (a percentile's 1..100 shows its tens die unless `part` says units). */
|
|
8
|
+
value: number;
|
|
9
|
+
part?: 'tens' | 'units';
|
|
10
|
+
/** Pixels, square. Default 128. */
|
|
11
|
+
size?: number;
|
|
12
|
+
/** PNG bytes (straight alpha), or the GPU texture itself (yours to destroy). Default 'png'. */
|
|
13
|
+
output?: 'png' | 'texture';
|
|
14
|
+
/**
|
|
15
|
+
* Draws it blended or opaque; default as its family draws a die at rest. Dropped dice and large
|
|
16
|
+
* pools draw some families the other way, so a shader bake (0064) shows both.
|
|
17
|
+
*/
|
|
18
|
+
blended?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface DiceThumbnail {
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
png: Uint8Array | undefined;
|
|
24
|
+
texture: GPUTexture | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Renders one landed die of a skin into an offscreen target on the app's own device (no new
|
|
28
|
+
* device, no canvas): its value on top, reading upright, seen from above at an angle. The app
|
|
29
|
+
* needs `dicePlugin`. In an app a runner drives, the frames come by themselves; a manual app
|
|
30
|
+
* (tests, tools) is stepped here.
|
|
31
|
+
*/
|
|
32
|
+
export declare function renderDiceThumbnail(app: App, options: DiceThumbnailOptions): Promise<DiceThumbnail>;
|
package/dist/preview.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
import { encodePng } from "@aethervtt/shard-protocol";
|
|
3
|
+
import {
|
|
4
|
+
Camera3d,
|
|
5
|
+
captureView,
|
|
6
|
+
EnvironmentMap,
|
|
7
|
+
Exposure,
|
|
8
|
+
Gpu,
|
|
9
|
+
InstanceData,
|
|
10
|
+
Mesh3d,
|
|
11
|
+
MeshMaterial,
|
|
12
|
+
OffscreenTarget,
|
|
13
|
+
RenderStats,
|
|
14
|
+
RenderTargets,
|
|
15
|
+
Shaders
|
|
16
|
+
} from "@aethervtt/shard-render";
|
|
17
|
+
import { FrameDemand, LOADING_DEMAND } from "@aethervtt/shard-runtime";
|
|
18
|
+
import { lookAt, Transform } from "@aethervtt/shard-transform";
|
|
19
|
+
import { dieGeometry, requireDie } from "./definition.js";
|
|
20
|
+
import { restingHeight, restRotation } from "./landing.js";
|
|
21
|
+
import { expandRoll } from "./roll.js";
|
|
22
|
+
import { DiceSkin } from "./skin.js";
|
|
23
|
+
import { DiceTable } from "./table.js";
|
|
24
|
+
let slot = 0;
|
|
25
|
+
async function renderDiceThumbnail(app, options) {
|
|
26
|
+
const world = app.world;
|
|
27
|
+
const table = world.resource(DiceTable);
|
|
28
|
+
const size = options.size ?? 128;
|
|
29
|
+
const all = expandRoll({
|
|
30
|
+
id: "thumbnail",
|
|
31
|
+
dice: [{ kind: options.kind, value: options.value, skin: options.skin }]
|
|
32
|
+
});
|
|
33
|
+
const die = all[options.part === "units" ? all.length - 1 : 0];
|
|
34
|
+
const skin = world.resource(DiceSkin.store).get(options.skin);
|
|
35
|
+
if (!skin) {
|
|
36
|
+
throw new ShardError(
|
|
37
|
+
"dice/unknown-skin",
|
|
38
|
+
`No dice skin ${options.skin.guid ?? options.skin.path}`,
|
|
39
|
+
{
|
|
40
|
+
hint: "Load the skin (or add it to DiceSkin.store) first."
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const g = dieGeometry(requireDie(die.definition));
|
|
45
|
+
const skinKey = options.skin.guid ?? options.skin.path ?? "";
|
|
46
|
+
const entry = table.resources.acquire(g, skinKey, skin, die.kind, (r) => table.layout(r), {});
|
|
47
|
+
const material = table.resources.material(entry, {
|
|
48
|
+
blended: options.blended ?? table.resources.blendedByNature(entry),
|
|
49
|
+
dropped: false
|
|
50
|
+
});
|
|
51
|
+
const x = 1e4 + slot++ % 64 * 20;
|
|
52
|
+
const scale = 1;
|
|
53
|
+
const rotation = restRotation(g, die.value);
|
|
54
|
+
const y = restingHeight(g, rotation, scale);
|
|
55
|
+
const gpu = world.resource(Gpu);
|
|
56
|
+
const target = new OffscreenTarget(gpu, { label: "dice-thumbnail", width: size, height: size });
|
|
57
|
+
const targetRef = world.resource(RenderTargets).add(target, `dice:thumbnail/${x}`);
|
|
58
|
+
const dieEntity = world.spawn(
|
|
59
|
+
[Mesh3d, { mesh: entry.mesh }],
|
|
60
|
+
[MeshMaterial, { material }],
|
|
61
|
+
[Transform, { translation: [x, y, 0], rotation, scale: [scale, scale, scale] }],
|
|
62
|
+
[InstanceData, { x: die.value, y: 0 }]
|
|
63
|
+
);
|
|
64
|
+
const reach = g.footprint * 0.5 * scale;
|
|
65
|
+
const fov = 30;
|
|
66
|
+
const distance = reach / Math.tan(fov / 2 * Math.PI / 180) * 1.45;
|
|
67
|
+
const eye = [x, y + distance * 0.82, distance * 0.57];
|
|
68
|
+
const camera = world.spawn(
|
|
69
|
+
[Camera3d, { fovY: fov, clearColor: [0, 0, 0, 0], target: targetRef, order: 50 }],
|
|
70
|
+
[Exposure, { ev100: 13 }],
|
|
71
|
+
[Transform, { translation: eye, rotation: lookAt(eye, [x, y * 0.6, 0], [0, 0, -1]) }]
|
|
72
|
+
);
|
|
73
|
+
const env = world.tryGet(table.camera, EnvironmentMap);
|
|
74
|
+
if (env) world.add(camera, EnvironmentMap, { texture: env.texture, intensity: env.intensity });
|
|
75
|
+
const view = `camera:${camera}`;
|
|
76
|
+
const manual = world.resource(FrameDemand).mode === "manual";
|
|
77
|
+
try {
|
|
78
|
+
const demand = world.resource(FrameDemand);
|
|
79
|
+
let image;
|
|
80
|
+
for (let i = 0; i < 240; i++) {
|
|
81
|
+
const shot = captureView(world, view);
|
|
82
|
+
if (manual) {
|
|
83
|
+
app.update(1 / 60);
|
|
84
|
+
await world.resource(Shaders).whenIdle();
|
|
85
|
+
await gpu.pipelines.whenIdle();
|
|
86
|
+
}
|
|
87
|
+
const frame = await shot;
|
|
88
|
+
const stats = world.resource(RenderStats).get(view);
|
|
89
|
+
if (stats && stats.drawCalls > 0 && stats.pending === 0 && !demand.isHeld(LOADING_DEMAND)) {
|
|
90
|
+
image = frame;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (!manual) await gpu.pipelines.whenIdle();
|
|
94
|
+
}
|
|
95
|
+
if (!image)
|
|
96
|
+
throw new ShardError("dice/thumbnail-failed", "The thumbnail never finished rendering");
|
|
97
|
+
if (options.output === "texture") {
|
|
98
|
+
return { width: size, height: size, png: void 0, texture: target.texture() };
|
|
99
|
+
}
|
|
100
|
+
const data = new Uint8Array(image.data);
|
|
101
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
102
|
+
const a = data[i + 3];
|
|
103
|
+
if (a === 0 || a === 255) continue;
|
|
104
|
+
for (let k = 0; k < 3; k++) data[i + k] = Math.min(255, Math.round(data[i + k] * 255 / a));
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
width: size,
|
|
108
|
+
height: size,
|
|
109
|
+
png: await encodePng(data, image.width, image.height),
|
|
110
|
+
texture: void 0
|
|
111
|
+
};
|
|
112
|
+
} finally {
|
|
113
|
+
world.despawn(dieEntity);
|
|
114
|
+
world.despawn(camera);
|
|
115
|
+
world.resource(RenderTargets).delete(targetRef.guid);
|
|
116
|
+
if (options.output !== "texture") target.destroy();
|
|
117
|
+
table.resources.release(entry, table.options.now());
|
|
118
|
+
world.resource(FrameDemand).after(8016);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export {
|
|
122
|
+
renderDiceThumbnail
|
|
123
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DiceQualityPreference } from './roll.js';
|
|
2
|
+
export type DiceQuality = 'full' | 'balanced' | 'large-pool';
|
|
3
|
+
export interface DiceQualityChoice {
|
|
4
|
+
tier: DiceQuality;
|
|
5
|
+
/** Why, for `dice.describe`. */
|
|
6
|
+
reason: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The tier for a number of bodies and a preference: `auto` is full up to 12, balanced to 16, then
|
|
10
|
+
* large-pool; `balanced` holds to 12 and `full` to 24 before large-pool takes over.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveDiceQuality(count: number, preference?: DiceQualityPreference): DiceQualityChoice;
|
package/dist/quality.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function resolveDiceQuality(count, preference = "auto") {
|
|
2
|
+
const n = `${count} ${count === 1 ? "die" : "dice"}`;
|
|
3
|
+
switch (preference) {
|
|
4
|
+
case "large-pool":
|
|
5
|
+
return { tier: "large-pool", reason: "large-pool asked for" };
|
|
6
|
+
case "full":
|
|
7
|
+
return count > 24 ? { tier: "large-pool", reason: `full asked for, but ${n} is over 24` } : { tier: "full", reason: `full asked for (${n})` };
|
|
8
|
+
case "balanced":
|
|
9
|
+
return count > 12 ? { tier: "large-pool", reason: `balanced asked for, but ${n} is over 12` } : { tier: "balanced", reason: `balanced asked for (${n})` };
|
|
10
|
+
default:
|
|
11
|
+
if (count > 16) return { tier: "large-pool", reason: `auto: ${n}, over 16` };
|
|
12
|
+
if (count > 12) return { tier: "balanced", reason: `auto: ${n}, over 12` };
|
|
13
|
+
return { tier: "full", reason: `auto: ${n}` };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
resolveDiceQuality
|
|
18
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { AssetRef, World } from '@aethervtt/shard-core';
|
|
2
|
+
import type { DieKind } from './builtins.js';
|
|
3
|
+
import type { DieGeometry } from './definition.js';
|
|
4
|
+
import type { FaceLayoutValue } from './layout.js';
|
|
5
|
+
import { type BakeMarksOptions, type MarkAtlas } from './marks.js';
|
|
6
|
+
import { type DiceFamily } from './material.js';
|
|
7
|
+
import { type DiceSkinValue } from './skin.js';
|
|
8
|
+
export declare const RELEASE_AFTER_MS = 8000;
|
|
9
|
+
/** Which material a die draws with. */
|
|
10
|
+
export interface DiceLookKey {
|
|
11
|
+
/**
|
|
12
|
+
* Premultiplied: dropped dice, fading dice, see-through families (but for the large-pool tier,
|
|
13
|
+
* which draws those opaque so they batch).
|
|
14
|
+
*/
|
|
15
|
+
blended: boolean;
|
|
16
|
+
dropped: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* An entrance die's index (0065): it draws with a material of its own, so its result and
|
|
19
|
+
* resultTime are its own (it lands after the dice sharing its kind and skin).
|
|
20
|
+
*/
|
|
21
|
+
entrance?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface DiceResourceEntry {
|
|
24
|
+
key: string;
|
|
25
|
+
geometry: DieGeometry;
|
|
26
|
+
kind: DieKind;
|
|
27
|
+
family: DiceFamily;
|
|
28
|
+
refs: number;
|
|
29
|
+
releaseAt: number;
|
|
30
|
+
mesh: AssetRef<'Mesh'>;
|
|
31
|
+
atlas: MarkAtlas;
|
|
32
|
+
texture: AssetRef<'Texture'>;
|
|
33
|
+
params: Record<string, unknown>;
|
|
34
|
+
materials: Map<string, AssetRef<'Material'>>;
|
|
35
|
+
/** Entrance dice's own materials (0065), by look: apart, so setAll leaves them be. */
|
|
36
|
+
own: Map<string, AssetRef<'Material'>>;
|
|
37
|
+
}
|
|
38
|
+
export declare class DiceResources {
|
|
39
|
+
private readonly entries;
|
|
40
|
+
private readonly world;
|
|
41
|
+
/** The earliest release due (Infinity when none): checked every frame, so kept, not searched. */
|
|
42
|
+
private due;
|
|
43
|
+
constructor(world: World);
|
|
44
|
+
/**
|
|
45
|
+
* The resources for one kind of die in one skin, made on first use. Each acquire needs a
|
|
46
|
+
* release. Throws `dice/unknown-family` for a skin whose family isn't defined.
|
|
47
|
+
*/
|
|
48
|
+
acquire(geometry: DieGeometry, skinKey: string, skin: DiceSkinValue, kind: DieKind, layouts: (ref: AssetRef) => FaceLayoutValue | undefined, bake: BakeMarksOptions): DiceResourceEntry;
|
|
49
|
+
/** The entry's material for a look, made on first use. */
|
|
50
|
+
material(entry: DiceResourceEntry, look: DiceLookKey): AssetRef<'Material'>;
|
|
51
|
+
/** Whether dice of this entry need blending in their normal look (glass, see-through resin). */
|
|
52
|
+
blendedByNature(entry: DiceResourceEntry): boolean;
|
|
53
|
+
/** Whether the family's type fixes a blend: its dice blend in every tier, large pools too. */
|
|
54
|
+
fixedBlend(entry: DiceResourceEntry): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Sets a field on every material of these entries (result, its time, fade); entrance dice's own
|
|
57
|
+
* (in `own`) follow their own landing.
|
|
58
|
+
*/
|
|
59
|
+
setAll(entries: Iterable<DiceResourceEntry>, field: 'result' | 'resultTime' | 'fade', value: number): void;
|
|
60
|
+
release(entry: DiceResourceEntry, now: number): void;
|
|
61
|
+
/** The earliest release due, or Infinity. */
|
|
62
|
+
nextRelease(): number;
|
|
63
|
+
/** Frees entries unused for 8 s. Returns how many. */
|
|
64
|
+
sweep(now: number): number;
|
|
65
|
+
private free;
|
|
66
|
+
/** Frees everything, used or not (teardown). */
|
|
67
|
+
dispose(): void;
|
|
68
|
+
stats(): {
|
|
69
|
+
entries: number;
|
|
70
|
+
used: number;
|
|
71
|
+
meshes: number;
|
|
72
|
+
atlases: number;
|
|
73
|
+
materials: number;
|
|
74
|
+
};
|
|
75
|
+
describe(): {
|
|
76
|
+
key: string;
|
|
77
|
+
die: string;
|
|
78
|
+
kind: DieKind;
|
|
79
|
+
family: string;
|
|
80
|
+
refs: number;
|
|
81
|
+
releaseInMs: number | null;
|
|
82
|
+
materials: string[];
|
|
83
|
+
atlas: [number, number];
|
|
84
|
+
}[];
|
|
85
|
+
}
|