@dice-o-rolla/dice-engine 0.3.1 → 0.5.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/README.md +5 -0
- package/dist/dice-engine.js +96 -23
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +20 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -73,6 +73,11 @@ labels, and optional value map. `skinId` and `soundPackId` are opaque applicatio
|
|
|
73
73
|
the engine does not load assets. Skins and sound definitions belong in the optional
|
|
74
74
|
`@dice-o-rolla/dice-assets` package, which is not a dependency of the engine.
|
|
75
75
|
|
|
76
|
+
Use `visualPresetSelector` in `roll()` or `simulate()` options to select a registered preset for
|
|
77
|
+
each physical die. The callback receives stable term/die/physical indices, physical and logical die
|
|
78
|
+
types, the current default, and paired-component metadata. Returning `undefined` keeps the default;
|
|
79
|
+
the engine rejects unknown IDs and presets for another physical die type before creating bodies.
|
|
80
|
+
|
|
76
81
|
The engine emits `die:spawn` and `die:remove` lifecycle events. Collision events are opt-in through
|
|
77
82
|
`DiceEngineOptions.collisionEvents`, bounded by `maxEventsPerFrame`, and suitable for an external
|
|
78
83
|
sound or effects adapter.
|
package/dist/dice-engine.js
CHANGED
|
@@ -293,7 +293,7 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
293
293
|
if (this.#active !== undefined && this.#queue.length >= this.#limits.maxQueuedRolls) {
|
|
294
294
|
throw new RollLimitExceededError('queue-size', this.#limits.maxQueuedRolls, this.#queue.length + 1);
|
|
295
295
|
}
|
|
296
|
-
const task = this.#createTask(notation, parsed, options
|
|
296
|
+
const task = this.#createTask(notation, parsed, options);
|
|
297
297
|
if (options.signal?.aborted === true) {
|
|
298
298
|
this.#rejectCancelled(task);
|
|
299
299
|
return task.promise;
|
|
@@ -328,7 +328,7 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
328
328
|
this.#physics.setCollisionEventsEnabled(true);
|
|
329
329
|
this.#physics.drainCollisionEvents();
|
|
330
330
|
this.#physics.drainImpactEvents();
|
|
331
|
-
dice.push(...this.#createSimulationDice(parsed, simulationId, throwGenerator));
|
|
331
|
+
dice.push(...this.#createSimulationDice(parsed, simulationId, throwGenerator, options.visualPresetSelector));
|
|
332
332
|
const captureFrames = options.captureFrames === true;
|
|
333
333
|
const frames = [];
|
|
334
334
|
const events = [];
|
|
@@ -677,7 +677,8 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
677
677
|
this.#renderer.setTheme(this.#theme);
|
|
678
678
|
this.#initialized = true;
|
|
679
679
|
}
|
|
680
|
-
#createTask(notation, parsed,
|
|
680
|
+
#createTask(notation, parsed, options) {
|
|
681
|
+
const { signal, visualPresetSelector } = options;
|
|
681
682
|
const session = {
|
|
682
683
|
id: `roll-${this.#nextSessionId++}`,
|
|
683
684
|
notation,
|
|
@@ -701,6 +702,7 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
701
702
|
reject,
|
|
702
703
|
...(signal === undefined ? {} : { signal }),
|
|
703
704
|
...(abortListener === undefined ? {} : { abortListener }),
|
|
705
|
+
...(visualPresetSelector === undefined ? {} : { visualPresetSelector }),
|
|
704
706
|
};
|
|
705
707
|
}
|
|
706
708
|
#startNext() {
|
|
@@ -731,7 +733,7 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
731
733
|
#createDice(task) {
|
|
732
734
|
const dice = [];
|
|
733
735
|
let index = 0;
|
|
734
|
-
const specs = this.#createPhysicalSpecs(task.parsed, task.session.id);
|
|
736
|
+
const specs = this.#createPhysicalSpecs(task.parsed, task.session.id, task.visualPresetSelector);
|
|
735
737
|
const totalDice = specs.length;
|
|
736
738
|
try {
|
|
737
739
|
for (const spec of specs) {
|
|
@@ -800,9 +802,9 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
800
802
|
});
|
|
801
803
|
}
|
|
802
804
|
}
|
|
803
|
-
#createSimulationDice(parsed, simulationId, throwGenerator) {
|
|
805
|
+
#createSimulationDice(parsed, simulationId, throwGenerator, visualPresetSelector) {
|
|
804
806
|
const dice = [];
|
|
805
|
-
const specs = this.#createPhysicalSpecs(parsed, simulationId);
|
|
807
|
+
const specs = this.#createPhysicalSpecs(parsed, simulationId, visualPresetSelector);
|
|
806
808
|
const totalDice = specs.length;
|
|
807
809
|
try {
|
|
808
810
|
for (const [index, spec] of specs.entries()) {
|
|
@@ -888,7 +890,7 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
888
890
|
}
|
|
889
891
|
events.push(Object.freeze(event));
|
|
890
892
|
}
|
|
891
|
-
#createPhysicalSpecs(parsed, sessionId) {
|
|
893
|
+
#createPhysicalSpecs(parsed, sessionId, visualPresetSelector) {
|
|
892
894
|
const specs = [];
|
|
893
895
|
let groupIndex = 0;
|
|
894
896
|
for (const [expressionIndex, expression] of parsed.expressions.entries()) {
|
|
@@ -901,8 +903,16 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
901
903
|
throw new RangeError(`${type} is not a standard die type`);
|
|
902
904
|
if (!isPhysicalDieType(type))
|
|
903
905
|
throw new RangeError(`${type} is not a physical die type`);
|
|
904
|
-
const preset = this.getVisualPreset(type);
|
|
905
906
|
for (let count = 0; count < expression.count; count += 1) {
|
|
907
|
+
const physicalIndex = specs.length;
|
|
908
|
+
const preset = this.#resolveVisualPreset({
|
|
909
|
+
physicalDieType: type,
|
|
910
|
+
logicalDieType: type,
|
|
911
|
+
termId,
|
|
912
|
+
expressionIndex,
|
|
913
|
+
dieIndex: count,
|
|
914
|
+
physicalIndex,
|
|
915
|
+
}, visualPresetSelector);
|
|
906
916
|
specs.push({
|
|
907
917
|
type,
|
|
908
918
|
geometryType: this.#getPresetGeometryType(preset),
|
|
@@ -910,7 +920,7 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
910
920
|
termId,
|
|
911
921
|
expressionIndex,
|
|
912
922
|
dieIndex: count,
|
|
913
|
-
physicalIndex
|
|
923
|
+
physicalIndex,
|
|
914
924
|
...(expression.selection === undefined ? {} : { selection: expression.selection }),
|
|
915
925
|
...(expression.score === undefined ? {} : { scoreRules: expression.score }),
|
|
916
926
|
});
|
|
@@ -920,56 +930,119 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
920
930
|
for (let count = 0; count < expression.count; count += 1) {
|
|
921
931
|
const groupId = `${sessionId}:group-${groupIndex++}`;
|
|
922
932
|
if (expression.type === 'd100') {
|
|
923
|
-
const
|
|
933
|
+
const tensPhysicalIndex = specs.length;
|
|
934
|
+
const tensPreset = this.#resolveVisualPreset({
|
|
935
|
+
physicalDieType: 'd10',
|
|
936
|
+
logicalDieType: 'd100',
|
|
937
|
+
termId,
|
|
938
|
+
expressionIndex,
|
|
939
|
+
dieIndex: count,
|
|
940
|
+
physicalIndex: tensPhysicalIndex,
|
|
941
|
+
component: { groupType: 'd100', role: 'tens' },
|
|
942
|
+
}, visualPresetSelector);
|
|
924
943
|
specs.push({
|
|
925
944
|
type: 'd100',
|
|
926
|
-
geometryType: this.#getPresetGeometryType(
|
|
927
|
-
preset,
|
|
945
|
+
geometryType: this.#getPresetGeometryType(tensPreset),
|
|
946
|
+
preset: tensPreset,
|
|
928
947
|
termId,
|
|
929
948
|
expressionIndex,
|
|
930
949
|
dieIndex: count,
|
|
931
|
-
physicalIndex:
|
|
950
|
+
physicalIndex: tensPhysicalIndex,
|
|
932
951
|
component: { groupId, groupType: 'd100', role: 'tens' },
|
|
933
952
|
faceLabels: D100_TENS_LABELS,
|
|
934
953
|
});
|
|
954
|
+
const unitsPhysicalIndex = specs.length;
|
|
955
|
+
const unitsPreset = this.#resolveVisualPreset({
|
|
956
|
+
physicalDieType: 'd10',
|
|
957
|
+
logicalDieType: 'd100',
|
|
958
|
+
termId,
|
|
959
|
+
expressionIndex,
|
|
960
|
+
dieIndex: count,
|
|
961
|
+
physicalIndex: unitsPhysicalIndex,
|
|
962
|
+
component: { groupType: 'd100', role: 'units' },
|
|
963
|
+
}, visualPresetSelector);
|
|
935
964
|
specs.push({
|
|
936
965
|
type: 'd10',
|
|
937
|
-
geometryType: this.#getPresetGeometryType(
|
|
938
|
-
preset,
|
|
966
|
+
geometryType: this.#getPresetGeometryType(unitsPreset),
|
|
967
|
+
preset: unitsPreset,
|
|
939
968
|
termId,
|
|
940
969
|
expressionIndex,
|
|
941
970
|
dieIndex: count,
|
|
942
|
-
physicalIndex:
|
|
971
|
+
physicalIndex: unitsPhysicalIndex,
|
|
943
972
|
component: { groupId, groupType: 'd100', role: 'units' },
|
|
944
973
|
});
|
|
945
974
|
continue;
|
|
946
975
|
}
|
|
947
|
-
const
|
|
976
|
+
const tensPhysicalIndex = specs.length;
|
|
977
|
+
const tensPreset = this.#resolveVisualPreset({
|
|
978
|
+
physicalDieType: 'd6',
|
|
979
|
+
logicalDieType: 'd66',
|
|
980
|
+
termId,
|
|
981
|
+
expressionIndex,
|
|
982
|
+
dieIndex: count,
|
|
983
|
+
physicalIndex: tensPhysicalIndex,
|
|
984
|
+
component: { groupType: 'd66', role: 'tens' },
|
|
985
|
+
}, visualPresetSelector);
|
|
948
986
|
specs.push({
|
|
949
987
|
type: 'd6',
|
|
950
|
-
geometryType: this.#getPresetGeometryType(
|
|
951
|
-
preset,
|
|
988
|
+
geometryType: this.#getPresetGeometryType(tensPreset),
|
|
989
|
+
preset: tensPreset,
|
|
952
990
|
termId,
|
|
953
991
|
expressionIndex,
|
|
954
992
|
dieIndex: count,
|
|
955
|
-
physicalIndex:
|
|
993
|
+
physicalIndex: tensPhysicalIndex,
|
|
956
994
|
component: { groupId, groupType: 'd66', role: 'tens' },
|
|
957
995
|
faceLabels: D66_TENS_LABELS,
|
|
958
996
|
});
|
|
997
|
+
const unitsPhysicalIndex = specs.length;
|
|
998
|
+
const unitsPreset = this.#resolveVisualPreset({
|
|
999
|
+
physicalDieType: 'd6',
|
|
1000
|
+
logicalDieType: 'd66',
|
|
1001
|
+
termId,
|
|
1002
|
+
expressionIndex,
|
|
1003
|
+
dieIndex: count,
|
|
1004
|
+
physicalIndex: unitsPhysicalIndex,
|
|
1005
|
+
component: { groupType: 'd66', role: 'units' },
|
|
1006
|
+
}, visualPresetSelector);
|
|
959
1007
|
specs.push({
|
|
960
1008
|
type: 'd6',
|
|
961
|
-
geometryType: this.#getPresetGeometryType(
|
|
962
|
-
preset,
|
|
1009
|
+
geometryType: this.#getPresetGeometryType(unitsPreset),
|
|
1010
|
+
preset: unitsPreset,
|
|
963
1011
|
termId,
|
|
964
1012
|
expressionIndex,
|
|
965
1013
|
dieIndex: count,
|
|
966
|
-
physicalIndex:
|
|
1014
|
+
physicalIndex: unitsPhysicalIndex,
|
|
967
1015
|
component: { groupId, groupType: 'd66', role: 'units' },
|
|
968
1016
|
});
|
|
969
1017
|
}
|
|
970
1018
|
}
|
|
971
1019
|
return specs;
|
|
972
1020
|
}
|
|
1021
|
+
#resolveVisualPreset(coordinates, selector) {
|
|
1022
|
+
const defaultPreset = this.getVisualPreset(coordinates.physicalDieType);
|
|
1023
|
+
if (selector === undefined)
|
|
1024
|
+
return defaultPreset;
|
|
1025
|
+
const context = Object.freeze({
|
|
1026
|
+
...coordinates,
|
|
1027
|
+
...(coordinates.component === undefined
|
|
1028
|
+
? {}
|
|
1029
|
+
: { component: Object.freeze({ ...coordinates.component }) }),
|
|
1030
|
+
defaultPresetId: defaultPreset.id,
|
|
1031
|
+
});
|
|
1032
|
+
const selectedId = selector(context);
|
|
1033
|
+
if (selectedId === undefined)
|
|
1034
|
+
return defaultPreset;
|
|
1035
|
+
if (typeof selectedId !== 'string') {
|
|
1036
|
+
throw new TypeError('visualPresetSelector must return a preset ID or undefined');
|
|
1037
|
+
}
|
|
1038
|
+
const preset = this.#visualPresets.get(selectedId);
|
|
1039
|
+
if (preset === undefined)
|
|
1040
|
+
throw new RangeError(`Unknown visual preset: ${selectedId}`);
|
|
1041
|
+
if (preset.dieType !== coordinates.physicalDieType) {
|
|
1042
|
+
throw new RangeError(`Visual preset "${selectedId}" is for ${preset.dieType}, not ${coordinates.physicalDieType}`);
|
|
1043
|
+
}
|
|
1044
|
+
return preset;
|
|
1045
|
+
}
|
|
973
1046
|
#placeDie(generated, index, total) {
|
|
974
1047
|
if (total <= 4)
|
|
975
1048
|
return generated;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { DiceEngine } from './dice-engine.js';
|
|
2
2
|
export { DiceEngineDestroyedError, RollCancelledError, RollLimitExceededError, RollTimeoutError, TraceLimitExceededError, } from './errors.js';
|
|
3
3
|
export type { RollLimit, TraceLimit } from './errors.js';
|
|
4
|
-
export type { DiceEngineEvents, DiceEngineFacade, DiceEngineLimits, DiceEngineOptions, DiceCollisionEvent, DiceImpactEvent, DiceCollisionEventOptions, DiceMaterialType, DiceRemovalReason, DiceRemoveEvent, DiceTheme, DiceTraceLimits, DiceVisualEvent, FrameScheduler, FrameToken, PhysicalRollFrame, PhysicalRollFrameDie, PhysicalRollTrace, PhysicalRollTraceCollisionEvent, PhysicalRollTraceDie, PhysicalRollTraceEvent, PhysicalRollTraceImpactEvent, PhysicalRollTraceProducer, PhysicalRollTraceProfile, RegisterEngineVisualPresetOptions, ReplayOptions, RollOptions, SimulateOptions, } from './types.js';
|
|
4
|
+
export type { DiceEngineEvents, DiceEngineFacade, DiceEngineLimits, DiceEngineOptions, DiceCollisionEvent, DiceImpactEvent, DiceCollisionEventOptions, DiceMaterialType, DiceRemovalReason, DiceRemoveEvent, DiceTheme, DiceTraceLimits, DiceVisualEvent, FrameScheduler, FrameToken, PhysicalRollFrame, PhysicalRollFrameDie, PhysicalRollTrace, PhysicalRollTraceCollisionEvent, PhysicalRollTraceDie, PhysicalRollTraceEvent, PhysicalRollTraceImpactEvent, PhysicalRollTraceProducer, PhysicalRollTraceProfile, RegisterEngineVisualPresetOptions, ReplayOptions, RollOptions, SimulateOptions, VisualPresetSelectionContext, VisualPresetSelector, } from './types.js';
|
|
5
5
|
export { getStandardVisualPresetId, PHYSICAL_DIE_TYPES, STANDARD_VISUAL_PRESETS, type PhysicalDieType, } from './visual-presets.js';
|
|
6
6
|
export { DICE_ENGINE_VERSION } from './version.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DieResult, DieType, QuaternionLike, RandomSource, RollMode, RollResult, RollSession, Vector3Like } from '@dice-o-rolla/dice-core';
|
|
1
|
+
import type { DiceComponentRole, DieResult, DieType, PairedDiceType, QuaternionLike, RandomSource, RollMode, RollResult, RollSession, Vector3Like } from '@dice-o-rolla/dice-core';
|
|
2
2
|
import type { DicePhysicsMaterial, PhysicsWorld, SettlingOptions, ThrowGeneratorOptions, TrayOptions } from '@dice-o-rolla/dice-physics';
|
|
3
3
|
import type { DiceRenderer, RendererTheme, RendererViewport } from '@dice-o-rolla/dice-renderer';
|
|
4
4
|
import type { RegisterVisualPresetOptions, VisualPresetDescriptor } from '@dice-o-rolla/dice-renderer';
|
|
@@ -11,15 +11,34 @@ export interface FrameScheduler {
|
|
|
11
11
|
}
|
|
12
12
|
export type DiceMaterialType = RendererTheme['material'];
|
|
13
13
|
export type DiceTheme = RendererTheme;
|
|
14
|
+
export interface VisualPresetSelectionContext {
|
|
15
|
+
/** Physical polyhedron being allocated. Paired dice use d10 or d6 here. */
|
|
16
|
+
readonly physicalDieType: PhysicalDieType;
|
|
17
|
+
/** Notation-level die type. Paired components retain d100 or d66 here. */
|
|
18
|
+
readonly logicalDieType: PhysicalDieType | PairedDiceType;
|
|
19
|
+
readonly termId: string;
|
|
20
|
+
readonly expressionIndex: number;
|
|
21
|
+
readonly dieIndex: number;
|
|
22
|
+
readonly physicalIndex: number;
|
|
23
|
+
readonly defaultPresetId: string;
|
|
24
|
+
readonly component?: {
|
|
25
|
+
readonly groupType: PairedDiceType;
|
|
26
|
+
readonly role: DiceComponentRole;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Returns a registered preset ID, or undefined to retain the selected default. */
|
|
30
|
+
export type VisualPresetSelector = (context: VisualPresetSelectionContext) => string | undefined;
|
|
14
31
|
export interface RollOptions {
|
|
15
32
|
readonly mode?: RollMode;
|
|
16
33
|
readonly signal?: AbortSignal;
|
|
34
|
+
readonly visualPresetSelector?: VisualPresetSelector;
|
|
17
35
|
}
|
|
18
36
|
export interface SimulateOptions {
|
|
19
37
|
readonly seed: number;
|
|
20
38
|
readonly captureFrames?: boolean;
|
|
21
39
|
/** Capture one frame every N fixed simulation steps. */
|
|
22
40
|
readonly frameIntervalSteps?: number;
|
|
41
|
+
readonly visualPresetSelector?: VisualPresetSelector;
|
|
23
42
|
}
|
|
24
43
|
export interface ReplayOptions {
|
|
25
44
|
readonly theme?: Partial<DiceTheme>;
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Runtime package version used in portable trace provenance. */
|
|
2
|
-
export declare const DICE_ENGINE_VERSION = "0.
|
|
2
|
+
export declare const DICE_ENGINE_VERSION = "0.5.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Runtime package version used in portable trace provenance. */
|
|
2
|
-
export const DICE_ENGINE_VERSION = '0.
|
|
2
|
+
export const DICE_ENGINE_VERSION = '0.5.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dice-o-rolla/dice-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Framework-neutral physical dice engine with Rapier and Three.js browser adapters.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dice",
|
|
@@ -47,12 +47,12 @@
|
|
|
47
47
|
"registry": "https://registry.npmjs.org/"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@dice-o-rolla/dice-core": "0.
|
|
51
|
-
"@dice-o-rolla/dice-geometry": "0.
|
|
52
|
-
"@dice-o-rolla/dice-physics": "0.
|
|
53
|
-
"@dice-o-rolla/dice-physics-rapier": "0.
|
|
54
|
-
"@dice-o-rolla/dice-renderer": "0.
|
|
55
|
-
"@dice-o-rolla/dice-renderer-three": "0.
|
|
50
|
+
"@dice-o-rolla/dice-core": "0.5.0",
|
|
51
|
+
"@dice-o-rolla/dice-geometry": "0.5.0",
|
|
52
|
+
"@dice-o-rolla/dice-physics": "0.5.0",
|
|
53
|
+
"@dice-o-rolla/dice-physics-rapier": "0.5.0",
|
|
54
|
+
"@dice-o-rolla/dice-renderer": "0.5.0",
|
|
55
|
+
"@dice-o-rolla/dice-renderer-three": "0.5.0"
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|
|
58
58
|
"node": ">=20.0.0"
|