diff --git a/daggerheart.mjs b/daggerheart.mjs index 8048f66a..8df8bf94 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -53,6 +53,8 @@ CONFIG.Canvas.rulerClass = placeables.DhRuler; CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer; CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate; +CONFIG.RollTable.documentClass = documents.DhRollTable; + CONFIG.Scene.documentClass = documents.DhScene; CONFIG.Token.documentClass = documents.DhToken; diff --git a/lang/en.json b/lang/en.json index a78ed588..ba67f14b 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2307,6 +2307,12 @@ "secondaryWeapon": "Secondary Weapon" } }, + "ROLLTABLES": { + "FIELDS": { + "formulaName": { "label": "Formula Name" } + }, + "formula": "Formula" + }, "SETTINGS": { "Appearance": { "FIELDS": { diff --git a/module/applications/sheets/rollTables/rollTable.mjs b/module/applications/sheets/rollTables/rollTable.mjs index 43524f0a..c3978988 100644 --- a/module/applications/sheets/rollTables/rollTable.mjs +++ b/module/applications/sheets/rollTables/rollTable.mjs @@ -1,21 +1,22 @@ export default class DhRollTableSheet extends foundry.applications.sheets.RollTableSheet { static DEFAULT_OPTIONS = { ...super.DEFAULT_OPTIONS, - classes:['daggerheart', 'sheet', 'dh-style'], actions: { - addAltFormula: DhRollTableSheet.#onAddAltFormula, - removeAltFormula: DhRollTableSheet.#onRemoveAltFormula + drawResult: DhRollTableSheet.#onDrawResult, + addFormula: DhRollTableSheet.#addFormula, + removeFormula: DhRollTableSheet.#removeFormula } }; static buildParts() { - const { footer, ...parts } = super.PARTS; - const test = { + const { footer, header, sheet, ...parts } = super.PARTS; + return { + sheet, + header: { template: 'systems/daggerheart/templates/sheets/rollTable/header.hbs' }, ...parts, summary: { template: 'systems/daggerheart/templates/sheets/rollTable/summary.hbs' }, footer }; - return test; } static PARTS = DhRollTableSheet.buildParts(); @@ -24,28 +25,17 @@ export default class DhRollTableSheet extends foundry.applications.sheets.RollTa context = await super._preparePartContext(partId, context, options); switch (partId) { + case 'header': + context.altFormulaOptions = { + '': { name: this.daggerheartFlag.formulaName }, + ...this.daggerheartFlag.altFormula + }; + context.activeAltFormula = this.daggerheartFlag.activeAltFormula; + break; case 'summary': - context.flagFields = this.daggerheartFlag.schema.fields; - context.flagData = this.daggerheartFlag; - const formulas =[]; - formulas.push({ - index: 0, - key: this.daggerheartFlag.formulaName, //Stored in flags as discussed - formula: context.source.formula, //Settinng default formula as part of first element - formulaInputName: "formula", - keyInputName: "flags.daggerheart.formulaName" - }); - this.daggerheartFlag.altFormula.forEach((alt,i) =>{ - formulas.push({ - index: i+1, //Logic stores not from 0 but from 1 onwards - key: alt.key, - formula: alt.formula, - formulaInputName:`flags.daggerheart.altFormula.${i}.formula`, //for .hbs - keyInputName: `flags.daggerheart.altFormula.${i}.key` - }); - }); - context.formulaList=formulas; - context.isListView=formulas.length>1; //Condition to show list view only if more than one entry + context.systemFields = this.daggerheartFlag.schema.fields; + context.altFormula = this.daggerheartFlag.altFormula; + context.formulaName = this.daggerheartFlag.formulaName; break; } @@ -55,53 +45,58 @@ export default class DhRollTableSheet extends foundry.applications.sheets.RollTa async _preRender(context, options) { await super._preRender(context, options); - if (!options.internalReferesh) + if (!options.internalRefresh) this.daggerheartFlag = new game.system.api.data.DhRollTable(this.document.flags.daggerheart); } /** @override */ async _processSubmitData(event, form, submitData, options) { - //submitData.flags.daggerheart = this.daggerheartFlag.toObject(); caused render headaches + /* RollTable sends an empty dummy event when swapping from view/edit first time */ + if (Object.keys(submitData).length) { + if (!submitData.flags.daggerheart.altFormula) submitData.flags.daggerheart.altFormula = {}; + for (const formulaKey of Object.keys(this.document._source.flags.daggerheart?.altFormula ?? {})) { + if (!submitData.flags.daggerheart.altFormula[formulaKey]) { + submitData.flags.daggerheart.altFormula[`-=${formulaKey}`] = null; + } + } + + await this.daggerheartFlag.updateSource(submitData.flags.daggerheart); + } super._processSubmitData(event, form, submitData, options); } - static async #onAddAltFormula(_event, target) { - const currentAltFormula=this.daggerheartFlag.altFormula; + /** + * Roll and draw a TableResult. + * @this {RollTableSheet} + * @type {ApplicationClickAction} + */ + static async #onDrawResult(_event, button) { + if (this.form) await this.submit({ operation: { render: false } }); + button.disabled = true; + const table = this.document; + const selectedFormula = this.daggerheartFlag.getActiveFormula(this.document.formula); + const tableRoll = await table.roll({ selectedFormula }); + const draws = table.getResultsForRoll(tableRoll.roll.total); + if (draws.length > 0) { + if (game.settings.get('core', 'animateRollTable')) await this._animateRoll(draws); + await table.draw(tableRoll); + } + + // Reenable the button if drawing with replacement since the draw won't trigger a sheet re-render + if (table.replacement) button.disabled = false; + } + + static async #addFormula() { await this.daggerheartFlag.updateSource({ - altFormula:[...currentAltFormula,{key:"",formula:""}] + [`altFormula.${foundry.utils.randomID()}`]: game.system.api.data.DhRollTable.getDefaultFormula() }); this.render({ internalRefresh: true }); } - static async #onRemoveAltFormula(_event, target) { - const visualIndex = parseInt(target.dataset.index); - // const currentAltFormula=this.daggerheartFlag.altFormula; - // if(visualIndex===0) {//If deleting formula at [0] index - // if(currentAltFormula.length>0) { //atleast 2 or more entries in altFormula - // const newCore = currentAltFormula[0]; - // const newAlt = currentAltFormula.slice(1); - // // await this.document.update({formula: newCore.formula}); - // await this.daggerheartFlag.updateSource({ - // formulaName:newCore.key, - // altFormula:newAlt - // }); - // } - // // } else { //I feel this logic is flawed for what I intended for exactly 2 entries (if one it prepares differently) - // // const newCore = currentAltFormula[0]; - // // // await this.document.update({ formula: newCore.formula }); - // // await this.daggerheartFlag.updateSource({ - // // formulaName: "", - // // altFormula: {key:"", formula: newCore.formula} }); - // // } - // } else { //normal delete that is not [0] index (1st entry) - // const arrayIndex = visualIndex - 1; - // await this.daggerheartFlag.updateSource({ - // altFormula: currentAltFormula.filter((_, i) => i !== arrayIndex) - // }); - // } + static async #removeFormula(_event, target) { await this.daggerheartFlag.updateSource({ - altFormula: this.daggerheartFlag.altFormula.filter((_, index) => index !== visualIndex) + [`altFormula.-=${target.dataset.key}`]: null }); this.render({ internalRefresh: true }); } diff --git a/module/data/rollTable.mjs b/module/data/rollTable.mjs index b1e90f51..ca24839f 100644 --- a/module/data/rollTable.mjs +++ b/module/data/rollTable.mjs @@ -7,21 +7,32 @@ export default class DhRollTable extends foundry.abstract.TypeDataModel { return { formulaName: new fields.StringField({ - // This is to give a name to go together with the core.formula required: true, nullable: false, - initial: 'Formula' // Should be a translation + initial: 'Roll Formula', + label: 'DAGGERHEART.ROLLTABLES.FIELDS.formulaName.label' }), - altFormula: new fields.ArrayField( + altFormula: new fields.TypedObjectField( new fields.SchemaField({ - key: new fields.StringField({ - required: false, + name: new fields.StringField({ + required: true, nullable: false, - blank: true + initial: 'Roll Formula', + label: 'DAGGERHEART.ROLLTABLES.FIELDS.formulaName.label' }), - formula: new FormulaField() + formula: new FormulaField({ label: 'Formula Roll', initial: '1d20' }) }) - ) + ), + activeAltFormula: new fields.StringField({ nullable: true, initial: null }) }; } + + getActiveFormula(baseFormula) { + return this.activeAltFormula ? (this.altFormula[this.activeAltFormula].formula ?? baseFormula) : baseFormula; + } + + static getDefaultFormula = () => ({ + name: game.i18n.localize('Roll Formula'), + formula: '1d20' + }); } diff --git a/module/documents/_module.mjs b/module/documents/_module.mjs index 22718bea..b946b6a5 100644 --- a/module/documents/_module.mjs +++ b/module/documents/_module.mjs @@ -4,6 +4,7 @@ export { default as DhpCombat } from './combat.mjs'; export { default as DHCombatant } from './combatant.mjs'; export { default as DhActiveEffect } from './activeEffect.mjs'; export { default as DhChatMessage } from './chatMessage.mjs'; +export { default as DhRollTable } from './rollTable.mjs'; export { default as DhScene } from './scene.mjs'; export { default as DhToken } from './token.mjs'; export { default as DhTooltipManager } from './tooltipManager.mjs'; diff --git a/module/documents/rollTable.mjs b/module/documents/rollTable.mjs new file mode 100644 index 00000000..6c8f4390 --- /dev/null +++ b/module/documents/rollTable.mjs @@ -0,0 +1,77 @@ +export default class DhRollTable extends foundry.documents.RollTable { + async roll({ selectedFormula, roll, recursive = true, _depth = 0 } = {}) { + // Prevent excessive recursion + if (_depth > 5) { + throw new Error(`Maximum recursion depth exceeded when attempting to draw from RollTable ${this.id}`); + } + + const formula = selectedFormula ?? this.formula; + + // If there is no formula, automatically calculate an even distribution + if (!this.formula) { + await this.normalize(); + } + + // Reference the provided roll formula + roll = roll instanceof Roll ? roll : Roll.create(formula); + let results = []; + + // Ensure that at least one non-drawn result remains + const available = this.results.filter(r => !r.drawn); + if (!available.length) { + ui.notifications.warn(game.i18n.localize('TABLE.NoAvailableResults')); + return { roll, results }; + } + + // Ensure that results are available within the minimum/maximum range + const minRoll = (await roll.reroll({ minimize: true })).total; + const maxRoll = (await roll.reroll({ maximize: true })).total; + const availableRange = available.reduce( + (range, result) => { + const r = result.range; + if (!range[0] || r[0] < range[0]) range[0] = r[0]; + if (!range[1] || r[1] > range[1]) range[1] = r[1]; + return range; + }, + [null, null] + ); + if (availableRange[0] > maxRoll || availableRange[1] < minRoll) { + ui.notifications.warn('No results can possibly be drawn from this table and formula.'); + return { roll, results }; + } + + // Continue rolling until one or more results are recovered + let iter = 0; + while (!results.length) { + if (iter >= 10000) { + ui.notifications.error( + `Failed to draw an available entry from Table ${this.name}, maximum iteration reached` + ); + break; + } + roll = await roll.reroll(); + results = this.getResultsForRoll(roll.total); + iter++; + } + + // Draw results recursively from any inner Roll Tables + if (recursive) { + const inner = []; + for (const result of results) { + const { type, documentUuid } = result; + const documentName = foundry.utils.parseUuid(documentUuid)?.type; + if (type === 'document' && documentName === 'RollTable') { + const innerTable = await fromUuid(documentUuid); + if (innerTable) { + const innerRoll = await innerTable.roll({ _depth: _depth + 1 }); + inner.push(...innerRoll.results); + } + } else inner.push(result); + } + results = inner; + } + + // Return the Roll and the results + return { roll, results }; + } +} diff --git a/src/packs/rolltables/tables_Consumables_tF04P02yVN1YDVel.json b/src/packs/rolltables/tables_Consumables_tF04P02yVN1YDVel.json index c2413ec3..5bbdbb61 100644 --- a/src/packs/rolltables/tables_Consumables_tF04P02yVN1YDVel.json +++ b/src/packs/rolltables/tables_Consumables_tF04P02yVN1YDVel.json @@ -1511,8 +1511,27 @@ "default": 0, "Bgvu4A6AMkRFOTGR": 3 }, - "flags": {}, - "formula": "1d60", + "flags": { + "daggerheart": { + "activeAltFormula": "", + "formulaName": "Common", + "altFormula": { + "uoUn5fRTUkyg6U2G": { + "name": "Uncommon", + "formula": "3d12" + }, + "FGxM2yoxUUUd9Eov": { + "name": "Rare", + "formula": "4d12" + }, + "HZ2hRBxu0k8IW0jC": { + "name": "Legendary", + "formula": "5d12" + } + } + } + }, + "formula": "2d12", "_id": "tF04P02yVN1YDVel", "sort": 300000, "_key": "!tables!tF04P02yVN1YDVel" diff --git a/src/packs/rolltables/tables_Loot_S61Shlt2I5CbLRjz.json b/src/packs/rolltables/tables_Loot_S61Shlt2I5CbLRjz.json index 9517eadd..71186336 100644 --- a/src/packs/rolltables/tables_Loot_S61Shlt2I5CbLRjz.json +++ b/src/packs/rolltables/tables_Loot_S61Shlt2I5CbLRjz.json @@ -1,7 +1,7 @@ { "name": "Loot", "img": "icons/commodities/treasure/brooch-gold-ruby.webp", - "description": "

To generate a random item, choose a rarity, roll the designated dice, and match the total to the item in the table:

", + "description": "

To generate a random item, choose a rarity, roll the designated dice, and match the total to the item in the table:

", "results": [ { "type": "document", @@ -1511,8 +1511,27 @@ "default": 0, "Bgvu4A6AMkRFOTGR": 3 }, - "flags": {}, - "formula": "1d60", + "flags": { + "daggerheart": { + "activeAltFormula": "", + "formulaName": "Common", + "altFormula": { + "hJJtajaMk14bYM4X": { + "name": "Uncommon", + "formula": "3d12" + }, + "yDVeXdKpG7LzjHWa": { + "name": "Rare", + "formula": "4d12" + }, + "qPHNIuUgWAHauI6V": { + "name": "Legendary", + "formula": "5d12" + } + } + } + }, + "formula": "2d12", "_id": "S61Shlt2I5CbLRjz", "sort": 200000, "_key": "!tables!S61Shlt2I5CbLRjz" diff --git a/styles/less/sheets/index.less b/styles/less/sheets/index.less index 1de1b055..0173c8e3 100644 --- a/styles/less/sheets/index.less +++ b/styles/less/sheets/index.less @@ -37,3 +37,5 @@ @import './items/feature.less'; @import './items/heritage.less'; @import './items/item-sheet-shared.less'; + +@import './rollTables/sheet.less'; diff --git a/styles/less/sheets/rollTables/sheet.less b/styles/less/sheets/rollTables/sheet.less new file mode 100644 index 00000000..9b360160 --- /dev/null +++ b/styles/less/sheets/rollTables/sheet.less @@ -0,0 +1,22 @@ +.application.sheet.roll-table-sheet { + .formulas-section { + legend { + margin-left: auto; + margin-right: auto; + } + + .formulas-container { + display: grid; + grid-template-columns: 1fr 1fr 40px; + gap: 10px; + text-align: center; + + .formula-button { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + } + } + } +} diff --git a/templates/sheets/rollTable/header.hbs b/templates/sheets/rollTable/header.hbs new file mode 100644 index 00000000..c1cd53cf --- /dev/null +++ b/templates/sheets/rollTable/header.hbs @@ -0,0 +1,19 @@ +
+ {{localize + + + +
+ +
+ +
+
+ + +
diff --git a/templates/sheets/rollTable/summary.hbs b/templates/sheets/rollTable/summary.hbs index 3390314e..4225ced7 100644 --- a/templates/sheets/rollTable/summary.hbs +++ b/templates/sheets/rollTable/summary.hbs @@ -1,43 +1,22 @@
{{formGroup fields.description value=source.description rootId=rootId}} -
- - Formula - - +
+ {{localize "DAGGERHEART.ROLLTABLES.formula"}} - {{#if isListView}} - {{#each formulaList as |row|}} -
- {{formGroup ../flagFields.formulaName - value=row.key - name=row.keyInputName - placeholder="Name" - }} - {{formGroup ../fields.formula - value=row.formula - name=row.formulaInputName - placeholder="Formula" - }} - - - -
+
+ {{localize "DAGGERHEART.ROLLTABLES.FIELDS.formulaName.label"}} + {{localize "Formula Roll"}} + + {{formInput systemFields.formulaName value=@root.formulaName name="flags.daggerheart.formulaName"}} + {{formInput fields.formula value=source.formula placeholder=formulaPlaceholder rootId=rootId}} + + {{#each @root.altFormula as | formula key |}} + {{formInput @root.systemFields.altFormula.element.fields.name value=formula.name name=(concat "flags.daggerheart.altFormula." key ".name")}} + {{formInput @root.systemFields.altFormula.element.fields.formula value=formula.formula name=(concat "flags.daggerheart.altFormula." key ".formula")}} + {{/each}} - {{else}} -
- {{!-- --}} - {{formGroup fields.formula - value=source.formula - placeholder=formulaPlaceholder - rootId=rootId - }} -
- {{/if}} +
{{formGroup fields.replacement value=source.replacement rootId=rootId}} {{formGroup fields.displayRoll value=source.displayRoll rootId=rootId}} -
\ No newline at end of file +