@dice-o-rolla/dice-engine 0.3.0 → 0.4.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 +105 -25
- 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;
|
|
@@ -320,6 +320,7 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
320
320
|
const throwGenerator = new ThrowGenerator(random, this.#throwOptions);
|
|
321
321
|
const simulationId = `simulation-${this.#nextSimulationId++}`;
|
|
322
322
|
this.#removeDisplayedDice();
|
|
323
|
+
this.#physics.clear();
|
|
323
324
|
const dice = [];
|
|
324
325
|
let trace;
|
|
325
326
|
let failure;
|
|
@@ -327,7 +328,7 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
327
328
|
this.#physics.setCollisionEventsEnabled(true);
|
|
328
329
|
this.#physics.drainCollisionEvents();
|
|
329
330
|
this.#physics.drainImpactEvents();
|
|
330
|
-
dice.push(...this.#createSimulationDice(parsed, simulationId, throwGenerator));
|
|
331
|
+
dice.push(...this.#createSimulationDice(parsed, simulationId, throwGenerator, options.visualPresetSelector));
|
|
331
332
|
const captureFrames = options.captureFrames === true;
|
|
332
333
|
const frames = [];
|
|
333
334
|
const events = [];
|
|
@@ -504,7 +505,7 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
504
505
|
if (abortListener !== undefined) {
|
|
505
506
|
options.signal?.addEventListener('abort', abortListener, { once: true });
|
|
506
507
|
}
|
|
507
|
-
|
|
508
|
+
const replay = {
|
|
508
509
|
trace,
|
|
509
510
|
resolve,
|
|
510
511
|
reject,
|
|
@@ -513,7 +514,13 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
513
514
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
514
515
|
...(abortListener === undefined ? {} : { abortListener }),
|
|
515
516
|
};
|
|
516
|
-
this.#
|
|
517
|
+
this.#replay = replay;
|
|
518
|
+
try {
|
|
519
|
+
this.#scheduleReplayFrame();
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
this.#finishReplay(replay, error);
|
|
523
|
+
}
|
|
517
524
|
return promise;
|
|
518
525
|
}
|
|
519
526
|
catch (error) {
|
|
@@ -670,7 +677,8 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
670
677
|
this.#renderer.setTheme(this.#theme);
|
|
671
678
|
this.#initialized = true;
|
|
672
679
|
}
|
|
673
|
-
#createTask(notation, parsed,
|
|
680
|
+
#createTask(notation, parsed, options) {
|
|
681
|
+
const { signal, visualPresetSelector } = options;
|
|
674
682
|
const session = {
|
|
675
683
|
id: `roll-${this.#nextSessionId++}`,
|
|
676
684
|
notation,
|
|
@@ -694,6 +702,7 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
694
702
|
reject,
|
|
695
703
|
...(signal === undefined ? {} : { signal }),
|
|
696
704
|
...(abortListener === undefined ? {} : { abortListener }),
|
|
705
|
+
...(visualPresetSelector === undefined ? {} : { visualPresetSelector }),
|
|
697
706
|
};
|
|
698
707
|
}
|
|
699
708
|
#startNext() {
|
|
@@ -724,7 +733,7 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
724
733
|
#createDice(task) {
|
|
725
734
|
const dice = [];
|
|
726
735
|
let index = 0;
|
|
727
|
-
const specs = this.#createPhysicalSpecs(task.parsed, task.session.id);
|
|
736
|
+
const specs = this.#createPhysicalSpecs(task.parsed, task.session.id, task.visualPresetSelector);
|
|
728
737
|
const totalDice = specs.length;
|
|
729
738
|
try {
|
|
730
739
|
for (const spec of specs) {
|
|
@@ -793,9 +802,9 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
793
802
|
});
|
|
794
803
|
}
|
|
795
804
|
}
|
|
796
|
-
#createSimulationDice(parsed, simulationId, throwGenerator) {
|
|
805
|
+
#createSimulationDice(parsed, simulationId, throwGenerator, visualPresetSelector) {
|
|
797
806
|
const dice = [];
|
|
798
|
-
const specs = this.#createPhysicalSpecs(parsed, simulationId);
|
|
807
|
+
const specs = this.#createPhysicalSpecs(parsed, simulationId, visualPresetSelector);
|
|
799
808
|
const totalDice = specs.length;
|
|
800
809
|
try {
|
|
801
810
|
for (const [index, spec] of specs.entries()) {
|
|
@@ -881,7 +890,7 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
881
890
|
}
|
|
882
891
|
events.push(Object.freeze(event));
|
|
883
892
|
}
|
|
884
|
-
#createPhysicalSpecs(parsed, sessionId) {
|
|
893
|
+
#createPhysicalSpecs(parsed, sessionId, visualPresetSelector) {
|
|
885
894
|
const specs = [];
|
|
886
895
|
let groupIndex = 0;
|
|
887
896
|
for (const [expressionIndex, expression] of parsed.expressions.entries()) {
|
|
@@ -894,8 +903,16 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
894
903
|
throw new RangeError(`${type} is not a standard die type`);
|
|
895
904
|
if (!isPhysicalDieType(type))
|
|
896
905
|
throw new RangeError(`${type} is not a physical die type`);
|
|
897
|
-
const preset = this.getVisualPreset(type);
|
|
898
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);
|
|
899
916
|
specs.push({
|
|
900
917
|
type,
|
|
901
918
|
geometryType: this.#getPresetGeometryType(preset),
|
|
@@ -903,7 +920,7 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
903
920
|
termId,
|
|
904
921
|
expressionIndex,
|
|
905
922
|
dieIndex: count,
|
|
906
|
-
physicalIndex
|
|
923
|
+
physicalIndex,
|
|
907
924
|
...(expression.selection === undefined ? {} : { selection: expression.selection }),
|
|
908
925
|
...(expression.score === undefined ? {} : { scoreRules: expression.score }),
|
|
909
926
|
});
|
|
@@ -913,56 +930,119 @@ export class DiceEngine extends TypedEventEmitter {
|
|
|
913
930
|
for (let count = 0; count < expression.count; count += 1) {
|
|
914
931
|
const groupId = `${sessionId}:group-${groupIndex++}`;
|
|
915
932
|
if (expression.type === 'd100') {
|
|
916
|
-
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);
|
|
917
943
|
specs.push({
|
|
918
944
|
type: 'd100',
|
|
919
|
-
geometryType: this.#getPresetGeometryType(
|
|
920
|
-
preset,
|
|
945
|
+
geometryType: this.#getPresetGeometryType(tensPreset),
|
|
946
|
+
preset: tensPreset,
|
|
921
947
|
termId,
|
|
922
948
|
expressionIndex,
|
|
923
949
|
dieIndex: count,
|
|
924
|
-
physicalIndex:
|
|
950
|
+
physicalIndex: tensPhysicalIndex,
|
|
925
951
|
component: { groupId, groupType: 'd100', role: 'tens' },
|
|
926
952
|
faceLabels: D100_TENS_LABELS,
|
|
927
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);
|
|
928
964
|
specs.push({
|
|
929
965
|
type: 'd10',
|
|
930
|
-
geometryType: this.#getPresetGeometryType(
|
|
931
|
-
preset,
|
|
966
|
+
geometryType: this.#getPresetGeometryType(unitsPreset),
|
|
967
|
+
preset: unitsPreset,
|
|
932
968
|
termId,
|
|
933
969
|
expressionIndex,
|
|
934
970
|
dieIndex: count,
|
|
935
|
-
physicalIndex:
|
|
971
|
+
physicalIndex: unitsPhysicalIndex,
|
|
936
972
|
component: { groupId, groupType: 'd100', role: 'units' },
|
|
937
973
|
});
|
|
938
974
|
continue;
|
|
939
975
|
}
|
|
940
|
-
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);
|
|
941
986
|
specs.push({
|
|
942
987
|
type: 'd6',
|
|
943
|
-
geometryType: this.#getPresetGeometryType(
|
|
944
|
-
preset,
|
|
988
|
+
geometryType: this.#getPresetGeometryType(tensPreset),
|
|
989
|
+
preset: tensPreset,
|
|
945
990
|
termId,
|
|
946
991
|
expressionIndex,
|
|
947
992
|
dieIndex: count,
|
|
948
|
-
physicalIndex:
|
|
993
|
+
physicalIndex: tensPhysicalIndex,
|
|
949
994
|
component: { groupId, groupType: 'd66', role: 'tens' },
|
|
950
995
|
faceLabels: D66_TENS_LABELS,
|
|
951
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);
|
|
952
1007
|
specs.push({
|
|
953
1008
|
type: 'd6',
|
|
954
|
-
geometryType: this.#getPresetGeometryType(
|
|
955
|
-
preset,
|
|
1009
|
+
geometryType: this.#getPresetGeometryType(unitsPreset),
|
|
1010
|
+
preset: unitsPreset,
|
|
956
1011
|
termId,
|
|
957
1012
|
expressionIndex,
|
|
958
1013
|
dieIndex: count,
|
|
959
|
-
physicalIndex:
|
|
1014
|
+
physicalIndex: unitsPhysicalIndex,
|
|
960
1015
|
component: { groupId, groupType: 'd66', role: 'units' },
|
|
961
1016
|
});
|
|
962
1017
|
}
|
|
963
1018
|
}
|
|
964
1019
|
return specs;
|
|
965
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
|
+
}
|
|
966
1046
|
#placeDie(generated, index, total) {
|
|
967
1047
|
if (total <= 4)
|
|
968
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.4.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.4.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.4.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.4.0",
|
|
51
|
+
"@dice-o-rolla/dice-geometry": "0.4.0",
|
|
52
|
+
"@dice-o-rolla/dice-physics": "0.4.0",
|
|
53
|
+
"@dice-o-rolla/dice-physics-rapier": "0.4.0",
|
|
54
|
+
"@dice-o-rolla/dice-renderer": "0.4.0",
|
|
55
|
+
"@dice-o-rolla/dice-renderer-three": "0.4.0"
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|
|
58
58
|
"node": ">=20.0.0"
|