foundryvtt / unfulfilled-rolls

A module that allows individual rolls to be fulfilled by other sources
MIT License
3 stars 5 forks source link
foundryvtt

Unfulfilled Rolls

Allows individual rolls to be fulfilled by other sources

Configuring per-die resolution methods

A new Sidebar app is added called "Open Dice Configuration"

image

In this app, each User (client setting) may pick how each die is resolved - via Foundry VTT digital rolling, via manual input, or via a bluetooth provider - as well as what Bluetooth provider they are using (requires a separate module install for the bluetooth provider, such as the GoDice module).

image

An example roll

In this example, d20's and d8's are configured for Bluetooth resolution, while d4 is Foundry VTT Digital Roll.

/roll 2d20 + d8 + d4 + 4

Unfulfilled rolls works with the Foundry VTT dice rolling workflow to determine that there are terms that need resolution (the 2d20 and the d8), and displays a window - note that it gives the full roll for context, but only prompts resolution of the ones marked

image

Inside of the resolution app, the following two parameters are given:

terms The array of terms that need to be fulfilled

[
  {
    "id": "d20-1",
    "faces": 20,
    "randomValue": 12,
    "fulfillmentMethod": "bluetooth",
    "icon": "fa-dice-d20"
  },
  {
    "id": "d20-2",
    "faces": 20,
    "randomValue": 17,
    "fulfillmentMethod": "bluetooth",
    "icon": "fa-dice-d20"
  },
  {
    "id": "d8-1",
    "faces": 8,
    "randomValue": 7,
    "fulfillmentMethod": "bluetooth",
    "icon": "fa-dice-d8"
  }
]

roll The originating Roll

{
  "class": "FulfillableRoll",
  "options": {},
  "dice": [],
  "formula": "2d20 + d8 + d4 + 4",
  "terms": [
    {
      "class": "FulfillableRollTerm",
      "options": {},
      "evaluated": false,
      "number": 2,
      "faces": 20,
      "modifiers": [],
      "results": []
    },
    {
      "class": "OperatorTerm",
      "options": {},
      "evaluated": false,
      "operator": "+"
    },
    {
      "class": "FulfillableRollTerm",
      "options": {},
      "evaluated": false,
      "number": 1,
      "faces": 8,
      "modifiers": [],
      "results": []
    },
    {
      "class": "OperatorTerm",
      "options": {},
      "evaluated": false,
      "operator": "+"
    },
    {
      "class": "FulfillableRollTerm",
      "options": {},
      "evaluated": false,
      "number": 1,
      "faces": 4,
      "modifiers": [],
      "results": []
    },
    {
      "class": "OperatorTerm",
      "options": {},
      "evaluated": false,
      "operator": "+"
    },
    {
      "class": "NumericTerm",
      "options": {},
      "evaluated": false,
      "number": 4
    }
  ],
  "evaluated": true
}

Registering a new Provider

Currently only bluetooth providers are supported, but more types can be added fairly easily by following the pattern bluetooth providers uses.

Hooks.once('unfulfilled-rolls-bluetooth', function(providers) {
    return foundry.utils.mergeObject(providers, {
        "godice": {
            label: "GoDice",
            app: GodiceResolver
        }
    })
});

Implementing a Provider

Each provider needs to register an application to handle the roll resolution. Here is a simplified example:

export default class ExampleResolver extends FormApplication {

    constructor(terms, roll, callback) {
        super({});
        this.terms = terms;
        this.roll = roll;
        this.callback = callback;
    }

    /* -------------------------------------------- */

    static get defaultOptions() {
        return foundry.utils.mergeObject(super.defaultOptions, {
            id: "example-resolver",
            template: "modules/example/templates/example-resolver.hbs",
            title: "Example Resolver",
            popOut: true,
            width: 720,
            submitOnChange: false,
            submitOnClose: true,
            closeOnSubmit: true
        });
    }

    /* -------------------------------------------- */

    /** @override */
    async getData(options={}) {
        const context = await super.getData(options);

        context.terms = this.terms;
        context.roll = this.roll;

        return context;
    }

    /* -------------------------------------------- */

    /** @override */
    async _updateObject(event, formData) {
        // Turn the entries into a map
        const fulfilled = new Map();
        for ( const [id, result] of Object.entries(formData) ) {
            // Parse the result as a number
            fulfilled.set(id, Number(result));
        }
        this.callback(fulfilled);
    }
}