diff --git a/.editorconfig b/.editorconfig index 6cfef2fc..aa391e00 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,6 @@ [*] indent_size = 4 indent_style = spaces +end_of_line = lf [*.yml] indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..56ce8818 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto eol=lf +*.json text eol=lf diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 6de9e5d0..00000000 --- a/.prettierrc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "trailingComma": "none", - "tabWidth": 4, - "useTabs": false, - "semi": true, - "singleQuote": true, - "quoteProps": "consistent", - "bracketSpacing": true, - "arrowParens": "avoid", - "printWidth": 120, - "endOfLine": "lf", - "bracketSameLine": true -} diff --git a/README.md b/README.md index f59143fd..ac3666b3 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,10 @@ You can find the documentation here: https://github.com/Foundryborne/daggerheart Looking to contribute to the project? Look no further, check out our [contributing guide](CONTRIBUTING.md), and keep the [Code of Conduct](coc.md) in mind when working on things. +## AI Policy + +The Foundryborne Daggerheart system does not make use of AI (generative or otherwise) for any area of its implementation. We expect all contributors to follow this same policy when contributing with a pull request; contributions made using AI will be rejected outright. + ## Disclaimer: **Daggerheart System** diff --git a/assets/icons/documents/actors/drama-masks.svg b/assets/icons/documents/actors/drama-masks.svg new file mode 100644 index 00000000..84307da0 --- /dev/null +++ b/assets/icons/documents/actors/drama-masks.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/daggerheart.d.ts b/daggerheart.d.ts index ab754b17..7ff7fd59 100644 --- a/daggerheart.d.ts +++ b/daggerheart.d.ts @@ -1,8 +1,11 @@ import '@client/global.mjs'; +import '@common/global.mjs'; +import '@common/primitives/global.mjs'; import Canvas from '@client/canvas/board.mjs'; // Foundry's use of `Object.assign(globalThis) means many globally available objects are not read as such // This declare global hopefully fixes that +// Note: eslint is not aware of these, whatever is added here should go in the eslint's globals list declare global { /** * A simple event framework used throughout Foundry Virtual Tabletop. @@ -12,9 +15,28 @@ declare global { class Hooks extends foundry.helpers.Hooks {} const fromUuid = foundry.utils.fromUuid; const fromUuidSync = foundry.utils.fromUuidSync; - + /** + * A representation of a color in hexadecimal format. + * This class provides methods for transformations and manipulations of colors. + */ + class Color extends foundry.utils.Color {} /** * The singleton game canvas */ const canvas: Canvas; + + const ActiveEffect: foundry.documents.ActiveEffect; + const Actor: foundry.documents.Actor; + const BaseScene: foundry.documents.BaseScene; + const ChatMessage: foundry.documents.ChatMessage; + const Combat: foundry.documents.Combat; + const Combatant: foundry.documents.Combatant; + const Item: foundry.documents.Item; + const Macro: foundry.documents.Macro; + const Scene: foundry.documents.Scene; + const TokenDocument: foundry.documents.TokenDocument; + + const Collection: foundry.utils.Collection; + const FormDataExtended: foundry.applications.ux.FormDataExtended; + const TextEditor: foundry.applications.ux.TextEditor; } diff --git a/daggerheart.mjs b/daggerheart.mjs index 363430be..7bfdf874 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -196,6 +196,11 @@ Hooks.once('init', () => { makeDefault: true, label: sheetLabel('TYPES.Actor.environment') }); + Actors.registerSheet(SYSTEM.id, applications.sheets.actors.NPC, { + types: ['npc'], + makeDefault: true, + label: sheetLabel('TYPES.Actor.npc') + }); Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Party, { types: ['party'], makeDefault: true, @@ -441,3 +446,33 @@ Hooks.on('canvasTearDown', canvas => { Hooks.on('canvasReady', canas => { game.system.registeredTriggers.registerSceneTriggers(canvas.scene); }); + +/** Make the user to select a document type, instead of having a default doc type for them to accidentally keep */ +Hooks.on('renderDialogV2', (_dialog, html) => { + if (!html.classList.contains('dialog')) return; + const cls = html.classList.contains('item-create') + ? documents.DHItem.implementation + : html.classList.contains('actor-create') + ? documents.DhpActor.implementation + : null; + if (!cls) return; + + const form = html.querySelector('form'); + const submit = html.querySelector('button[type=submit]'); + const select = html.querySelector('select[name=type]'); + const nameInput = html.querySelector('input[name=name]'); + if (!form || !select || !submit || !nameInput) return; + + nameInput.placeholder = cls.defaultName({}); + const emptyOption = document.createElement('option'); + emptyOption.value = ''; + emptyOption.selected = true; + select.required = true; + select.prepend(emptyOption); + submit.addEventListener('click', event => { + if (!form.reportValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + }); +}); diff --git a/eslint.config.mjs b/eslint.config.mjs index ce2bb86f..3c9b8fd9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,14 +1,101 @@ import globals from 'globals'; -import { defineConfig } from 'eslint/config'; -import prettier from 'eslint-plugin-prettier'; +import { defineConfig, globalIgnores } from 'eslint/config'; +import tseslint from 'typescript-eslint'; +import js from '@eslint/js'; +import stylistic from '@stylistic/eslint-plugin'; + +/** @type {Partial} */ +export const stylisticRules = { + '@stylistic/indent': [ + 'error', + 4, + { + SwitchCase: 1 + } + ], + '@stylistic/max-len': ['error', { + code: 120, + ignoreComments: true, + ignoreStrings: true, + ignoreTemplateLiterals: true, + ignoreRegExpLiterals: true + }], + '@stylistic/quotes': ['error', 'single', { allowTemplateLiterals: 'always' }], + '@stylistic/arrow-parens': ['error', 'as-needed'], + '@stylistic/quote-props': ['error', 'as-needed'], + '@stylistic/array-bracket-newline': ['error', 'consistent'], + '@stylistic/key-spacing': 'error', + '@stylistic/comma-dangle': ['error', 'never'], + '@stylistic/space-in-parens': ['error', 'never'], + '@stylistic/space-infix-ops': 2, + '@stylistic/keyword-spacing': 2, + '@stylistic/semi-spacing': 2, + '@stylistic/no-multi-spaces': 2, + '@stylistic/no-extra-semi': 2, + '@stylistic/no-whitespace-before-property': 2, + '@stylistic/space-unary-ops': 2 +}; export default defineConfig([ - { files: ['**/*.{js,mjs,cjs}'], languageOptions: { globals: globals.browser } }, - { plugins: { prettier } }, + globalIgnores(['foundry/**/*', 'build/**/*']), + { + files: ['gulpfile.js', 'postcss.config.js'], + languageOptions: { globals: globals.node } + }, { files: ['**/*.{js,mjs,cjs}'], + plugins: { + '@stylistic': stylistic + }, + languageOptions: { + globals: { + ...globals.browser, + CONFIG: 'readonly', + CONST: 'readonly', + // Global classes + Color: 'readonly', + Handlebars: 'readonly', + Hooks: 'readonly', + PIXI: 'readonly', + ProseMirror: 'readonly', + Roll: 'readonly', + // global namespaces + canvas: 'readonly', + foundry: 'readonly', + game: 'readonly', + ui: 'readonly', + // global functions + fromUuid: 'readonly', + fromUuidSync: 'readonly', + getDocumentClass: 'readonly', + _del: 'readonly', + _replace: 'readonly', + _loc: 'readonly', + // Documents + ActiveEffect: 'readonly', + Actor: 'readonly', + BaseScene: 'readonly', + ChatMessage: 'readonly', + Combat: 'readonly', + Combatant: 'readonly', + Item: 'readonly', + Macro: 'readonly', + Scene: 'readonly', + TokenDocument: 'readonly', + // Other + Collection: 'readonly', + FormDataExtended: 'readonly', + TextEditor: 'readonly' + } + }, rules: { - 'prettier/prettier': 'error' + 'no-undef': 'error', + // 'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], + ...stylisticRules } + }, + { + files: ['**/*.ts'], + extends: [js.configs.recommended, tseslint.configs.recommended] } ]); diff --git a/jsconfig.json b/jsconfig.json index 00bab1f5..a0d51d0b 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "module": "ES6", - "target": "ES6", + "module": "es2022", + "target": "es2022", "paths": { "@client/*": ["./foundry/client/*"], "@common/*": ["./foundry/common/*"] diff --git a/lang/en.json b/lang/en.json index a06c46c2..3a1340e0 100755 --- a/lang/en.json +++ b/lang/en.json @@ -23,6 +23,7 @@ "companion": "Companion", "adversary": "Adversary", "environment": "Environment", + "npc": "NPC", "party": "Party" } }, @@ -333,6 +334,11 @@ }, "newAdversary": "New Adversary" }, + "NPC": { + "FIELDS": { + "motives": { "label": "Motives" } + } + }, "Party": { "Subtitle": { "character": "{community} {ancestry} | {subclass} {class}", @@ -705,19 +711,13 @@ }, "PendingReactionsDialog": { "title": "Pending Reaction Rolls Found", - "unfinishedRolls": "Some Tokens still need to roll their Reaction Roll.", - "confirmation": "Are you sure you want to continue ?", - "warning": "Undone reaction rolls will be considered as failed" + "unfinishedRolls": "Some Tokens have not finished their Reaction Rolls.", + "warning": "Unfinished reaction rolls will be considered as failed.", + "confirmation": "Are you sure you want to continue?" }, "ReactionRoll": { "title": "Reaction Roll: {trait}" }, - "RerollDialog": { - "title": "Reroll", - "damageTitle": "Reroll Damage", - "deselectDiceNotification": "Deselect one of the selected dice first", - "acceptCurrentRolls": "Accept Current Rolls" - }, "ResourceDice": { "title": "{name} Resource", "rerollDice": "Reroll Dice" @@ -3097,6 +3097,7 @@ } }, "ChatLog": { + "rerollActionRoll": "Reroll Action", "rerollDamage": "Reroll Damage", "assignTagRoll": "Assign as Tag Roll" }, diff --git a/module/applications/characterCreation/characterCreation.mjs b/module/applications/characterCreation/characterCreation.mjs index 82ca9ccb..517f95da 100644 --- a/module/applications/characterCreation/characterCreation.mjs +++ b/module/applications/characterCreation/characterCreation.mjs @@ -154,8 +154,8 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : this.tabGroups.primary !== 'equipment' - ? v.active - : false; + ? v.active + : false; v.cssClass = v.active ? 'active' : ''; switch (v.id) { @@ -211,9 +211,9 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl context.suggestedTraits = this.setup.class.system ? Object.keys(this.setup.class.system.characterGuide.suggestedTraits).map(traitKey => { - const trait = this.setup.class.system.characterGuide.suggestedTraits[traitKey]; - return `${game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${traitKey}.short`)} ${trait > 0 ? `+${trait}` : trait}`; - }) + const trait = this.setup.class.system.characterGuide.suggestedTraits[traitKey]; + return `${game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${traitKey}.short`)} ${trait > 0 ? `+${trait}` : trait}`; + }) : []; context.traits = { values: Object.keys(this.setup.traits).map(traitKey => { @@ -450,7 +450,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl if (equipment.includes(type)) presets.filter = { 'system.tier': { key: 'system.tier', value: 1 }, - 'type': { key: 'type', value: type } + type: { key: 'type', value: type } }; ui.compendiumBrowser.open(presets); diff --git a/module/applications/dialogs/_module.mjs b/module/applications/dialogs/_module.mjs index c866f1cd..fc5169b2 100644 --- a/module/applications/dialogs/_module.mjs +++ b/module/applications/dialogs/_module.mjs @@ -10,7 +10,6 @@ export { default as ImageSelectDialog } from './imageSelectDialog.mjs'; export { default as ItemTransferDialog } from './itemTransfer.mjs'; export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.mjs'; export { default as OwnershipSelection } from './ownershipSelection.mjs'; -export { default as RerollDamageDialog } from './rerollDamageDialog.mjs'; export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs'; export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs'; export { default as TagTeamDialog } from './tagTeamDialog.mjs'; diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index 76b2e751..9a98b197 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -196,14 +196,14 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio this.config.costs.indexOf(this.config.costs.find(c => c.extKey === button.dataset.key)) > -1 ? this.config.costs.filter(x => x.extKey !== button.dataset.key) : [ - ...this.config.costs, - { - extKey: button.dataset.key, - key: this.config?.data?.parent?.isNPC ? 'fear' : 'hope', - value: 1, - name: this.config.data?.system.experiences?.[button.dataset.key]?.name - } - ]; + ...this.config.costs, + { + extKey: button.dataset.key, + key: this.config?.data?.parent?.isNPC ? 'fear' : 'hope', + value: 1, + name: this.config.data?.system.experiences?.[button.dataset.key]?.name + } + ]; this.render(); } @@ -213,8 +213,8 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio this.config.actionType = this.reactionOverride ? 'reaction' : this.config.actionType === 'reaction' - ? 'action' - : this.config.actionType; + ? 'action' + : this.config.actionType; this.render(); } } diff --git a/module/applications/dialogs/damageReductionDialog.mjs b/module/applications/dialogs/damageReductionDialog.mjs index b916a5de..e5108e34 100644 --- a/module/applications/dialogs/damageReductionDialog.mjs +++ b/module/applications/dialogs/damageReductionDialog.mjs @@ -138,13 +138,13 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap const stressReductionStress = this.availableStressReductions ? stressReductions.reduce((acc, red) => acc + red.cost, 0) : 0; + const stress = this.actor.system.resources.stress; context.stress = selectedStressMarks.length > 0 || this.availableStressReductions ? { - value: - this.actor.system.resources.stress.value + selectedStressMarks.length + stressReductionStress, - max: this.actor.system.resources.stress.max - } + value: stress.value + selectedStressMarks.length + stressReductionStress, + max: stress.max + } : null; context.maxArmorUsed = maxArmorUsed; diff --git a/module/applications/dialogs/downtime.mjs b/module/applications/dialogs/downtime.mjs index 367540bf..e209cc3b 100644 --- a/module/applications/dialogs/downtime.mjs +++ b/module/applications/dialogs/downtime.mjs @@ -259,10 +259,10 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV const resetValue = increasing ? 0 : feature.system.resource.max - ? new Roll( + ? new Roll( Roll.replaceFormulaData(feature.system.resource.max, this.actor.getRollData()) ).evaluateSync().total - : 0; + : 0; await feature.update({ 'system.resource.value': resetValue }); } diff --git a/module/applications/dialogs/groupRollDialog.mjs b/module/applications/dialogs/groupRollDialog.mjs index bd45fe91..7196d848 100644 --- a/module/applications/dialogs/groupRollDialog.mjs +++ b/module/applications/dialogs/groupRollDialog.mjs @@ -106,7 +106,12 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat const context = await super._prepareContext(_options); context.isGM = game.user.isGM; - context.isEditable = this.getIsEditable(); + context.isEditable = + game.user.isGM || + this.party.system.partyMembers.some(actor => { + const selected = Boolean(this.party.system.groupRoll.participants[actor.id]); + return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER); + }); context.fields = this.party.system.schema.fields.groupRoll.fields; context.data = this.party.system.groupRoll; context.traitOptions = CONFIG.DH.ACTOR.abilities; @@ -162,8 +167,8 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat partContext.groupRoll = { totalLabel: leader?.rollData ? game.i18n.format('DAGGERHEART.GENERAL.withThing', { - thing: leader.roll.totalLabel - }) + thing: leader.roll.totalLabel + }) : null, totalDualityClass: leader?.roll?.isCritical ? 'critical' : leader?.roll?.withHope ? 'hope' : 'fear', total: leaderTotal + modifierTotal, @@ -265,13 +270,6 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat ]; } - getIsEditable() { - return this.party.system.partyMembers.some(actor => { - const selected = Boolean(this.party.system.groupRoll.participants[actor.id]); - return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER); - }); - } - groupRollRefresh = ({ refreshType, action, parts }) => { if (refreshType !== RefreshType.GroupRoll) return; @@ -358,8 +356,6 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat }); if (!result) return; - // todo: move logic to actor.rollTrait() or actor.diceRoll() - if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); const rollData = result.messageRoll.toJSON(); delete rollData.options.messageRoll; diff --git a/module/applications/dialogs/rerollDamageDialog.mjs b/module/applications/dialogs/rerollDamageDialog.mjs deleted file mode 100644 index b821bd24..00000000 --- a/module/applications/dialogs/rerollDamageDialog.mjs +++ /dev/null @@ -1,280 +0,0 @@ -const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; - -export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(message, options = {}) { - super(options); - - this.message = message; - this.damage = Object.keys(message.system.damage).reduce((acc, typeKey) => { - const type = message.system.damage[typeKey]; - acc[typeKey] = Object.keys(type.parts).reduce((acc, partKey) => { - const part = type.parts[partKey]; - acc[partKey] = Object.keys(part.dice).reduce((acc, diceKey) => { - const dice = part.dice[diceKey]; - const activeResults = dice.results.filter(x => x.active); - acc[diceKey] = { - dice: dice.dice, - selectedResults: activeResults.length, - maxSelected: activeResults.length, - results: activeResults.map(x => ({ ...x, selected: true })) - }; - - return acc; - }, {}); - - return acc; - }, {}); - - return acc; - }, {}); - } - - static DEFAULT_OPTIONS = { - id: 'reroll-dialog', - classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'reroll-dialog'], - window: { - icon: 'fa-solid fa-dice' - }, - actions: { - toggleResult: RerollDamageDialog.#toggleResult, - selectRoll: RerollDamageDialog.#selectRoll, - doReroll: RerollDamageDialog.#doReroll, - save: RerollDamageDialog.#save - } - }; - - /** @override */ - static PARTS = { - main: { - id: 'main', - template: 'systems/daggerheart/templates/dialogs/rerollDialog/damage/main.hbs' - }, - footer: { - id: 'footer', - template: 'systems/daggerheart/templates/dialogs/rerollDialog/footer.hbs' - } - }; - - get title() { - return game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.damageTitle'); - } - - _attachPartListeners(partId, htmlElement, options) { - super._attachPartListeners(partId, htmlElement, options); - - htmlElement.querySelectorAll('.to-reroll-input').forEach(element => { - element.addEventListener('change', this.toggleDice.bind(this)); - }); - } - - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - context.damage = this.damage; - context.disabledReroll = !this.getRerollDice().length; - context.saveDisabled = !this.isSelectionDone(); - - return context; - } - - static async #save() { - const update = { - 'system.damage': Object.keys(this.damage).reduce((acc, typeKey) => { - const type = this.damage[typeKey]; - let typeTotal = 0; - const messageType = this.message.system.damage[typeKey]; - const parts = Object.keys(type).map(partKey => { - const part = type[partKey]; - const messagePart = messageType.parts[partKey]; - let partTotal = messagePart.modifierTotal; - const dice = Object.keys(part).map(diceKey => { - const dice = part[diceKey]; - const total = dice.results.reduce((acc, result) => { - if (result.active) acc += result.result; - return acc; - }, 0); - partTotal += total; - const messageDice = messagePart.dice[diceKey]; - return { - ...messageDice, - total: total, - results: dice.results.map(x => ({ - ...x, - hasRerolls: dice.results.length > 1 - })) - }; - }); - - typeTotal += partTotal; - return { - ...messagePart, - total: partTotal, - dice: dice - }; - }); - - acc[typeKey] = { - ...messageType, - total: typeTotal, - parts: parts - }; - - return acc; - }, {}) - }; - - await this.message.update(update); - await this.close(); - } - - getRerollDice() { - const rerollDice = []; - Object.keys(this.damage).forEach(typeKey => { - const type = this.damage[typeKey]; - Object.keys(type).forEach(partKey => { - const part = type[partKey]; - Object.keys(part).forEach(diceKey => { - const dice = part[diceKey]; - Object.keys(dice.results).forEach(resultKey => { - const result = dice.results[resultKey]; - if (result.toReroll) { - rerollDice.push({ - ...result, - dice: dice.dice, - type: typeKey, - part: partKey, - dice: diceKey, - result: resultKey - }); - } - }); - }); - }); - }); - - return rerollDice; - } - - isSelectionDone() { - const diceFinishedData = []; - Object.keys(this.damage).forEach(typeKey => { - const type = this.damage[typeKey]; - Object.keys(type).forEach(partKey => { - const part = type[partKey]; - Object.keys(part).forEach(diceKey => { - const dice = part[diceKey]; - const selected = dice.results.reduce((acc, result) => acc + (result.active ? 1 : 0), 0); - diceFinishedData.push(selected === dice.maxSelected); - }); - }); - }); - - return diceFinishedData.every(x => x); - } - - toggleDice(event) { - const target = event.target; - const { type, part, dice } = target.dataset; - const toggleDice = this.damage[type][part][dice]; - - const existingDiceRerolls = this.getRerollDice().filter( - x => x.type === type && x.part === part && x.dice === dice - ); - - const allRerolled = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length; - - toggleDice.toReroll = !allRerolled; - toggleDice.results.forEach(result => { - if (result.active) { - result.toReroll = !allRerolled; - } - }); - - this.render(); - } - - static #toggleResult(event) { - event.stopPropagation(); - - const target = event.target.closest('.to-reroll-result'); - const { type, part, dice, result } = target.dataset; - const toggleDice = this.damage[type][part][dice]; - const toggleResult = toggleDice.results[result]; - toggleResult.toReroll = !toggleResult.toReroll; - - const existingDiceRerolls = this.getRerollDice().filter( - x => x.type === type && x.part === part && x.dice === dice - ); - - const allToReroll = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length; - toggleDice.toReroll = allToReroll; - - this.render(); - } - - static async #selectRoll(_, button) { - const { type, part, dice, result } = button.dataset; - - const diceVal = this.damage[type][part][dice]; - const diceResult = diceVal.results[result]; - if (!diceResult.active && diceVal.results.filter(x => x.active).length === diceVal.maxSelected) { - return ui.notifications.warn( - game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.deselectDiceNotification') - ); - } - - if (diceResult.active) { - diceVal.toReroll = false; - diceResult.toReroll = false; - } - - diceVal.selectedResults += diceResult.active ? -1 : 1; - diceResult.active = !diceResult.active; - - this.render(); - } - - static async #doReroll() { - const toReroll = this.getRerollDice().map(x => { - const { type, part, dice, result } = x; - const diceData = this.damage[type][part][dice].results[result]; - return { - ...diceData, - dice: this.damage[type][part][dice].dice, - typeKey: type, - partKey: part, - diceKey: dice, - resultsIndex: result - }; - }); - - const roll = await new Roll(toReroll.map(x => `1${x.dice}`).join(' + ')).evaluate(); - - if (game.modules.get('dice-so-nice')?.active) { - const diceSoNiceRoll = { - _evaluated: true, - dice: roll.dice, - options: { appearance: {} } - }; - - await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true); - } - - toReroll.forEach((data, index) => { - const { typeKey, partKey, diceKey, resultsIndex } = data; - const rerolledDice = roll.dice[index]; - - const dice = this.damage[typeKey][partKey][diceKey]; - dice.toReroll = false; - dice.results[resultsIndex].active = false; - dice.results[resultsIndex].discarded = true; - dice.results[resultsIndex].toReroll = false; - dice.results.splice(dice.results.length, 0, { - ...rerolledDice.results[0], - toReroll: false, - selected: true - }); - }); - - this.render(); - } -} diff --git a/module/applications/dialogs/rerollDialog.mjs b/module/applications/dialogs/rerollDialog.mjs deleted file mode 100644 index cae4e53a..00000000 --- a/module/applications/dialogs/rerollDialog.mjs +++ /dev/null @@ -1,279 +0,0 @@ -const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; - -export default class RerollDialog extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(message, options = {}) { - super(options); - - this.message = message; - this.damage = Object.keys(message.system.damage).reduce((acc, typeKey) => { - const type = message.system.damage[typeKey]; - acc[typeKey] = Object.keys(type.parts).reduce((acc, partKey) => { - const part = type.parts[partKey]; - acc[partKey] = Object.keys(part.dice).reduce((acc, diceKey) => { - const dice = part.dice[diceKey]; - const activeResults = dice.results.filter(x => x.active); - acc[diceKey] = { - dice: dice.dice, - selectedResults: activeResults.length, - maxSelected: activeResults.length, - results: activeResults.map(x => ({ ...x, selected: true })) - }; - - return acc; - }, {}); - - return acc; - }, {}); - - return acc; - }, {}); - } - - static DEFAULT_OPTIONS = { - id: 'reroll-dialog', - classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'reroll-dialog'], - window: { - icon: 'fa-solid fa-dice' - }, - actions: { - toggleResult: RerollDialog.#toggleResult, - selectRoll: RerollDialog.#selectRoll, - doReroll: RerollDialog.#doReroll, - save: RerollDialog.#save - } - }; - - /** @override */ - static PARTS = { - main: { - id: 'main', - template: 'systems/daggerheart/templates/dialogs/rerollDialog/main.hbs' - }, - footer: { - id: 'footer', - template: 'systems/daggerheart/templates/dialogs/rerollDialog/footer.hbs' - } - }; - - get title() { - return game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.title'); - } - - _attachPartListeners(partId, htmlElement, options) { - super._attachPartListeners(partId, htmlElement, options); - - htmlElement.querySelectorAll('.to-reroll-input').forEach(element => { - element.addEventListener('change', this.toggleDice.bind(this)); - }); - } - - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - context.damage = this.damage; - context.disabledReroll = !this.getRerollDice().length; - context.saveDisabled = !this.isSelectionDone(); - - return context; - } - - static async #save() { - const update = { - 'system.damage': Object.keys(this.damage).reduce((acc, typeKey) => { - const type = this.damage[typeKey]; - let typeTotal = 0; - const messageType = this.message.system.damage[typeKey]; - const parts = Object.keys(type).map(partKey => { - const part = type[partKey]; - const messagePart = messageType.parts[partKey]; - let partTotal = messagePart.modifierTotal; - const dice = Object.keys(part).map(diceKey => { - const dice = part[diceKey]; - const total = dice.results.reduce((acc, result) => { - if (result.active) acc += result.result; - return acc; - }, 0); - partTotal += total; - const messageDice = messagePart.dice[diceKey]; - return { - ...messageDice, - total: total, - results: dice.results.map(x => ({ - ...x, - hasRerolls: dice.results.length > 1 - })) - }; - }); - - typeTotal += partTotal; - return { - ...messagePart, - total: partTotal, - dice: dice - }; - }); - - acc[typeKey] = { - ...messageType, - total: typeTotal, - parts: parts - }; - - return acc; - }, {}) - }; - await this.message.update(update); - await this.close(); - } - - getRerollDice() { - const rerollDice = []; - Object.keys(this.damage).forEach(typeKey => { - const type = this.damage[typeKey]; - Object.keys(type).forEach(partKey => { - const part = type[partKey]; - Object.keys(part).forEach(diceKey => { - const dice = part[diceKey]; - Object.keys(dice.results).forEach(resultKey => { - const result = dice.results[resultKey]; - if (result.toReroll) { - rerollDice.push({ - ...result, - dice: dice.dice, - type: typeKey, - part: partKey, - dice: diceKey, - result: resultKey - }); - } - }); - }); - }); - }); - - return rerollDice; - } - - isSelectionDone() { - const diceFinishedData = []; - Object.keys(this.damage).forEach(typeKey => { - const type = this.damage[typeKey]; - Object.keys(type).forEach(partKey => { - const part = type[partKey]; - Object.keys(part).forEach(diceKey => { - const dice = part[diceKey]; - const selected = dice.results.reduce((acc, result) => acc + (result.active ? 1 : 0), 0); - diceFinishedData.push(selected === dice.maxSelected); - }); - }); - }); - - return diceFinishedData.every(x => x); - } - - toggleDice(event) { - const target = event.target; - const { type, part, dice } = target.dataset; - const toggleDice = this.damage[type][part][dice]; - - const existingDiceRerolls = this.getRerollDice().filter( - x => x.type === type && x.part === part && x.dice === dice - ); - - const allRerolled = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length; - - toggleDice.toReroll = !allRerolled; - toggleDice.results.forEach(result => { - if (result.active) { - result.toReroll = !allRerolled; - } - }); - - this.render(); - } - - static #toggleResult(event) { - event.stopPropagation(); - - const target = event.target.closest('.to-reroll-result'); - const { type, part, dice, result } = target.dataset; - const toggleDice = this.damage[type][part][dice]; - const toggleResult = toggleDice.results[result]; - toggleResult.toReroll = !toggleResult.toReroll; - - const existingDiceRerolls = this.getRerollDice().filter( - x => x.type === type && x.part === part && x.dice === dice - ); - - const allToReroll = existingDiceRerolls.length === toggleDice.results.length; - toggleDice.toReroll = allToReroll; - - this.render(); - } - - static async #selectRoll(_, button) { - const { type, part, dice, result } = button.dataset; - - const diceVal = this.damage[type][part][dice]; - const diceResult = diceVal.results[result]; - if (!diceResult.active && diceVal.results.filter(x => x.active).length === diceVal.maxSelected) { - return ui.notifications.warn( - game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.deselectDiceNotification') - ); - } - - if (diceResult.active) { - diceVal.toReroll = false; - diceResult.toReroll = false; - } - - diceVal.selectedResults += diceResult.active ? -1 : 1; - diceResult.active = !diceResult.active; - - this.render(); - } - - static async #doReroll() { - const toReroll = this.getRerollDice().map(x => { - const { type, part, dice, result } = x; - const diceData = this.damage[type][part][dice].results[result]; - return { - ...diceData, - dice: this.damage[type][part][dice].dice, - typeKey: type, - partKey: part, - diceKey: dice, - resultsIndex: result - }; - }); - - const roll = await new Roll(toReroll.map(x => `1${x.dice}`).join(' + ')).evaluate(); - - if (game.modules.get('dice-so-nice')?.active) { - const diceSoNiceRoll = { - _evaluated: true, - dice: roll.dice, - options: { appearance: {} } - }; - - await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true); - } - - toReroll.forEach((data, index) => { - const { typeKey, partKey, diceKey, resultsIndex } = data; - const rerolledDice = roll.dice[index]; - - const dice = this.damage[typeKey][partKey][diceKey]; - dice.toReroll = false; - dice.results[resultsIndex].active = false; - dice.results[resultsIndex].discarded = true; - dice.results[resultsIndex].toReroll = false; - dice.results.splice(dice.results.length, 0, { - ...rerolledDice.results[0], - toReroll: false, - selected: true - }); - }); - - this.render(); - } -} diff --git a/module/applications/dialogs/resourceDiceDialog.mjs b/module/applications/dialogs/resourceDiceDialog.mjs index 32e1e5d8..8394538c 100644 --- a/module/applications/dialogs/resourceDiceDialog.mjs +++ b/module/applications/dialogs/resourceDiceDialog.mjs @@ -1,4 +1,4 @@ -import { itemAbleRollParse } from '../../helpers/utils.mjs'; +import { itemAbleRollParse, triggerChatRollFx } from '../../helpers/utils.mjs'; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; @@ -69,7 +69,7 @@ export default class ResourceDiceDialog extends HandlebarsApplicationMixin(Appli const max = itemAbleRollParse(this.item.system.resource.max, this.actor, this.item); const diceFormula = `${max}${this.item.system.resource.dieFaces}`; const roll = await new Roll(diceFormula).evaluate(); - if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true); + await triggerChatRollFx([roll]); this.rollValues = roll.terms[0].results.map(x => ({ value: x.result, used: false })); this.resetUsed = true; diff --git a/module/applications/dialogs/tagTeamDialog.mjs b/module/applications/dialogs/tagTeamDialog.mjs index ba76831f..b2ce0258 100644 --- a/module/applications/dialogs/tagTeamDialog.mjs +++ b/module/applications/dialogs/tagTeamDialog.mjs @@ -116,7 +116,12 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio async _prepareContext(_options) { const context = await super._prepareContext(_options); - context.isEditable = this.getIsEditable(); + context.isEditable = + game.user.isGM || + this.party.system.partyMembers.some(actor => { + const selected = Boolean(this.party.system.tagTeam.members[actor.id]); + return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER); + }); context.fields = this.party.system.schema.fields.tagTeam.fields; context.data = this.party.system.tagTeam; context.rollTypes = CONFIG.DH.GENERAL.tagTeamRollTypes; @@ -179,57 +184,56 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio } if (Object.keys(this.party.system.tagTeam.members).includes(partId)) { - const data = this.party.system.tagTeam.members[partId]; - const actor = game.actors.get(partId); - - const rollOptions = []; - const damageRollOptions = []; - for (const item of actor.items) { - if (item.system.metadata.hasActions) { - const actions = [...item.system.actions, ...(item.system.attack ? [item.system.attack] : [])]; - for (const action of actions) { - if (action.hasRoll) { - const actionItem = { - value: action.uuid, - label: action.name, - group: item.name, - baseAction: action.baseAction - }; - - if (action.hasDamage) damageRollOptions.push(actionItem); - else rollOptions.push(actionItem); - } - } - } - } - - const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected); - const critSelected = !selectedRoll - ? undefined - : (selectedRoll?.rollData?.options?.roll?.isCritical ?? false); - - const damage = data.rollData?.options?.damage; - partContext.hasDamage |= Boolean(damage); - const critHitPointsDamage = await this.getCriticalDamage(damage); - - partContext.members[partId] = { - ...data, - roll: data.roll, - isEditable: actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER), - key: partId, - readyToRoll: Boolean(data.rollChoice), - hasRolled: Boolean(data.rollData), - rollOptions, - damageRollOptions, - damage: damage, - critDamage: critHitPointsDamage, - useCritDamage: critSelected || (critSelected === undefined && data.rollData?.options?.roll?.isCritical) - }; + const data = await this.#prepareMemberContext(partId); + partContext.hasDamage |= Boolean(data?.damage); + partContext.members[partId] = data; } return partContext; } + async #prepareMemberContext(partId) { + const data = this.party.system.tagTeam.members[partId] ?? {}; + const actor = game.actors.get(partId); + if (!actor) console.error(`Failed to get actor ${partId}`); + + const rollOptions = []; + const damageRollOptions = []; + for (const item of actor?.items ?? []) { + if (!item.system.metadata.hasActions) continue; + const actions = [...item.system.actions, ...(item.system.attack ? [item.system.attack] : [])]; + for (const action of actions) { + if (action.hasRoll) { + const collection = action.hasDamage ? damageRollOptions : rollOptions; + collection.push({ + value: action.uuid, + label: action.name, + group: item.name, + baseAction: action.baseAction + }); + } + } + } + + const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected); + const critSelected = !selectedRoll ? undefined : (selectedRoll?.rollData?.options?.roll?.isCritical ?? false); + const damage = data.rollData?.options?.damage; + + return { + ...data, + roll: data.roll, + isEditable: actor?.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER), + key: partId, + readyToRoll: Boolean(data.rollChoice), + hasRolled: Boolean(data.rollData), + rollOptions, + damageRollOptions, + damage: damage, + critDamage: await this.getCriticalDamage(damage), + useCritDamage: critSelected || (critSelected === undefined && data.rollData?.options?.roll?.isCritical) + }; + } + getUpdatingParts(target) { const { initialization, rollSelection, result } = this.constructor.PARTS; const isInitialization = this.tabGroups.application === initialization.id; @@ -273,13 +277,6 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio ); } - getIsEditable() { - return this.party.system.partyMembers.some(actor => { - const selected = Boolean(this.party.system.tagTeam.members[actor.id]); - return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER); - }); - } - tagTeamRefresh = ({ refreshType, action, parts }) => { if (refreshType !== RefreshType.TagTeamRoll) return; @@ -434,8 +431,6 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio if (!result) return; - if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); - const rollData = result.messageRoll.toJSON(); delete rollData.options.messageRoll; this.updatePartyData( @@ -651,42 +646,50 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio } async getJoinedRoll({ overrideIsCritical, displayVersion } = {}) { - const memberValues = Object.values(this.party.system.tagTeam.members); - const selectedRoll = memberValues.find(x => x.selected); - let baseMainRoll = selectedRoll ?? memberValues[0]; - let baseSecondaryRoll = selectedRoll - ? memberValues.find(x => !x.selected) - : memberValues.length > 1 - ? memberValues[1] - : null; + try { + const memberValues = Object.values(this.party.system.tagTeam.members); + const selectedRoll = memberValues.find(x => x.selected); + const baseMainRoll = selectedRoll ?? memberValues[0]; + const baseSecondaryRoll = selectedRoll + ? memberValues.find(x => !x.selected) + : memberValues.length > 1 + ? memberValues[1] + : null; - if (!baseMainRoll?.rollData || !baseSecondaryRoll) return null; + if (!baseMainRoll?.rollData || !baseSecondaryRoll) return null; - const mainRoll = new MemberData(baseMainRoll.toObject()); - const secondaryRollData = new MemberData(baseSecondaryRoll.toObject()).rollData; - const systemData = mainRoll.rollData.options; - const isCritical = overrideIsCritical ?? systemData.roll.isCritical; - if (isCritical) systemData.damage = await this.getCriticalDamage(systemData.damage); + const mainRoll = new MemberData(baseMainRoll.toObject()); + const secondaryRollData = new MemberData(baseSecondaryRoll.toObject()).rollData; + const systemData = mainRoll.rollData.options; + const isCritical = overrideIsCritical ?? systemData.roll.isCritical; + if (isCritical) systemData.damage = await this.getCriticalDamage(systemData.damage); - if (secondaryRollData?.options.hasDamage) { - const secondaryDamage = (displayVersion ? overrideIsCritical : isCritical) - ? await this.getCriticalDamage(secondaryRollData.options.damage) - : secondaryRollData.options.damage; - if (systemData.damage) { - for (const key in secondaryDamage) { - const damage = secondaryDamage[key]; - systemData.damage[key].formula = [systemData.damage[key].formula, damage.formula] - .filter(x => x) - .join(' + '); - systemData.damage[key].total += damage.total; - systemData.damage[key].parts.push(...damage.parts); + if (secondaryRollData?.options.hasDamage) { + const secondaryDamage = (displayVersion ? overrideIsCritical : isCritical) + ? await this.getCriticalDamage(secondaryRollData.options.damage) + : secondaryRollData.options.damage; + if (systemData.damage) { + for (const [key, damage] of Object.entries(secondaryDamage ?? {})) { + if (key in systemData.damage) { + systemData.damage[key].formula = [systemData.damage[key]?.formula, damage.formula] + .filter(x => x) + .join(' + '); + systemData.damage[key].total += damage.total; + systemData.damage[key].parts.push(...damage.parts); + } else { + systemData.damage[key] = damage; + } + } + } else { + systemData.damage = secondaryDamage; } - } else { - systemData.damage = secondaryDamage; } - } - return mainRoll; + return mainRoll; + } catch (err) { + console.error(err); + return null; + } } static async #onCancelRoll(_event, _button, options = { confirm: true }) { diff --git a/module/applications/hud/tokenHUD.mjs b/module/applications/hud/tokenHUD.mjs index 671b01a1..4805cd9e 100644 --- a/module/applications/hud/tokenHUD.mjs +++ b/module/applications/hud/tokenHUD.mjs @@ -50,11 +50,11 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { ).showGenericStatusEffects; context.genericStatusEffects = useGeneric ? Object.keys(context.statusEffects).reduce((acc, key) => { - const effect = context.statusEffects[key]; - if (!effect.systemEffect) acc[key] = effect; + const effect = context.statusEffects[key]; + if (!effect.systemEffect) acc[key] = effect; - return acc; - }, {}) + return acc; + }, {}) : null; context.hasCompanion = this.actor.system.companion; @@ -68,11 +68,11 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { const warning = tokensWithoutActors.length === 1 ? game.i18n.format('DAGGERHEART.UI.Notifications.tokenActorMissing', { - name: tokensWithoutActors[0].name - }) + name: tokensWithoutActors[0].name + }) : game.i18n.format('DAGGERHEART.UI.Notifications.tokenActorsMissing', { - names: tokensWithoutActors.map(x => x.name).join(', ') - }); + names: tokensWithoutActors.map(x => x.name).join(', ') + }); const tokens = canvas.tokens.controlled .filter(t => t.actor && !DHTokenHUD.#nonCombatTypes.includes(t.actor.type)) @@ -174,8 +174,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { nonZeroIndex === sideMiddle ? 0 : nonZeroIndex < sideMiddle - ? -nonZeroIndex - : nonZeroIndex - sideMiddle; + ? -nonZeroIndex + : nonZeroIndex - sideMiddle; return { x: actorX - sizeX * distance, y: actorY - sizeY * distanceCoefficient }; } else if (index < side + inbetween) { const inbetweenIndex = nonZeroIndex - side; @@ -183,8 +183,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { inbetweenIndex === inbetweenMiddle ? 0 : inbetweenIndex < inbetweenMiddle - ? -inbetweenIndex - : inbetweenIndex - inbetweenMiddle; + ? -inbetweenIndex + : inbetweenIndex - inbetweenMiddle; return { x: actorX + sizeX * distanceCoefficient, y: actorY + sizeY * distance }; } else if (index < 2 * side + inbetween) { const sideIndex = nonZeroIndex - side - inbetween; @@ -192,8 +192,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { sideIndex === sideMiddle ? 0 : sideIndex < sideMiddle - ? sideIndex - : -(sideIndex - sideMiddle); + ? sideIndex + : -(sideIndex - sideMiddle); return { x: actorX + sizeX * distance, y: actorY + sizeY * distanceCoefficient }; } else { const inbetweenIndex = nonZeroIndex - 2 * side - inbetween; @@ -201,8 +201,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { inbetweenIndex === inbetweenMiddle ? 0 : inbetweenIndex < inbetweenMiddle - ? inbetweenIndex - : -(inbetweenIndex - inbetweenMiddle); + ? inbetweenIndex + : -(inbetweenIndex - inbetweenMiddle); return { x: actorX - sizeX * distanceCoefficient, y: actorY + sizeY * distance }; } }) diff --git a/module/applications/levelup/characterLevelup.mjs b/module/applications/levelup/characterLevelup.mjs index e8d6cf1c..a2df63c1 100644 --- a/module/applications/levelup/characterLevelup.mjs +++ b/module/applications/levelup/characterLevelup.mjs @@ -210,9 +210,9 @@ export default class DhCharacterLevelUp extends LevelUpBase { achievementExperiences = level.achievements.experiences ? Object.values(level.achievements.experiences).reduce((acc, experience) => { - if (experience.name) acc.push(experience); - return acc; - }, []) + if (experience.name) acc.push(experience); + return acc; + }, []) : []; } @@ -315,15 +315,15 @@ export default class DhCharacterLevelUp extends LevelUpBase { : null; advancement[choiceKey] = multiclassItem ? { - ...multiclassItem.toObject(), - domain: checkbox.secondaryData.domain - ? game.i18n.localize( - CONFIG.DH.DOMAIN.allDomains()[checkbox.secondaryData.domain] - .label - ) - : null, - subclass: subclass ? subclass.name : null - } + ...multiclassItem.toObject(), + domain: checkbox.secondaryData.domain + ? game.i18n.localize( + CONFIG.DH.DOMAIN.allDomains()[checkbox.secondaryData.domain] + .label + ) + : null, + subclass: subclass ? subclass.name : null + } : {}; break; } diff --git a/module/applications/levelup/companionLevelup.mjs b/module/applications/levelup/companionLevelup.mjs index d6bf2d78..92cf3050 100644 --- a/module/applications/levelup/companionLevelup.mjs +++ b/module/applications/levelup/companionLevelup.mjs @@ -77,9 +77,9 @@ export default class DhCompanionLevelUp extends BaseLevelUp { achievementExperiences = level.achievements.experiences ? Object.values(level.achievements.experiences).reduce((acc, experience) => { - if (experience.name) acc.push(experience); - return acc; - }, []) + if (experience.name) acc.push(experience); + return acc; + }, []) : []; } context.achievements = { @@ -155,15 +155,15 @@ export default class DhCompanionLevelUp extends BaseLevelUp { vicious: { damage: advancement.vicious?.damage ? { - old: actorDamageDice, - new: advancement.vicious.damage - } + old: actorDamageDice, + new: advancement.vicious.damage + } : null, range: advancement.vicious?.range ? { - old: game.i18n.localize(`DAGGERHEART.CONFIG.Range.${actorRange}.name`), - new: game.i18n.localize(advancement.vicious.range.label) - } + old: game.i18n.localize(`DAGGERHEART.CONFIG.Range.${actorRange}.name`), + new: game.i18n.localize(advancement.vicious.range.label) + } : null }, simple: advancement.simple ?? {} diff --git a/module/applications/levelup/levelup.mjs b/module/applications/levelup/levelup.mjs index c4616d9a..cafc5c89 100644 --- a/module/applications/levelup/levelup.mjs +++ b/module/applications/levelup/levelup.mjs @@ -135,192 +135,6 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) context.tabs.advancements.progress = { selected: selections, max: currentLevel.maxSelections }; context.showTabs = this.tabGroups.primary !== 'summary'; break; - - const actorArmor = this.actor.system.armor; - const levelKeys = Object.keys(this.levelup.levels); - let achivementProficiency = 0; - const achievementCards = []; - let achievementExperiences = []; - for (var levelKey of levelKeys) { - const level = this.levelup.levels[levelKey]; - if (Number(levelKey) < this.levelup.startLevel) continue; - - achivementProficiency += level.achievements.proficiency ?? 0; - const cards = level.achievements.domainCards ? Object.values(level.achievements.domainCards) : null; - if (cards) { - for (var card of cards) { - const itemCard = await foundry.utils.fromUuid(card.uuid); - achievementCards.push(itemCard); - } - } - - achievementExperiences = level.achievements.experiences - ? Object.values(level.achievements.experiences).reduce((acc, experience) => { - if (experience.name) acc.push(experience); - return acc; - }, []) - : []; - } - - context.achievements = { - proficiency: { - old: this.actor.system.proficiency, - new: this.actor.system.proficiency + achivementProficiency, - shown: achivementProficiency > 0 - }, - damageThresholds: { - major: { - old: this.actor.system.damageThresholds.major, - new: this.actor.system.damageThresholds.major + changedActorLevel - currentActorLevel - }, - severe: { - old: this.actor.system.damageThresholds.severe, - new: - this.actor.system.damageThresholds.severe + - (actorArmor - ? changedActorLevel - currentActorLevel - : (changedActorLevel - currentActorLevel) * 2) - }, - unarmored: !actorArmor - }, - domainCards: { - values: achievementCards, - shown: achievementCards.length > 0 - }, - experiences: { - values: achievementExperiences - } - }; - - const advancement = {}; - for (var levelKey of levelKeys) { - const level = this.levelup.levels[levelKey]; - if (Number(levelKey) < this.levelup.startLevel) continue; - - for (var choiceKey of Object.keys(level.choices)) { - const choice = level.choices[choiceKey]; - for (var checkbox of Object.values(choice)) { - switch (choiceKey) { - case 'proficiency': - case 'hitPoint': - case 'stress': - case 'evasion': - advancement[choiceKey] = advancement[choiceKey] - ? advancement[choiceKey] + Number(checkbox.value) - : Number(checkbox.value); - break; - case 'trait': - if (!advancement[choiceKey]) advancement[choiceKey] = {}; - for (var traitKey of checkbox.data) { - if (!advancement[choiceKey][traitKey]) advancement[choiceKey][traitKey] = 0; - advancement[choiceKey][traitKey] += 1; - } - break; - case 'domainCard': - if (!advancement[choiceKey]) advancement[choiceKey] = []; - if (checkbox.data.length === 1) { - const choiceItem = await foundry.utils.fromUuid(checkbox.data[0]); - advancement[choiceKey].push(choiceItem.toObject()); - } - break; - case 'experience': - if (!advancement[choiceKey]) advancement[choiceKey] = []; - const data = checkbox.data.map(data => { - const experience = Object.keys(this.actor.system.experiences).find( - x => x === data - ); - return this.actor.system.experiences[experience]?.description ?? ''; - }); - advancement[choiceKey].push({ data: data, value: checkbox.value }); - break; - case 'subclass': - if (checkbox.data[0]) { - const subclassItem = await foundry.utils.fromUuid(checkbox.data[0]); - if (!advancement[choiceKey]) advancement[choiceKey] = []; - advancement[choiceKey].push({ - ...subclassItem.toObject(), - featureLabel: game.i18n.localize( - subclassFeatureLabels[Number(checkbox.secondaryData.featureState)] - ) - }); - } - break; - case 'multiclass': - const multiclassItem = await foundry.utils.fromUuid(checkbox.data[0]); - const subclass = multiclassItem - ? await foundry.utils.fromUuid(checkbox.secondaryData.subclass) - : null; - advancement[choiceKey] = multiclassItem - ? { - ...multiclassItem.toObject(), - domain: checkbox.secondaryData.domain - ? game.i18n.localize( - CONFIG.DH.DOMAIN.allDomains()[checkbox.secondaryData.domain] - .label - ) - : null, - subclass: subclass ? subclass.name : null - } - : {}; - break; - } - } - } - } - - context.advancements = { - statistics: { - proficiency: { - old: context.achievements.proficiency.new, - new: context.achievements.proficiency.new + (advancement.proficiency ?? 0) - }, - hitPoints: { - old: this.actor.system.resources.hitPoints.max, - new: this.actor.system.resources.hitPoints.max + (advancement.hitPoint ?? 0) - }, - stress: { - old: this.actor.system.resources.stress.max, - new: this.actor.system.resources.stress.max + (advancement.stress ?? 0) - }, - evasion: { - old: this.actor.system.evasion, - new: this.actor.system.evasion + (advancement.evasion ?? 0) - } - }, - traits: Object.keys(this.actor.system.traits).reduce((acc, traitKey) => { - if (advancement.trait?.[traitKey]) { - if (!acc) acc = {}; - acc[traitKey] = { - label: game.i18n.localize(abilities[traitKey].label), - old: this.actor.system.traits[traitKey].value, - new: this.actor.system.traits[traitKey].value + advancement.trait[traitKey] - }; - } - return acc; - }, null), - domainCards: advancement.domainCard ?? [], - experiences: - advancement.experience?.flatMap(x => x.data.map(data => ({ name: data, modifier: x.value }))) ?? - [], - multiclass: advancement.multiclass, - subclass: advancement.subclass - }; - - context.advancements.statistics.proficiency.shown = - context.advancements.statistics.proficiency.new > context.advancements.statistics.proficiency.old; - context.advancements.statistics.hitPoints.shown = - context.advancements.statistics.hitPoints.new > context.advancements.statistics.hitPoints.old; - context.advancements.statistics.stress.shown = - context.advancements.statistics.stress.new > context.advancements.statistics.stress.old; - context.advancements.statistics.evasion.shown = - context.advancements.statistics.evasion.new > context.advancements.statistics.evasion.old; - context.advancements.statistics.shown = - context.advancements.statistics.proficiency.shown || - context.advancements.statistics.hitPoints.shown || - context.advancements.statistics.stress.shown || - context.advancements.statistics.evasion.shown; - - break; } return context; @@ -358,14 +172,14 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases'); if (experienceIncreaseTagify) { const allExperiences = { - ...this.actor.system.experiences, ...Object.values(this.levelup.levels).reduce((acc, level) => { for (const key of Object.keys(level.achievements.experiences)) { acc[key] = level.achievements.experiences[key]; } return acc; - }, {}) + }, {}), + ...this.actor.system.experiences }; tagifyElement( experienceIncreaseTagify, @@ -384,37 +198,35 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) this._dragDrop.forEach(d => d.bind(htmlElement)); } - tagifyUpdate = - type => - async (_, { option, removed }) => { - const updatePath = Object.keys(this.levelup.levels[this.levelup.currentLevel].choices).reduce( - (acc, choiceKey) => { - const choice = this.levelup.levels[this.levelup.currentLevel].choices[choiceKey]; - Object.keys(choice).forEach(checkboxNr => { - const checkbox = choice[checkboxNr]; - if ( - choiceKey === type && - (removed ? checkbox.data.includes(option) : checkbox.data.length < checkbox.amount) - ) { - acc = `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}.data`; - } - }); + tagifyUpdate = type => async (_, { option, removed }) => { + const updatePath = Object.keys(this.levelup.levels[this.levelup.currentLevel].choices).reduce( + (acc, choiceKey) => { + const choice = this.levelup.levels[this.levelup.currentLevel].choices[choiceKey]; + Object.keys(choice).forEach(checkboxNr => { + const checkbox = choice[checkboxNr]; + if ( + choiceKey === type && + (removed ? checkbox.data.includes(option) : checkbox.data.length < checkbox.amount) + ) { + acc = `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}.data`; + } + }); - return acc; - }, - null - ); + return acc; + }, + null + ); - if (!updatePath) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noSelectionsLeft')); - return; - } + if (!updatePath) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noSelectionsLeft')); + return; + } - const currentData = foundry.utils.getProperty(this.levelup, updatePath); - const updatedData = removed ? currentData.filter(x => x !== option) : [...currentData, option]; - await this.levelup.updateSource({ [updatePath]: updatedData }); - this.render(); - }; + const currentData = foundry.utils.getProperty(this.levelup, updatePath); + const updatedData = removed ? currentData.filter(x => x !== option) : [...currentData, option]; + await this.levelup.updateSource({ [updatePath]: updatedData }); + this.render(); + }; static async updateForm(event, _, formData) { const { levelup } = foundry.utils.expandObject(formData.object); @@ -593,10 +405,10 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) const domainCards = this.levelup.levels[this.levelup.currentLevel].achievements.domainCards; const illegalDomainCards = option.secondaryData.domain ? Object.keys(domainCards) - .map(key => ({ ...domainCards[key], key })) - .filter( - x => x.uuid && foundry.utils.fromUuidSync(x.uuid).system.domain === option.secondaryData.domain - ) + .map(key => ({ ...domainCards[key], key })) + .filter( + x => x.uuid && foundry.utils.fromUuidSync(x.uuid).system.domain === option.secondaryData.domain + ) : []; illegalDomainCards.forEach(card => { update[`levels.${this.levelup.currentLevel}.achievements.domainCards.${card.key}.uuid`] = null; diff --git a/module/applications/settings/homebrewSettings.mjs b/module/applications/settings/homebrewSettings.mjs index 40ea0301..c4dfc397 100644 --- a/module/applications/settings/homebrewSettings.mjs +++ b/module/applications/settings/homebrewSettings.mjs @@ -111,7 +111,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli switch (partId) { case 'domains': - const selectedDomain = this.selected.domain ? this.settings.domains[this.selected.domain] : null; + const selectedDomain = this.settings.domains[this.selected.domain] ?? null; const enrichedDescription = selectedDomain ? await foundry.applications.ux.TextEditor.implementation.enrichHTML(selectedDomain.description) : null; @@ -251,8 +251,8 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli const configTitle = isDowntime ? game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.downtimeMove') : type === 'armorFeatures' - ? game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.armorFeature') - : game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.weaponFeature'); + ? game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.armorFeature') + : game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.weaponFeature'); const editedBase = await game.system.api.applications.sheetConfigs.SettingFeatureConfig.configure( configTitle, diff --git a/module/applications/sheets-configs/_module.mjs b/module/applications/sheets-configs/_module.mjs index 4b83a042..9528a424 100644 --- a/module/applications/sheets-configs/_module.mjs +++ b/module/applications/sheets-configs/_module.mjs @@ -2,6 +2,7 @@ export { default as ActionConfig } from './action-config.mjs'; export { default as ActionSettingsConfig } from './action-settings-config.mjs'; export { default as CharacterSettings } from './character-settings.mjs'; export { default as AdversarySettings } from './adversary-settings.mjs'; +export { default as NPCSettings } from './npc-settings.mjs'; export { default as CompanionSettings } from './companion-settings.mjs'; export { default as SettingFeatureConfig } from './setting-feature-config.mjs'; export { default as EnvironmentSettings } from './environment-settings.mjs'; diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs index e83dfae4..b65e1cdf 100644 --- a/module/applications/sheets-configs/action-base-config.mjs +++ b/module/applications/sheets-configs/action-base-config.mjs @@ -204,7 +204,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) }; } - if (this.action.parent.metadata.isInventoryItem) { + if (this.action.parent.metadata?.isInventoryItem) { options.quantity = { label: 'DAGGERHEART.GENERAL.itemQuantity', group: 'Global' diff --git a/module/applications/sheets-configs/activeEffectConfig.mjs b/module/applications/sheets-configs/activeEffectConfig.mjs index cb9f1701..2d9eafc3 100644 --- a/module/applications/sheets-configs/activeEffectConfig.mjs +++ b/module/applications/sheets-configs/activeEffectConfig.mjs @@ -41,7 +41,7 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac * @returns {ChangeChoice { value: string, label: string, hint: string, group: string }[]} */ static getChangeChoices() { - const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty']; + const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty', 'DhNPC']; const getAllLeaves = (root, group, parentPath = '') => { const leaves = []; diff --git a/module/applications/sheets-configs/npc-settings.mjs b/module/applications/sheets-configs/npc-settings.mjs new file mode 100644 index 00000000..c187877c --- /dev/null +++ b/module/applications/sheets-configs/npc-settings.mjs @@ -0,0 +1,85 @@ +import DHBaseActorSettings from '../sheets/api/actor-setting.mjs'; + +/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ + +export default class DHNPCSettings extends DHBaseActorSettings { + /**@inheritdoc */ + static DEFAULT_OPTIONS = { + classes: ['npc-settings'], + position: { width: 455, height: 'auto' }, + actions: {}, + dragDrop: [ + { dragSelector: null, dropSelector: '.tab.features' }, + { dragSelector: '.feature-item', dropSelector: null } + ] + }; + + /**@override */ + static PARTS = { + header: { + id: 'header', + template: 'systems/daggerheart/templates/sheets-settings/npc-settings/header.hbs' + }, + tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, + details: { + id: 'details', + template: 'systems/daggerheart/templates/sheets-settings/npc-settings/details.hbs' + }, + features: { + id: 'features', + template: 'systems/daggerheart/templates/sheets-settings/npc-settings/features.hbs' + } + }; + + /** @override */ + static TABS = { + primary: { + tabs: [{ id: 'details' }, { id: 'features' }], + initial: 'details', + labelPrefix: 'DAGGERHEART.GENERAL.Tabs' + } + }; + + async _prepareContext(options) { + const context = await super._prepareContext(options); + + const featureForms = ['passive', 'action', 'reaction']; + context.features = context.document.system.features.sort((a, b) => + a.system.featureForm !== b.system.featureForm + ? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm) + : a.sort - b.sort + ); + + return context; + } + + /* -------------------------------------------- */ + + async _onDragStart(event) { + const featureItem = event.currentTarget.closest('.feature-item'); + + if (featureItem) { + const feature = this.actor.items.get(featureItem.id); + const featureData = { type: 'Item', uuid: feature.uuid, fromInternal: true }; + event.dataTransfer.setData('text/plain', JSON.stringify(featureData)); + event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0); + } + } + + async _onDrop(event) { + event.stopPropagation(); + const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); + + const item = await fromUuid(data.uuid); + if (item?.type === 'feature') { + if (data.fromInternal && item.parent?.uuid === this.actor.uuid) { + return; + } + + const itemData = item.toObject(); + delete itemData._id; + + await this.actor.createEmbeddedDocuments('Item', [itemData]); + } + } +} diff --git a/module/applications/sheets-configs/setting-feature-config.mjs b/module/applications/sheets-configs/setting-feature-config.mjs index a5bcc4f9..531158cd 100644 --- a/module/applications/sheets-configs/setting-feature-config.mjs +++ b/module/applications/sheets-configs/setting-feature-config.mjs @@ -168,8 +168,8 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App updatedEffects = deleteEffect ? currentEffects.filter(x => x.id !== effectData.id) : existingEffectIndex === -1 - ? [...currentEffects, effectData] - : currentEffects.with(existingEffectIndex, effectData); + ? [...currentEffects, effectData] + : currentEffects.with(existingEffectIndex, effectData); await this.updateMove({ [`${this.movePath}.effects`]: updatedEffects }); @@ -235,9 +235,9 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App return this.hasEffects ? tabs : Object.keys(tabs).reduce((acc, key) => { - if (key !== 'effects') acc[key] = tabs[key]; - return acc; - }, {}); + if (key !== 'effects') acc[key] = tabs[key]; + return acc; + }, {}); } /** @override */ diff --git a/module/applications/sheets/actors/_module.mjs b/module/applications/sheets/actors/_module.mjs index c4ea2d94..1a2bebfb 100644 --- a/module/applications/sheets/actors/_module.mjs +++ b/module/applications/sheets/actors/_module.mjs @@ -2,4 +2,5 @@ export { default as Adversary } from './adversary.mjs'; export { default as Character } from './character.mjs'; export { default as Companion } from './companion.mjs'; export { default as Environment } from './environment.mjs'; +export { default as NPC } from './npc.mjs'; export { default as Party } from './party.mjs'; diff --git a/module/applications/sheets/actors/adversary.mjs b/module/applications/sheets/actors/adversary.mjs index 04be3efb..06dd4a0f 100644 --- a/module/applications/sheets/actors/adversary.mjs +++ b/module/applications/sheets/actors/adversary.mjs @@ -31,6 +31,16 @@ export default class AdversarySheet extends DHBaseActorSheet { dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]', dropSelector: null } + ], + contextMenus: [ + { + handler: DHBaseActorSheet.getBaseAttackContextOptions, + selector: '[data-item-uuid][data-type="attack"]', + options: { + parentClassHooks: false, + fixed: true + } + } ] }; diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 19b82712..bc2cdb41 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -65,6 +65,14 @@ export default class CharacterSheet extends DHBaseActorSheet { fixed: true } }, + { + handler: DHBaseActorSheet.getBaseAttackContextOptions, + selector: '[data-item-uuid][data-type="attack"]', + options: { + parentClassHooks: false, + fixed: true + } + }, { handler: CharacterSheet.#getDomainCardContextOptions, selector: '[data-item-uuid][data-type="domainCard"]', @@ -777,11 +785,11 @@ export default class CharacterSheet extends DHBaseActorSheet { filter: key === 'subclasses' ? { - 'system.linkedClass.uuid': { - key: 'system.linkedClass.uuid', - value: this.document.system.class.value?._stats.compendiumSource - } - } + 'system.linkedClass.uuid': { + key: 'system.linkedClass.uuid', + value: this.document.system.class.value?._stats.compendiumSource + } + } : undefined, render: { noFolder: true @@ -1045,7 +1053,7 @@ export default class CharacterSheet extends DHBaseActorSheet { game.tooltip.activate(target, { html, locked: true, - cssClass: 'bordered-tooltip', + cssClass: 'bordered-tooltip dh-style', direction: 'DOWN' }); @@ -1141,7 +1149,7 @@ export default class CharacterSheet extends DHBaseActorSheet { game.tooltip.activate(target, { html, locked: true, - cssClass: 'bordered-tooltip', + cssClass: 'bordered-tooltip dh-style', direction: 'DOWN', noOffset: true }); diff --git a/module/applications/sheets/actors/companion.mjs b/module/applications/sheets/actors/companion.mjs index b30b9c07..a01b4a64 100644 --- a/module/applications/sheets/actors/companion.mjs +++ b/module/applications/sheets/actors/companion.mjs @@ -11,7 +11,17 @@ export default class DhCompanionSheet extends DHBaseActorSheet { toggleStress: DhCompanionSheet.#toggleStress, actionRoll: DhCompanionSheet.#actionRoll, levelManagement: DhCompanionSheet.#levelManagement - } + }, + contextMenus: [ + { + handler: DHBaseActorSheet.getBaseAttackContextOptions, + selector: '[data-item-uuid][data-type="attack"]', + options: { + parentClassHooks: false, + fixed: true + } + } + ] }; static PARTS = { diff --git a/module/applications/sheets/actors/npc.mjs b/module/applications/sheets/actors/npc.mjs new file mode 100644 index 00000000..8c9048c2 --- /dev/null +++ b/module/applications/sheets/actors/npc.mjs @@ -0,0 +1,136 @@ +import DHBaseActorSheet from '../api/base-actor.mjs'; + +export default class NPCSheet extends DHBaseActorSheet { + /** @inheritDoc */ + static DEFAULT_OPTIONS = { + classes: ['npc'], + position: { width: 660, height: 600 }, + window: { resizable: true }, + actions: {}, + window: { + resizable: true, + controls: [ + { + icon: 'fa-solid fa-signature', + label: 'DAGGERHEART.UI.Tooltip.configureAttribution', + action: 'editAttribution' + } + ] + }, + dragDrop: [ + { + dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]', + dropSelector: null + } + ] + }; + + static PARTS = { + header: { template: 'systems/daggerheart/templates/sheets/actors/npc/header.hbs' }, + tabs: { template: 'systems/daggerheart/templates/sheets/actors/npc/navigation.hbs' }, + features: { + template: 'systems/daggerheart/templates/sheets/actors/npc/features.hbs', + scrollable: ['.feature-section'] + }, + notes: { + template: 'systems/daggerheart/templates/sheets/actors/npc/notes.hbs' + } + }; + + /** @inheritdoc */ + static TABS = { + primary: { + tabs: [{ id: 'notes' }, { id: 'features' }], + initial: 'notes', + labelPrefix: 'DAGGERHEART.GENERAL.Tabs' + } + }; + + /** @inheritdoc */ + _prepareTabs(group) { + const result = super._prepareTabs(group); + if (group === 'primary') { + result.features.empty = this.document.system.features.length === 0; + } + return result; + } + + /** @inheritdoc */ + async _preparePartContext(partId, context, options) { + context = await super._preparePartContext(partId, context, options); + switch (partId) { + case 'header': + await this._prepareHeaderContext(context, options); + break; + case 'features': + await this._prepareFeaturesContext(context, options); + break; + case 'notes': + await this._prepareNotesContext(context, options); + break; + } + + return context; + } + + /** + * Prepare render context for the Header part. + * @param {ApplicationRenderContext} context + * @param {ApplicationRenderOptions} options + * @returns {Promise} + * @protected + */ + async _prepareHeaderContext(context, _options) { + const { system } = this.document; + const { TextEditor } = foundry.applications.ux; + + context.description = await TextEditor.implementation.enrichHTML(system.description, { + secrets: this.document.isOwner, + relativeTo: this.document + }); + } + + /** + * Prepare render context for the Features part. + * @param {ApplicationRenderContext} context + * @param {ApplicationRenderOptions} options + * @returns {Promise} + * @protected + */ + async _prepareFeaturesContext(context, _options) { + const featureForms = ['passive', 'action', 'reaction']; + context.features = this.document.system.features.sort((a, b) => + a.system.featureForm !== b.system.featureForm + ? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm) + : a.sort - b.sort + ); + } + + /** + * Prepare render context for the Biography part. + * @param {ApplicationRenderContext} context + * @param {ApplicationRenderOptions} options + * @returns {Promise} + * @protected + */ + async _prepareNotesContext(context, _options) { + const { system } = this.document; + const { TextEditor } = foundry.applications.ux; + + const paths = { + notes: 'notes' + }; + + for (const [key, path] of Object.entries(paths)) { + const value = foundry.utils.getProperty(system, path); + context[key] = { + field: system.schema.getField(path), + value, + enriched: await TextEditor.implementation.enrichHTML(value, { + secrets: this.document.isOwner, + relativeTo: this.document + }) + }; + } + } +} diff --git a/module/applications/sheets/actors/party.mjs b/module/applications/sheets/actors/party.mjs index cec1e1f0..3af8ea5f 100644 --- a/module/applications/sheets/actors/party.mjs +++ b/module/applications/sheets/actors/party.mjs @@ -47,11 +47,6 @@ export default class Party extends DHBaseActorSheet { template: 'systems/daggerheart/templates/sheets/actors/party/party-members.hbs', scrollable: [''] }, - /* NOT YET IMPLEMENTED */ - // projects: { - // template: 'systems/daggerheart/templates/sheets/actors/party/projects.hbs', - // scrollable: [''] - // }, inventory: { template: 'systems/daggerheart/templates/sheets/actors/party/inventory.hbs', scrollable: ['.tab.inventory .items-section'] @@ -62,19 +57,13 @@ export default class Party extends DHBaseActorSheet { /** @inheritdoc */ static TABS = { primary: { - tabs: [ - { id: 'partyMembers' }, - /* NOT YET IMPLEMENTED */ - // { id: 'projects' }, - { id: 'inventory' }, - { id: 'notes' } - ], + tabs: [{ id: 'partyMembers' }, { id: 'inventory' }, { id: 'notes' }], initial: 'partyMembers', labelPrefix: 'DAGGERHEART.GENERAL.Tabs' } }; - static ALLOWED_ACTOR_TYPES = ['character', 'companion', 'adversary']; + static ALLOWED_ACTOR_TYPES = ['character', 'companion', 'adversary', 'npc']; static DICE_ROLL_ACTOR_TYPES = ['character']; async _onRender(context, options) { @@ -173,9 +162,9 @@ export default class Party extends DHBaseActorSheet { difficulty: actor.system.difficulty, traits: actor.system.traits ? traits.map(t => ({ - label: game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${t}.short`), - value: actor.system.traits[t].value - })) + label: game.i18n.localize(`DAGGERHEART.CONFIG.Traits.${t}.short`), + value: actor.system.traits[t].value + })) : null, weapons }); @@ -317,7 +306,7 @@ export default class Party extends DHBaseActorSheet { static async downtimeMoveQuery({ actorId, downtimeType }) { const actor = await foundry.utils.fromUuid(actorId); - if (!actor || !actor?.isOwner) reject(); + if (!actor || !actor?.isOwner) return; new game.system.api.applications.dialogs.Downtime(actor, downtimeType === 'shortRest').render({ force: true }); diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 2b0c3e55..63bbb536 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -722,10 +722,10 @@ export default function DHApplicationMixin(Base) { const parent = featureOnCharacter ? this.document.parent : parentIsItem && documentClass === 'Item' - ? type === 'action' - ? this.document.system - : null - : this.document; + ? type === 'action' + ? this.document.system + : null + : this.document; let systemData = {}; if (featureOnCharacter) { diff --git a/module/applications/sheets/api/base-actor.mjs b/module/applications/sheets/api/base-actor.mjs index 5cd0f6a5..812ad311 100644 --- a/module/applications/sheets/api/base-actor.mjs +++ b/module/applications/sheets/api/base-actor.mjs @@ -189,6 +189,43 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true }); } + /** + * Get the set of ContextMenu options for the base attack. + * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance + * @this {CharacterSheet} + * @protected + */ + static getBaseAttackContextOptions() { + /**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */ + return [ + { + label: 'DAGGERHEART.CONFIG.RollTypes.attack.name', + icon: 'fa-solid fa-burst', + onClick: async (event, target) => (await getDocFromElement(target)).use(event) + }, + { + label: 'DAGGERHEART.GENERAL.damage', + icon: 'fa-solid fa-explosion', + onClick: async (event, target) => { + const doc = await getDocFromElement(target), + action = doc?.system?.attack ?? doc; + const config = action.prepareConfig(event); + config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects( + this.document, + doc + ); + config.hasRoll = false; + return action && action.workflow.get('damage').execute(config, null, true); + } + }, + { + label: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat', + icon: 'fa-solid fa-message', + onClick: async (_, target) => (await getDocFromElement(target)).toChat(this.document.uuid) + } + ]; + } + /* -------------------------------------------- */ /* Application Listener Actions */ /* -------------------------------------------- */ @@ -340,7 +377,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { action: 'update', documentName: 'Item', parent: targetActor, - updates: [{ '_id': existing.id, 'system.quantity': existing.system.quantity + quantity }] + updates: [{ _id: existing.id, 'system.quantity': existing.system.quantity + quantity }] }); } else { const itemsToCreate = []; @@ -373,7 +410,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { action: 'update', documentName: 'Item', parent: originActor, - updates: [{ '_id': item.id, 'system.quantity': item.system.quantity - quantity }] + updates: [{ _id: item.id, 'system.quantity': item.system.quantity - quantity }] }); } diff --git a/module/applications/sheets/api/item-attachment-sheet.mjs b/module/applications/sheets/api/item-attachment-sheet.mjs index bcf2fc3c..ea4d5352 100644 --- a/module/applications/sheets/api/item-attachment-sheet.mjs +++ b/module/applications/sheets/api/item-attachment-sheet.mjs @@ -29,16 +29,6 @@ export default function ItemAttachmentSheet(Base) { } }; - async _preparePartContext(partId, context) { - await super._preparePartContext(partId, context); - - if (partId === 'attachments') { - context.attachedItems = await prepareAttachmentContext(this.document); - } - - return context; - } - async _onDrop(event) { const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); diff --git a/module/applications/sidebar/tabs/actorDirectory.mjs b/module/applications/sidebar/tabs/actorDirectory.mjs index 89da1426..a6f48b54 100644 --- a/module/applications/sidebar/tabs/actorDirectory.mjs +++ b/module/applications/sidebar/tabs/actorDirectory.mjs @@ -13,8 +13,8 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs. return document.type === 'adversary' ? game.i18n.localize(adversaryTypes[document.system.type]?.label ?? 'TYPES.Actor.adversary') : document.type === 'environment' - ? game.i18n.localize(environmentTypes[document.system.type]?.label ?? 'TYPES.Actor.environment') - : null; + ? game.i18n.localize(environmentTypes[document.system.type]?.label ?? 'TYPES.Actor.environment') + : null; }; } diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index 34b25591..199ee87d 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -41,8 +41,8 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo const advantage = rollCommand.advantage ? CONFIG.DH.ACTIONS.advantageState.advantage.value : rollCommand.disadvantage - ? CONFIG.DH.ACTIONS.advantageState.disadvantage.value - : undefined; + ? CONFIG.DH.ACTIONS.advantageState.disadvantage.value + : undefined; const difficulty = rollCommand.difficulty; const grantResources = rollCommand.grantResources; @@ -50,8 +50,8 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo const title = (flavor ?? traitValue) ? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { - ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label) - }) + ability: game.i18n.localize(CONFIG.DH.ACTOR.abilities[traitValue].label) + }) : game.i18n.localize('DAGGERHEART.GENERAL.duality'); enrichedDualityRoll({ @@ -103,6 +103,19 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo _getEntryContextOptions() { return [ ...super._getEntryContextOptions(), + { + label: 'DAGGERHEART.UI.ChatLog.rerollActionRoll', + icon: '', + visible: li => { + const message = game.messages.get(li.dataset.messageId); + return message.system.hasRoll && (game.user.isGM || message.isAuthor); + }, + callback: async li => { + const message = game.messages.get(li.dataset.messageId); + const reroll = await message.rolls[0].reroll({ liveRoll: true }); + message.update({ rolls: [reroll] }); + } + }, { label: 'DAGGERHEART.UI.ChatLog.rerollDamage', icon: '', @@ -113,9 +126,10 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo : false; return (game.user.isGM || message.isAuthor) && hasRolledDamage; }, - callback: li => { + callback: async li => { const message = game.messages.get(li.dataset.messageId); - new game.system.api.applications.dialogs.RerollDamageDialog(message).render({ force: true }); + const update = await message.system.getRerolledDamage(); + message.update(update); } } ]; diff --git a/module/applications/ui/combatTracker.mjs b/module/applications/ui/combatTracker.mjs index 0989bcb8..25f3e06b 100644 --- a/module/applications/ui/combatTracker.mjs +++ b/module/applications/ui/combatTracker.mjs @@ -84,19 +84,49 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C }); } + /** + * Open the dialog used to edit the name of the currently viewed Combat encounter. + * @this {CombatTracker} + * @returns {Promise} + */ + static async #onEditName() { + const combat = this.viewed; + if (!combat || !game.user.isGM) return null; + const field = combat.schema.fields.name; + const inputHTML = field.toFormGroup({}, { name: 'name', value: combat.name, autofocus: true }).outerHTML; + const formData = await foundry.applications.api.DialogV2.input({ + window: { icon: 'fa-solid fa-tag', title: 'COMBAT.ACTIONS.EditNameTitle' }, + position: { width: 480 }, + content: inputHTML + }); + await combat.update({ name: formData.name || '' }); + } + _getCombatContextOptions() { return [ { - label: 'COMBAT.ClearMovementHistories', - icon: '', - visible: () => game.user.isGM && this.viewed?.combatants.size > 0, - callback: () => this.viewed.clearMovementHistories() + label: 'COMBAT.ACTIONS.EditName', + icon: 'fa-solid fa-tag', + visible: () => game.user.isGM && !!this.viewed, + onClick: () => DhCombatTracker.#onEditName.call(this) }, { - label: 'COMBAT.Delete', - icon: '', + label: 'COMBAT.ACTIONS.LinkToScene', + icon: '', + visible: () => game.user.isGM && !this.scene, + onClick: () => this.viewed.toggleSceneLink() + }, + { + label: 'COMBAT.ACTIONS.UnlinkFromScene', + icon: '', + visible: () => game.user.isGM && !!this.scene, + onClick: () => this.viewed.toggleSceneLink() + }, + { + label: 'COMBAT.End', + icon: 'fa-solid fa-xmark', visible: () => game.user.isGM && !!this.viewed, - callback: () => this.viewed.endCombat() + onClick: () => this.viewed.endCombat() } ]; } @@ -133,7 +163,7 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'), type: combatant.actor?.system?.type, img: await this._getCombatantThumbnail(combatant), - disposition: combatant.token.disposition + disposition: combatant.token?.disposition }; turn.css = [turn.active ? 'active' : null, hidden ? 'hide' : null, isDefeated ? 'defeated' : null].filterJoin( diff --git a/module/applications/ui/countdownEdit.mjs b/module/applications/ui/countdownEdit.mjs index b418107c..5974e1f2 100644 --- a/module/applications/ui/countdownEdit.mjs +++ b/module/applications/ui/countdownEdit.mjs @@ -56,8 +56,8 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio ? countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id ? 'DAGGERHEART.UI.Countdowns.increasingLoop' : countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id - ? 'DAGGERHEART.UI.Countdowns.decreasingLoop' - : 'DAGGERHEART.UI.Countdowns.loop' + ? 'DAGGERHEART.UI.Countdowns.decreasingLoop' + : 'DAGGERHEART.UI.Countdowns.loop' : null; const randomizeValid = !new Roll(countdown.progress.startFormula ?? '').isDeterministic; acc[key] = { @@ -148,11 +148,11 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio } async gmSetSetting(data) { - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data), - game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.Refresh, - data: { refreshType: RefreshType.Countdown } - }); + await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data); + game.socket.emit(`system.${CONFIG.DH.id}`, { + action: socketEvent.Refresh, + data: { refreshType: RefreshType.Countdown } + }); Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown }); } diff --git a/module/applications/ui/countdowns.mjs b/module/applications/ui/countdowns.mjs index 76e2b399..2a2a113e 100644 --- a/module/applications/ui/countdowns.mjs +++ b/module/applications/ui/countdowns.mjs @@ -31,9 +31,9 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application minimizable: false }, actions: { - toggleViewMode: DhCountdowns.#toggleViewMode, - editCountdowns: DhCountdowns.#editCountdowns, - loopCountdown: DhCountdowns.#loopCountdown, + toggleViewMode: DhCountdowns.#onToggleViewMode, + editCountdowns: DhCountdowns.#onEditCountdowns, + loopCountdown: DhCountdowns.#onLoopCountdown, decreaseCountdown: (_, target) => this.editCountdown(false, target), increaseCountdown: (_, target) => this.editCountdown(true, target) }, @@ -101,8 +101,8 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application ? countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id ? 'DAGGERHEART.UI.Countdowns.increasingLoop' : countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id - ? 'DAGGERHEART.UI.Countdowns.decreasingLoop' - : 'DAGGERHEART.UI.Countdowns.loop' + ? 'DAGGERHEART.UI.Countdowns.decreasingLoop' + : 'DAGGERHEART.UI.Countdowns.loop' : null; const loopDisabled = !countdownEditable || @@ -147,7 +147,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application return true; } - static async #toggleViewMode() { + static async #onToggleViewMode() { const currentMode = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode); const appMode = CONFIG.DH.GENERAL.countdownAppMode; const newMode = currentMode === appMode.textIcon ? appMode.iconOnly : appMode.textIcon; @@ -158,15 +158,16 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application this.render(); } - static async #editCountdowns() { + static async #onEditCountdowns() { new game.system.api.applications.ui.CountdownEdit().render(true); } - static async #loopCountdown(_, target) { + static async #onLoopCountdown(_, target) { if (!DhCountdowns.canPerformEdit()) return; const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); - const countdown = settings.countdowns[target.id]; + const countdownId = target.closest('[data-countdown]').dataset.countdown; + const countdown = settings.countdowns[countdownId]; let progressMax = countdown.progress.start; let message = null; @@ -180,12 +181,12 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id ? Number(progressMax) + 1 : countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id - ? Math.max(Number(progressMax) - 1, 0) - : progressMax; + ? Math.max(Number(progressMax) - 1, 0) + : progressMax; await waitForDiceSoNice(message); await settings.updateSource({ - [`countdowns.${target.id}.progress`]: { + [`countdowns.${countdownId}.progress`]: { current: newMax, start: newMax } @@ -199,22 +200,23 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application if (!DhCountdowns.canPerformEdit()) return; const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); - const countdown = settings.countdowns[target.id]; + const countdownId = target.closest('[data-countdown]').dataset.countdown; + const countdown = settings.countdowns[countdownId]; const newCurrent = increase ? Math.min(countdown.progress.current + 1, countdown.progress.start) : Math.max(countdown.progress.current - 1, 0); - await settings.updateSource({ [`countdowns.${target.id}.progress.current`]: newCurrent }); + await settings.updateSource({ [`countdowns.${countdownId}.progress.current`]: newCurrent }); await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { refreshType: RefreshType.Countdown }); } static async gmSetSetting(data) { - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data), - game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.Refresh, - data: { refreshType: RefreshType.Countdown } - }); + await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data); + game.socket.emit(`system.${CONFIG.DH.id}`, { + action: socketEvent.Refresh, + data: { refreshType: RefreshType.Countdown } + }); Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown }); } diff --git a/module/applications/ui/effectsDisplay.mjs b/module/applications/ui/effectsDisplay.mjs index 035041e1..a64b1b22 100644 --- a/module/applications/ui/effectsDisplay.mjs +++ b/module/applications/ui/effectsDisplay.mjs @@ -67,10 +67,10 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica const actor = token ? token.actor : canvas.tokens.controlled.length === 0 - ? !game.user.isGM - ? game.user.character - : null - : canvas.tokens.controlled[0].actor; + ? !game.user.isGM + ? game.user.character + : null + : canvas.tokens.controlled[0].actor; return getIconVisibleActiveEffects(actor?.getActiveEffects() ?? []); }; diff --git a/module/canvas/placeables/token.mjs b/module/canvas/placeables/token.mjs index 68e325c2..02eed7db 100644 --- a/module/canvas/placeables/token.mjs +++ b/module/canvas/placeables/token.mjs @@ -155,15 +155,15 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { const targetEdge = this.#getEdgeBoundary(targetBounds, originPoint, targetPoint); const adjustedOriginPoint = originEdge ? canvas.grid.getTopLeftPoint({ - x: originEdge.x + Math.sign(originPoint.x - originEdge.x), - y: originEdge.y + Math.sign(originPoint.y - originEdge.y) - }) + x: originEdge.x + Math.sign(originPoint.x - originEdge.x), + y: originEdge.y + Math.sign(originPoint.y - originEdge.y) + }) : originPoint; const adjustDestinationPoint = targetEdge ? canvas.grid.getTopLeftPoint({ - x: targetEdge.x + Math.sign(targetPoint.x - targetEdge.x), - y: targetEdge.y + Math.sign(targetPoint.y - targetEdge.y) - }) + x: targetEdge.x + Math.sign(targetPoint.x - targetEdge.x), + y: targetEdge.y + Math.sign(targetPoint.y - targetEdge.y) + }) : targetPoint; const distance = canvas.grid.measurePath([ { ...adjustedOriginPoint, elevation: 0 }, diff --git a/module/config/itemBrowserConfig.mjs b/module/config/itemBrowserConfig.mjs index ae5fa71b..83572dc0 100644 --- a/module/config/itemBrowserConfig.mjs +++ b/module/config/itemBrowserConfig.mjs @@ -127,8 +127,8 @@ export const typeConfig = { isSecondary ? 'DAGGERHEART.ITEMS.Weapon.secondaryWeapon.short' : isSecondary === false - ? 'DAGGERHEART.ITEMS.Weapon.primaryWeapon.short' - : '-' + ? 'DAGGERHEART.ITEMS.Weapon.primaryWeapon.short' + : '-' }, { key: 'system.tier', diff --git a/module/data/action/attackAction.mjs b/module/data/action/attackAction.mjs index c4d07c25..1988b1d8 100644 --- a/module/data/action/attackAction.mjs +++ b/module/data/action/attackAction.mjs @@ -75,7 +75,12 @@ export default class DHAttackAction extends DHDamageAction { const useAltDamage = this.actor?.effects?.find(x => x.type === 'horde')?.active; for (const { value, valueAlt, type } of damage.parts) { const usedValue = useAltDamage ? valueAlt : value; - const str = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {}); + const damageString = Roll.replaceFormulaData(usedValue.getFormula(), this.actor?.getRollData() ?? {}); + const str = damageString + ? damageString + : game.i18n.format('DAGGERHEART.GENERAL.missingX', { + x: game.i18n.localize('DAGGERHEART.GENERAL.damage') + }); const icons = Array.from(type) .map(t => CONFIG.DH.GENERAL.damageTypes[t]?.icon) diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index acd104a7..c71f5ef9 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -144,8 +144,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel return this.item instanceof DhpActor ? this.item : this.item?.parent instanceof DhpActor - ? this.item.parent - : null; + ? this.item.parent + : null; } /** @@ -223,7 +223,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel * @returns {object} */ async use(event, configOptions = {}) { - if (!this.actor) throw new Error("An Action can't be used outside of an Actor context."); + if (!this.actor) throw new Error('An Action can\'t be used outside of an Actor context.'); let config = this.prepareConfig(event, configOptions); if (!config) return; @@ -300,17 +300,17 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel const groupAttackTokens = this.damage.groupAttack ? game.system.api.fields.ActionFields.DamageField.getGroupAttackTokens( - this.actor.id, - this.damage.groupAttack - ) + this.actor.id, + this.damage.groupAttack + ) : null; config.damageOptions = { groupAttack: this.damage.groupAttack ? { - numAttackers: Math.max(groupAttackTokens.length, 1), - range: this.damage.groupAttack - } + numAttackers: Math.max(groupAttackTokens.length, 1), + range: this.damage.groupAttack + } : null }; } diff --git a/module/data/action/countdownAction.mjs b/module/data/action/countdownAction.mjs index abcc6b40..cb141637 100644 --- a/module/data/action/countdownAction.mjs +++ b/module/data/action/countdownAction.mjs @@ -36,7 +36,7 @@ export default class DhCountdownAction extends DHBaseAction { /** @inheritDoc */ static migrateData(source) { - for (const countdown of source.countdown) { + for (const countdown of Object.values(source.countdown)) { if (countdown.progress.max) { countdown.progress.startFormula = countdown.progress.max; countdown.progress.start = 1; diff --git a/module/data/activeEffect/beastformEffect.mjs b/module/data/activeEffect/beastformEffect.mjs index 0fbea122..7e037f5b 100644 --- a/module/data/activeEffect/beastformEffect.mjs +++ b/module/data/activeEffect/beastformEffect.mjs @@ -90,13 +90,13 @@ export default class BeastformEffect extends BaseEffect { ...baseUpdate, x, y, - 'texture': { + texture: { enabled: this.characterTokenData.usesDynamicToken, src: token.flags.daggerheart?.beastformTokenImg ?? this.characterTokenData.tokenImg, scaleX: this.characterTokenData.tokenSize.scale, scaleY: this.characterTokenData.tokenSize.scale }, - 'ring': { + ring: { subject: { texture: token.flags.daggerheart?.beastformSubjectTexture ?? this.characterTokenData.tokenRingImg diff --git a/module/data/activeEffect/changeTypes/armor.mjs b/module/data/activeEffect/changeTypes/armor.mjs index 217ff9dd..0c226513 100644 --- a/module/data/activeEffect/changeTypes/armor.mjs +++ b/module/data/activeEffect/changeTypes/armor.mjs @@ -166,10 +166,10 @@ export default class ArmorChange extends foundry.abstract.DataModel { value: change.type === 'armor' ? { - ...change.value, - current: Math.min(change.value.current, newMax), - max: newMax - } + ...change.value, + current: Math.min(change.value.current, newMax), + max: newMax + } : change.value })) ]; diff --git a/module/data/actor/_module.mjs b/module/data/actor/_module.mjs index 99577620..1fe1ef3f 100644 --- a/module/data/actor/_module.mjs +++ b/module/data/actor/_module.mjs @@ -1,15 +1,17 @@ import DhCharacter from './character.mjs'; import DhCompanion from './companion.mjs'; import DhAdversary from './adversary.mjs'; +import DhNPC from './npc.mjs'; import DhEnvironment from './environment.mjs'; import DhParty from './party.mjs'; -export { DhCharacter, DhCompanion, DhAdversary, DhEnvironment, DhParty }; +export { DhCharacter, DhCompanion, DhAdversary, DhNPC, DhEnvironment, DhParty }; export const config = { character: DhCharacter, companion: DhCompanion, adversary: DhAdversary, + npc: DhNPC, environment: DhEnvironment, party: DhParty }; diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs index d69e17ad..d6d0dcdf 100644 --- a/module/data/actor/adversary.mjs +++ b/module/data/actor/adversary.mjs @@ -3,8 +3,7 @@ import { ActionField } from '../fields/actionField.mjs'; import { commonActorRules } from './base.mjs'; import DhCreature from './creature.mjs'; import { bonusField } from '../fields/actorField.mjs'; -import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs'; -import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs'; +import { getTierAdjustedAdversary } from './tierAdjustment.mjs'; export default class DhpAdversary extends DhCreature { static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary']; @@ -206,205 +205,6 @@ export default class DhpAdversary extends DhCreature { /** Returns source data for this actor adjusted to a new tier, which can be used to create a new actor. */ adjustForTier(tier) { const source = this.parent.toObject(true); - - /** @type {(2 | 3 | 4)[]} */ - const tiers = new Array(Math.abs(tier - this.tier)) - .fill(0) - .map((_, idx) => idx + Math.min(tier, this.tier) + 1); - if (tier < this.tier) tiers.reverse(); - const typeData = adversaryScalingData[source.system.type] ?? adversaryScalingData[source.system.standard]; - const tierEntries = tiers.map(t => ({ tier: t, ...typeData[t] })); - - // Apply simple tier changes - const scale = tier > this.tier ? 1 : -1; - for (const entry of tierEntries) { - source.system.difficulty += scale * entry.difficulty; - source.system.damageThresholds.major += scale * entry.majorThreshold; - source.system.damageThresholds.severe += scale * entry.severeThreshold; - source.system.resources.hitPoints.max += scale * entry.hp; - source.system.resources.stress.max += scale * entry.stress; - source.system.attack.roll.bonus += scale * entry.attack; - } - - // Get the mean and standard deviation of expected damage in the previous and new tier - // The data we have is for attack scaling, but we reuse this for action scaling later - const expectedDamageData = adversaryExpectedDamage[source.system.type] ?? adversaryExpectedDamage.basic; - const damageMeta = { - currentDamageRange: { tier: source.system.tier, ...expectedDamageData[source.system.tier] }, - newDamageRange: { tier, ...expectedDamageData[tier] }, - type: 'attack' - }; - - // Update damage of base attack - try { - this.#adjustActionDamage(source.system.attack, damageMeta); - } catch (err) { - ui.notifications.warn('Failed to convert attack damage of adversary'); - console.error(err); - } - - // Update damage of each item action, making sure to also update the description if possible - const damageRegex = /@Damage\[([^\[\]]*)\]({[^}]*})?/g; - for (const item of source.items) { - // Replace damage inlines with new formulas - for (const withDescription of [item.system, ...Object.values(item.system.actions)]) { - withDescription.description = withDescription.description.replace(damageRegex, (match, inner) => { - const { value: formula } = parseInlineParams(inner); - if (!formula || !type) return match; - - try { - const adjusted = this.#calculateAdjustedDamage(formula, { ...damageMeta, type: 'action' }); - const newFormula = [ - adjusted.diceQuantity ? `${adjusted.diceQuantity}d${adjusted.faces}` : null, - adjusted.bonus - ] - .filter(p => !!p) - .join('+'); - return match.replace(formula, newFormula); - } catch { - return match; - } - }); - } - - // Update damage in item actions - // Parse damage, and convert all formula matches in the descriptions to the new damage - for (const action of Object.values(item.system.actions)) { - try { - const result = this.#adjustActionDamage(action, { ...damageMeta, type: 'action' }); - if (!result) continue; - - for (const { previousFormula, formula } of Object.values(result)) { - const oldFormulaRegexp = new RegExp( - previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?') - ); - item.system.description = item.system.description.replace(oldFormulaRegexp, formula); - action.description = action.description.replace(oldFormulaRegexp, formula); - } - } catch (err) { - ui.notifications.warn(`Failed to convert action damage for item ${item.name}`); - console.error(err); - } - } - } - - // Finally set the tier of the source data, now that everything is complete - source.system.tier = tier; - return source; - } - - /** - * Converts a damage object to a new damage range - * @returns {{ diceQuantity: number; faces: number; bonus: number }} the adjusted result as a combined term - * @throws error if the formula is the wrong type - */ - #calculateAdjustedDamage(formula, { currentDamageRange, newDamageRange, type }) { - const terms = parseTermsFromSimpleFormula(formula); - const flatTerms = terms.filter(t => t.diceQuantity === 0); - const diceTerms = terms.filter(t => t.diceQuantity > 0); - if (flatTerms.length > 1 || diceTerms.length > 1) { - throw new Error('invalid formula for conversion'); - } - const value = { - ...(diceTerms[0] ?? { diceQuantity: 0, faces: 1 }), - bonus: flatTerms[0]?.bonus ?? 0 - }; - const previousExpected = calculateExpectedValue(value); - if (previousExpected === 0) return value; // nothing to do - - const dieSizes = [4, 6, 8, 10, 12, 20]; - const steps = newDamageRange.tier - currentDamageRange.tier; - const increasing = steps > 0; - const deviation = (previousExpected - currentDamageRange.mean) / currentDamageRange.deviation; - const expected = Math.max(1, newDamageRange.mean + newDamageRange.deviation * deviation); - - // If this was just a flat number, convert to the expected damage and exit - if (value.diceQuantity === 0) { - value.bonus = Math.round(expected); - return value; - } - - const getExpectedDie = () => calculateExpectedValue({ diceQuantity: 1, faces: value.faces }) || 1; - const getBaseAverage = () => calculateExpectedValue({ ...value, bonus: 0 }); - - // Check the number of base overages over the expected die. In the end, if the bonus inflates too much, we add a die - const baseOverages = Math.floor(value.bonus / getExpectedDie()); - - // Prestep. Change number of dice for attacks, bump up/down for actions - // We never bump up to d20, though we might bump down from it - if (type === 'attack') { - const minimum = increasing ? value.diceQuantity : 0; - value.diceQuantity = Math.max(minimum, newDamageRange.tier); - } else { - const currentIdx = dieSizes.indexOf(value.faces); - value.faces = dieSizes[Math.clamp(currentIdx + steps, 0, 4)]; - } - - value.bonus = Math.round(expected - getBaseAverage()); - - // Attempt to handle negative values. - // If we can do it with only step downs, do so. Otherwise remove tier dice, and try again - if (value.bonus < 0) { - let stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity); - const currentIdx = dieSizes.indexOf(value.faces); - - // If step downs alone don't suffice, change the flat modifier, then calculate steps required again - // If this isn't sufficient, the result will be slightly off. This is unlikely to happen - if (type !== 'attack' && stepsRequired > currentIdx && value.diceQuantity > 0) { - value.diceQuantity -= increasing ? 1 : Math.abs(steps); - value.bonus = Math.round(expected - getBaseAverage()); - if (value.bonus >= 0) return value; // complete - } - - stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity); - value.faces = dieSizes[Math.max(0, currentIdx - stepsRequired)]; - value.bonus = Math.max(0, Math.round(expected - getBaseAverage())); - } - - // If value is really high, we add a number of dice based on the number of overages - // This attempts to preserve a similar amount of variance when increasing an action - const overagesToRemove = Math.floor(value.bonus / getExpectedDie()) - baseOverages; - if (type !== 'attack' && increasing && overagesToRemove > 0) { - value.diceQuantity += overagesToRemove; - value.bonus = Math.round(expected - getBaseAverage()); - } - - return value; - } - - /** - * Updates damage to reflect a specific value. - * @throws if damage structure is invalid for conversion - * @returns the converted formula and value as a simplified term, or null if it doesn't deal HP damage - */ - #adjustActionDamage(action, damageMeta) { - if (!action.damage?.parts.hitPoints) return null; - - const result = {}; - for (const property of ['value', 'valueAlt']) { - const data = action.damage.parts.hitPoints[property]; - const previousFormula = data.custom.enabled - ? data.custom.formula - : [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0] - .filter(p => !!p) - .join('+'); - const value = this.#calculateAdjustedDamage(previousFormula, damageMeta); - const formula = [value.diceQuantity ? `${value.diceQuantity}d${value.faces}` : null, value.bonus] - .filter(p => !!p) - .join('+'); - if (value.diceQuantity) { - data.custom.enabled = false; - data.bonus = value.bonus; - data.dice = `d${value.faces}`; - data.flatMultiplier = value.diceQuantity; - } else if (!value.diceQuantity) { - data.custom.enabled = true; - data.custom.formula = formula; - } - - result[property] = { previousFormula, formula, value }; - } - - return result; + return getTierAdjustedAdversary(source, tier); } } diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 10d53c13..aed27650 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -315,8 +315,8 @@ export default class DhCharacter extends DhCreature { return currentLevel === 1 ? 1 : Object.values(game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers).find( - tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end - ).tier; + tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end + ).tier; } get ancestry() { @@ -520,20 +520,20 @@ export default class DhCharacter extends DhCreature { if (armorSource.type === 'armor') { armorUpdates[armorSource.parent.id].updates.push({ - '_id': armorSource.id, + _id: armorSource.id, 'system.armor.current': armorSource.system.armor.current + usedArmorChange }); } else { effectUpdates[armorSource.parent.id].updates.push({ - '_id': armorSource.id, + _id: armorSource.id, 'system.changes': armorSource.system.changes.map(change => ({ ...change, value: change.type === 'armor' ? { - ...change.value, - current: armorSource.system.armorChange.value.current + usedArmorChange - } + ...change.value, + current: armorSource.system.armorChange.value.current + usedArmorChange + } : change.value })) }); @@ -621,21 +621,21 @@ export default class DhCharacter extends DhCreature { }, ...(multiclassFeatures.length ? { - multiclassFeatures: { - title: `${game.i18n.localize('DAGGERHEART.GENERAL.multiclass')} - ${this.multiclass.value?.name}`, - type: 'multiclass', - values: multiclassFeatures - } - } + multiclassFeatures: { + title: `${game.i18n.localize('DAGGERHEART.GENERAL.multiclass')} - ${this.multiclass.value?.name}`, + type: 'multiclass', + values: multiclassFeatures + } + } : {}), ...(multiclassSubclassFeatures.length ? { - multiclassSubclassFeatures: { - title: `${game.i18n.localize('DAGGERHEART.GENERAL.multiclass')} ${game.i18n.localize('TYPES.Item.subclass')} - ${this.multiclass.subclass?.name}`, - type: 'multiclassSubclass', - values: multiclassSubclassFeatures - } - } + multiclassSubclassFeatures: { + title: `${game.i18n.localize('DAGGERHEART.GENERAL.multiclass')} ${game.i18n.localize('TYPES.Item.subclass')} - ${this.multiclass.subclass?.name}`, + type: 'multiclassSubclass', + values: multiclassSubclassFeatures + } + } : {}), companionFeatures: { title: game.i18n.localize('DAGGERHEART.ACTORS.Character.companionFeatures'), @@ -659,8 +659,8 @@ export default class DhCharacter extends DhCreature { (this.primaryWeapon && this.secondaryWeapon) ? burden.twoHanded.value : this.primaryWeapon || this.secondaryWeapon - ? burden.oneHanded.value - : null; + ? burden.oneHanded.value + : null; } get deathMoveViable() { @@ -726,8 +726,8 @@ export default class DhCharacter extends DhCreature { currentLevel === 1 ? null : Object.values(game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers).find( - tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end - ).tier; + tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end + ).tier; if (game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto) { for (let levelKey in this.levelData.levelups) { const level = this.levelData.levelups[levelKey]; diff --git a/module/data/actor/npc.mjs b/module/data/actor/npc.mjs new file mode 100644 index 00000000..2ccaf926 --- /dev/null +++ b/module/data/actor/npc.mjs @@ -0,0 +1,43 @@ +import DHNPCSettings from '../../applications/sheets-configs/npc-settings.mjs'; +import BaseDataActor from './base.mjs'; + +export default class DhpNPC extends BaseDataActor { + static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.NPC']; + + static get metadata() { + return foundry.utils.mergeObject(super.metadata, { + label: 'TYPES.Actor.npc', + type: 'npc', + settingSheet: DHNPCSettings, + hasResistances: false, + hasAttribution: true + }); + } + + static defineSchema() { + const fields = foundry.data.fields; + return { + ...super.defineSchema(), + difficulty: new fields.NumberField({ + nullable: true, + initial: null, + integer: true, + label: 'DAGGERHEART.GENERAL.difficulty' + }), + description: new fields.HTMLField({ label: 'DAGGERHEART.GENERAL.description' }), + motives: new fields.StringField(), + notes: new fields.HTMLField() + }; + } + + /**@inheritdoc */ + static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/actors/drama-masks.svg'; + + get features() { + return this.parent.items.filter(x => x.type === 'feature'); + } + + isItemValid(source) { + return super.isItemValid(source) || source.type === 'feature'; + } +} diff --git a/module/data/actor/tierAdjustment.mjs b/module/data/actor/tierAdjustment.mjs new file mode 100644 index 00000000..bc6ad176 --- /dev/null +++ b/module/data/actor/tierAdjustment.mjs @@ -0,0 +1,219 @@ +import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs'; +import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs'; +import { parseInlineParams } from '../../enrichers/parser.mjs'; + +export function getTierAdjustedAdversary(source, tier) { + const currentTier = source.tier ?? 1; + + /** @type {(2 | 3 | 4)[]} */ + const tiers = new Array(Math.abs(tier - currentTier)) + .fill(0) + .map((_, idx) => idx + Math.min(tier, currentTier) + 1); + if (tier < currentTier) tiers.reverse(); + const typeData = adversaryScalingData[source.system.type] ?? adversaryScalingData[source.system.standard]; + const tierEntries = tiers.map(t => ({ tier: t, ...typeData[t] })); + + // Apply simple tier changes + const scale = tier > currentTier ? 1 : -1; + for (const entry of tierEntries) { + source.system.difficulty += scale * entry.difficulty; + source.system.damageThresholds.major += scale * entry.majorThreshold; + source.system.damageThresholds.severe += scale * entry.severeThreshold; + source.system.resources.hitPoints.max += scale * entry.hp; + source.system.resources.stress.max += scale * entry.stress; + source.system.attack.roll.bonus += scale * entry.attack; + } + + // Get the mean and standard deviation of expected damage in the previous and new tier + // The data we have is for attack scaling, but we reuse this for action scaling later + const expectedDamageData = adversaryExpectedDamage[source.system.type] ?? adversaryExpectedDamage.basic; + const damageMeta = { + currentDamageRange: { tier: source.system.tier, ...expectedDamageData[source.system.tier] }, + newDamageRange: { tier, ...expectedDamageData[tier] } + }; + + // Store initial attack damage for abilities that have you deal a "standard attack" + const initialAttack = { + type: source.system.attack.damage?.parts.hitPoints?.type?.toSorted(), + value: getDamagePartsFormula(source.system.attack.damage?.parts.hitPoints?.value) + }; + + // Update damage of base attack. + try { + const damage = source.system.attack.damage; + if (!damage?.parts.hitPoints) throw new Error('Unexpected missing attack in adversary'); + + for (const property of ['value', 'valueAlt']) { + const data = damage.parts.hitPoints[property]; + const previousFormula = getDamagePartsFormula(data); + const { value, formula } = calculateAdjustedDamage(previousFormula, 'attack', damageMeta); + applyAdjustedDamage(data, value, formula); + } + } catch (err) { + ui.notifications.warn('Failed to convert attack damage of adversary'); + console.error(err); + } + + // Update damage of each item action, making sure to also update the description if possible + const damageRegex = /@Damage\[([^\[\]]*)\]({[^}]*})?/g; + for (const item of source.items) { + // Replace damage inlines with new formulas. Keep a record for a specific check later + const descriptionFormulas = []; + for (const withDescription of [item.system, ...Object.values(item.system.actions)]) { + withDescription.description = withDescription.description.replace(damageRegex, (match, inner) => { + const { value: formula } = parseInlineParams(inner, { first: 'value' }); + if (!formula) return match; + + try { + const newFormula = calculateAdjustedDamage(formula, 'action', damageMeta)?.formula; + descriptionFormulas.push(formula); + return match.replace(formula, newFormula); + } catch { + return match; + } + }); + } + + // Update damage in item actions and convert all formula matches in the descriptions to the new damage + for (const action of Object.values(item.system.actions)) { + if (!action.damage?.parts.hitPoints) continue; + try { + // Apply conversions and save a record. If it matches attack damage *and* Its not in the description, use attack conversion instead + const result = []; + for (const property of ['value', 'valueAlt']) { + const { [property]: data, type: damageType } = action.damage.parts.hitPoints; + const previousFormula = getDamagePartsFormula(data); + const isActuallyAttack = + previousFormula === initialAttack.value && + foundry.utils.equals(damageType.toSorted(), initialAttack.type) && + !descriptionFormulas.includes(previousFormula); + const type = isActuallyAttack ? 'attack' : 'action'; + const { value, formula } = calculateAdjustedDamage(previousFormula, type, damageMeta); + applyAdjustedDamage(data, value, formula); + result.push({ previousFormula, formula }); + } + + // Override text in the description with those values + for (const { previousFormula, formula } of Object.values(result)) { + const oldFormulaRegexp = new RegExp( + previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?') + ); + item.system.description = item.system.description.replace(oldFormulaRegexp, formula); + action.description = action.description.replace(oldFormulaRegexp, formula); + } + } catch (err) { + ui.notifications.warn(`Failed to convert action damage for item ${item.name}`); + console.error(err); + } + } + } + + // Finally set the tier of the source data, now that everything is complete + source.system.tier = tier; + return source; +} + +/** + * Converts a damage object to a new damage range + * @returns {{ diceQuantity: number; faces: number; bonus: number }} the adjusted result as a combined term + * @throws error if the formula is the wrong type + */ +function calculateAdjustedDamage(formula, type, { currentDamageRange, newDamageRange }) { + const terms = parseTermsFromSimpleFormula(formula); + const flatTerms = terms.filter(t => t.diceQuantity === 0); + const diceTerms = terms.filter(t => t.diceQuantity > 0); + if (flatTerms.length > 1 || diceTerms.length > 1) { + throw new Error('invalid formula for conversion'); + } + const value = { + ...(diceTerms[0] ?? { diceQuantity: 0, faces: 1 }), + bonus: flatTerms[0]?.bonus ?? 0 + }; + const previousExpected = calculateExpectedValue(value); + if (previousExpected === 0) return value; // nothing to do + + const dieSizes = [4, 6, 8, 10, 12, 20]; + const steps = newDamageRange.tier - currentDamageRange.tier; + const increasing = steps > 0; + const deviation = (previousExpected - currentDamageRange.mean) / currentDamageRange.deviation; + const expected = Math.max(1, newDamageRange.mean + newDamageRange.deviation * deviation); + + // If this was just a flat number, convert to the expected damage and exit + if (value.diceQuantity === 0) { + value.bonus = Math.round(expected); + return value; + } + + const getExpectedDie = () => calculateExpectedValue({ diceQuantity: 1, faces: value.faces }) || 1; + const getBaseAverage = () => calculateExpectedValue({ ...value, bonus: 0 }); + + // Check the number of base overages over the expected die. In the end, if the bonus inflates too much, we add a die + const baseOverages = Math.floor(value.bonus / getExpectedDie()); + + // Prestep. Change number of dice for attacks, bump up/down for actions + // We never bump up to d20, though we might bump down from it + if (type === 'attack') { + const minimum = increasing ? value.diceQuantity : 0; + value.diceQuantity = Math.max(minimum, newDamageRange.tier); + } else { + const currentIdx = dieSizes.indexOf(value.faces); + value.faces = dieSizes[Math.clamp(currentIdx + steps, 0, 4)]; + } + + value.bonus = Math.round(expected - getBaseAverage()); + + // Attempt to handle negative values. + // If we can do it with only step downs, do so. Otherwise remove tier dice, and try again + if (value.bonus < 0) { + let stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity); + const currentIdx = dieSizes.indexOf(value.faces); + + // If step downs alone don't suffice, change the flat modifier, then calculate steps required again + // If this isn't sufficient, the result will be slightly off. This is unlikely to happen + if (type !== 'attack' && stepsRequired > currentIdx && value.diceQuantity > 0) { + value.diceQuantity -= increasing ? 1 : Math.abs(steps); + value.bonus = Math.round(expected - getBaseAverage()); + if (value.bonus >= 0) return value; // complete + } + + stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity); + value.faces = dieSizes[Math.max(0, currentIdx - stepsRequired)]; + value.bonus = Math.max(0, Math.round(expected - getBaseAverage())); + } + + // If value is really high, we add a number of dice based on the number of overages + // This attempts to preserve a similar amount of variance when increasing an action + const overagesToRemove = Math.floor(value.bonus / getExpectedDie()) - baseOverages; + if (type !== 'attack' && increasing && overagesToRemove > 0) { + value.diceQuantity += overagesToRemove; + value.bonus = Math.round(expected - getBaseAverage()); + } + + const newFormula = [value.diceQuantity ? `${value.diceQuantity}d${value.faces}` : null, value.bonus] + .filter(p => !!p) + .join('+'); + return { value, formula: newFormula }; +} + +function getDamagePartsFormula(data) { + return data.custom.enabled + ? data.custom.formula + : [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0].filter(p => !!p).join('+'); +} + +/** + * Updates damage to reflect a specific value. + * @throws if damage structure is invalid for conversion + * @returns the converted formula and value as a simplified term, or null if it doesn't deal HP damage + */ +function applyAdjustedDamage(diceData, value, formula) { + if (value.diceQuantity) { + diceData.custom.enabled = false; + diceData.bonus = value.bonus; + diceData.dice = `d${value.faces}`; + diceData.flatMultiplier = value.diceQuantity; + } else if (!value.diceQuantity) { + diceData.custom.enabled = true; + diceData.custom.formula = formula; + } +} diff --git a/module/data/chat-message/actorRoll.mjs b/module/data/chat-message/actorRoll.mjs index eaa1cdc2..ccfe25ea 100644 --- a/module/data/chat-message/actorRoll.mjs +++ b/module/data/chat-message/actorRoll.mjs @@ -1,3 +1,5 @@ +import { triggerChatRollFx } from '../../helpers/utils.mjs'; + const fields = foundry.data.fields; const targetsField = () => @@ -130,6 +132,35 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { }); } + /* TODO: Change how damage data is stored somehow to enable better rerolling */ + async getRerolledDamage() { + if (!this.damage) return; + + const rerolls = []; + const update = { system: { damage: {} } }; + for (const partKey in this.damage) { + const part = this.damage[partKey]; + const testRoll = Roll.fromData(part.parts[0].roll); + const rerolled = await testRoll.reroll(); + rerolls.push(rerolled); + + if (!update.system.damage[partKey]) update.system.damage[partKey] = { parts: [part.parts[0]] }; + const partData = update.system.damage[partKey].parts[0]; + update.system.damage[partKey].total = rerolled.total; + partData.modifierTotal = rerolled.terms.reduce((acc, x) => { + if (x.isDeterministic && !x.operator) acc += x.total; + return acc; + }, 0); + partData.dice = rerolled.dice.map(d => ({ ...d.toJSON(), dice: d.denomination })); + partData.total = rerolled.total; + partData.roll = rerolled.toJSON(); + } + + await triggerChatRollFx(rerolls); + + return update; + } + registerTargetHook() { if (!this.parent.isAuthor || !this.hasTarget) return; if (this.targetMode && this.parent.targetHook !== null) { diff --git a/module/data/companionLevelup.mjs b/module/data/companionLevelup.mjs index 7ab61210..e24820de 100644 --- a/module/data/companionLevelup.mjs +++ b/module/data/companionLevelup.mjs @@ -22,12 +22,12 @@ export class DhCompanionLevelup extends foundry.abstract.DataModel { const initialAchievements = i === tier.levels.start ? tier.initialAchievements : {}; const experiences = initialAchievements.experience ? [...Array(initialAchievements.experience.nr).keys()].reduce((acc, _) => { - acc[foundry.utils.randomID()] = { - name: '', - modifier: initialAchievements.experience.modifier - }; - return acc; - }, {}) + acc[foundry.utils.randomID()] = { + name: '', + modifier: initialAchievements.experience.modifier + }; + return acc; + }, {}) : {}; const currentChoices = pcLevelData.levelups[i]?.selections?.length; @@ -302,9 +302,9 @@ export class DhLevelupLevel extends foundry.abstract.DataModel { experiences: levelData.achievements?.experiences ?? achievements.experiences ?? {}, domainCards: levelData.achievements?.domainCards ? levelData.achievements.domainCards.reduce((acc, card, index) => { - acc[index] = { ...card }; - return acc; - }, {}) + acc[index] = { ...card }; + return acc; + }, {}) : (achievements.domainCards ?? {}), proficiency: levelData.achievements?.proficiency ?? achievements.proficiency ?? null }, diff --git a/module/data/countdowns.mjs b/module/data/countdowns.mjs index 7d27197d..54971d34 100644 --- a/module/data/countdowns.mjs +++ b/module/data/countdowns.mjs @@ -77,11 +77,11 @@ export class DhCountdown extends foundry.abstract.DataModel { static defaultCountdown(type, playerHidden) { const ownership = playerHidden ? game.users.reduce((acc, user) => { - if (!user.isGM) { - acc[user.id] = CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE; - } - return acc; - }, {}) + if (!user.isGM) { + acc[user.id] = CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE; + } + return acc; + }, {}) : undefined; return { @@ -102,8 +102,8 @@ export class DhCountdown extends foundry.abstract.DataModel { value: user.isGM ? CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER : this.ownership.players[user.id] && this.ownership.players[user.id].type !== -1 - ? this.ownership.players[user.id].type - : this.ownership.default, + ? this.ownership.players[user.id].type + : this.ownership.default, isGM: user.isGM }; diff --git a/module/data/fields/action/beastformField.mjs b/module/data/fields/action/beastformField.mjs index e3be9937..0eeb95c2 100644 --- a/module/data/fields/action/beastformField.mjs +++ b/module/data/fields/action/beastformField.mjs @@ -105,8 +105,8 @@ export default class BeastformField extends fields.SchemaField { baseSize === 'custom' ? 'custom' : (Object.keys(CONFIG.DH.ACTOR.tokenSize).find( - x => CONFIG.DH.ACTOR.tokenSize[x].value === CONFIG.DH.ACTOR.tokenSize[baseSize].value + 1 - ) ?? baseSize); + x => CONFIG.DH.ACTOR.tokenSize[x].value === CONFIG.DH.ACTOR.tokenSize[baseSize].value + 1 + ) ?? baseSize); formData.system.tokenSize = { ...evolvedData.form.system.tokenSize, size: evolvedSize diff --git a/module/data/fields/action/costField.mjs b/module/data/fields/action/costField.mjs index 1928af41..82cfcd23 100644 --- a/module/data/fields/action/costField.mjs +++ b/module/data/fields/action/costField.mjs @@ -116,8 +116,8 @@ export default class CostField extends fields.ArrayField { c.key === 'fear' ? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) : resources[c.key].isReversed - ? resources[c.key].max - resources[c.key].value - : resources[c.key].value; + ? resources[c.key].max - resources[c.key].value + : resources[c.key].value; if (c.scalable) c.maxStep = Math.floor((c.max - c.value) / c.step); return c; }); @@ -149,8 +149,8 @@ export default class CostField extends fields.ArrayField { !resources[c.key] ? a : a && resources[c.key].isReversed - ? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max - : resources[c.key]?.value >= (c.total ?? c.value), + ? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max + : resources[c.key]?.value >= (c.total ?? c.value), true ); } diff --git a/module/data/fields/action/countdownField.mjs b/module/data/fields/action/countdownField.mjs index 990f8ef1..96d9dd91 100644 --- a/module/data/fields/action/countdownField.mjs +++ b/module/data/fields/action/countdownField.mjs @@ -87,11 +87,11 @@ export default class CountdownField extends fields.ArrayField { CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSetting.toObject() - ), - game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.Refresh, - data: { refreshType: RefreshType.Countdown } - }); + ); + game.socket.emit(`system.${CONFIG.DH.id}`, { + action: socketEvent.Refresh, + data: { refreshType: RefreshType.Countdown } + }); Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown }); }, data, diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index 30a5ad7c..9b21d3ba 100644 --- a/module/data/fields/action/damageField.mjs +++ b/module/data/fields/action/damageField.mjs @@ -72,9 +72,6 @@ export default class DamageField extends fields.SchemaField { damageConfig.source.message = messageId; damageConfig.directDamage = !!damageConfig.source?.message; - // if(damageConfig.source?.message && game.modules.get('dice-so-nice')?.active) - // await game.dice3d.waitFor3DAnimationByMessageID(damageConfig.source.message); - const damageResult = await CONFIG.Dice.daggerheart.DamageRoll.build(damageConfig); if (!damageResult) return false; if (damageResult.actionChatMessageHandled) config.actionChatMessageHandled = true; diff --git a/module/data/fields/action/effectsField.mjs b/module/data/fields/action/effectsField.mjs index 1053e51d..e943d63d 100644 --- a/module/data/fields/action/effectsField.mjs +++ b/module/data/fields/action/effectsField.mjs @@ -1,5 +1,3 @@ -import { emitGMUpdate, GMUpdateEvent } from '../../../systemRegistration/socket.mjs'; - const fields = foundry.data.fields; export default class EffectsField extends fields.ArrayField { @@ -34,8 +32,7 @@ export default class EffectsField extends fields.ArrayField { } if (EffectsField.getAutomation() || force) { targets ??= (message.system?.targets ?? config.targets).filter(t => !config.hasRoll || t.hit); - await emitGMUpdate(GMUpdateEvent.UpdateEffect, EffectsField.applyEffects.bind(this), targets, this.uuid); - // EffectsField.applyEffects.call(this, config.targets.filter(t => !config.hasRoll || t.hit)); + EffectsField.applyEffects.call(this, targets); } } @@ -59,16 +56,16 @@ export default class EffectsField extends fields.ArrayField { if (!token) return; const messageToken = token.document ?? token; - const conditionImmunities = messageToken.actor.system.rules.conditionImmunities ?? {}; + const conditionImmunities = messageToken.actor.system.rules?.conditionImmunities ?? {}; messageTargets.push({ token: messageToken, conditionImmunities: Object.values(conditionImmunities).some(x => x) ? game.i18n.format('DAGGERHEART.UI.Chat.effectSummary.immunityTo', { - immunities: Object.keys(conditionImmunities) - .filter(x => conditionImmunities[x]) - .map(x => game.i18n.localize(conditions[x].name)) - .join(', ') - }) + immunities: Object.keys(conditionImmunities) + .filter(x => conditionImmunities[x]) + .map(x => game.i18n.localize(conditions[x].name)) + .join(', ') + }) : null }); diff --git a/module/data/fields/action/saveField.mjs b/module/data/fields/action/saveField.mjs index 0629353e..7343ab85 100644 --- a/module/data/fields/action/saveField.mjs +++ b/module/data/fields/action/saveField.mjs @@ -69,11 +69,11 @@ export default class SaveField extends fields.SchemaField { game.user === actor.owner ? SaveField.rollSave.call(this, actor, event) : actor.owner.query('reactionRoll', { - actionId: this.uuid, - actorId: actor.uuid, - event, - message - }); + actionId: this.uuid, + actorId: actor.uuid, + event, + message + }); const result = await rollSave; await SaveField.updateSaveMessage.call(this, result, message, target.id); subResolve(); @@ -97,8 +97,8 @@ export default class SaveField extends fields.SchemaField { const title = actor.isNPC ? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll') : game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { - ability: game.i18n.localize(abilities[this.save.trait]?.label) - }), + ability: game.i18n.localize(abilities[this.save.trait]?.label) + }), rollConfig = { event, title, diff --git a/module/data/fields/action/summonField.mjs b/module/data/fields/action/summonField.mjs index ec7881f7..a2275fa5 100644 --- a/module/data/fields/action/summonField.mjs +++ b/module/data/fields/action/summonField.mjs @@ -1,4 +1,4 @@ -import { itemAbleRollParse } from '../../../helpers/utils.mjs'; +import { itemAbleRollParse, triggerChatRollFx } from '../../../helpers/utils.mjs'; import FormulaField from '../formulaField.mjs'; const fields = foundry.data.fields; @@ -40,7 +40,7 @@ export default class DHSummonField extends fields.ArrayField { const roll = new Roll(itemAbleRollParse(summon.count, this.actor, this.item)); await roll.evaluate(); const count = roll.total; - if (!roll.isDeterministic && game.modules.get('dice-so-nice')?.active) rolls.push(roll); + if (!roll.isDeterministic) rolls.push(roll); const actor = await DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID)); /* Extending summon data in memory so it's available in actionField.toChat. Think it's harmless, but ugly. Could maybe find a better way. */ @@ -56,7 +56,7 @@ export default class DHSummonField extends fields.ArrayField { } } - if (rolls.length) await Promise.all(rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true))); + if (rolls.length) await triggerChatRollFx(rolls); this.actor.sheet?.minimize(); DHSummonField.handleSummon(summonData, this.actor); diff --git a/module/data/item/ancestry.mjs b/module/data/item/ancestry.mjs index b9253a3c..eae1136c 100644 --- a/module/data/item/ancestry.mjs +++ b/module/data/item/ancestry.mjs @@ -1,6 +1,6 @@ import BaseDataItem from './base.mjs'; import ItemLinkFields from '../../data/fields/itemLinkFields.mjs'; -import { getFeaturesHTMLData } from '../../helpers/utils.mjs'; +import { fromUuids, getFeaturesHTMLData } from '../../helpers/utils.mjs'; export default class DHAncestry extends BaseDataItem { /** @inheritDoc */ @@ -45,6 +45,10 @@ export default class DHAncestry extends BaseDataItem { /**@inheritdoc */ async getDescriptionData() { + // Preload all ancestry features for acquisition from the cache + // todo: make feature acquisition async and replace feature helpers for methods + await fromUuids(this._source.features.map(f => f.item)); + const baseDescription = this.description; const features = await getFeaturesHTMLData(this.features); diff --git a/module/data/item/beastform.mjs b/module/data/item/beastform.mjs index ee9d9839..ba274cc7 100644 --- a/module/data/item/beastform.mjs +++ b/module/data/item/beastform.mjs @@ -208,8 +208,8 @@ export default class DHBeastform extends BaseDataItem { const autoTokenSize = this.tokenSize.size !== 'custom' ? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes[ - this.tokenSize.size - ] + this.tokenSize.size + ] : null; const width = autoTokenSize ?? this.tokenSize.width; const height = autoTokenSize ?? this.tokenSize.height; diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs index 7014e011..470a1e3c 100644 --- a/module/data/item/class.mjs +++ b/module/data/item/class.mjs @@ -2,7 +2,7 @@ import BaseDataItem from './base.mjs'; import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; import ItemLinkFields from '../fields/itemLinkFields.mjs'; -import { addLinkedItemsDiff, getFeaturesHTMLData, updateLinkedItemApps } from '../../helpers/utils.mjs'; +import { addLinkedItemsDiff, fromUuids, getFeaturesHTMLData, updateLinkedItemApps } from '../../helpers/utils.mjs'; export default class DHClass extends BaseDataItem { /** @inheritDoc */ @@ -73,15 +73,16 @@ export default class DHClass extends BaseDataItem { const uuids = [this.parent.uuid, this.parent._stats?.compendiumSource].filter(u => !!u); const subclasses = game.items.filter(x => x.type === 'subclass' && uuids.includes(x.system.linkedClass)); for (const pack of game.packs) { + const packIds = []; const indexes = await pack.getIndex({ fields: ['system.linkedClass'] }); for (const index of indexes) { if (index.type !== 'subclass') continue; if (!uuids.includes(index.system?.linkedClass)) continue; if (subclasses.find(x => x.uuid === index.uuid)) continue; - - const subclass = await foundry.utils.fromUuid(index.uuid); - subclasses.push(subclass); + packIds.push(index._id); } + + if (packIds.length > 0) subclasses.push(...(await pack.getDocuments({ _id__in: packIds }))); } return subclasses; @@ -216,6 +217,10 @@ export default class DHClass extends BaseDataItem { classItems.push(contentLink.outerHTML); } + // Preload all class features for acquisition from the cache + // todo: make feature acquisition async and replace feature helpers for methods + await fromUuids(this._source.features.map(f => f.item)); + const hopeFeatures = await getFeaturesHTMLData(this.hopeFeatures); const classFeatures = await getFeaturesHTMLData(this.classFeatures); diff --git a/module/data/item/community.mjs b/module/data/item/community.mjs index 6d054976..6f4470b8 100644 --- a/module/data/item/community.mjs +++ b/module/data/item/community.mjs @@ -1,4 +1,4 @@ -import { getFeaturesHTMLData } from '../../helpers/utils.mjs'; +import { fromUuids, getFeaturesHTMLData } from '../../helpers/utils.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; import BaseDataItem from './base.mjs'; @@ -27,6 +27,10 @@ export default class DHCommunity extends BaseDataItem { /**@inheritdoc */ async getDescriptionData() { + // Preload all community features for acquisition from the cache + // todo: make feature acquisition async and replace feature helpers for methods + await fromUuids(this._source.features); + const baseDescription = this.description; const features = await getFeaturesHTMLData(this.features); diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs index ecf72de3..934b55d3 100644 --- a/module/data/item/subclass.mjs +++ b/module/data/item/subclass.mjs @@ -1,5 +1,4 @@ -import { getFeaturesHTMLData } from '../../helpers/utils.mjs'; -import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; +import { fromUuids, getFeaturesHTMLData } from '../../helpers/utils.mjs'; import ItemLinkFields from '../fields/itemLinkFields.mjs'; import BaseDataItem from './base.mjs'; @@ -91,6 +90,11 @@ export default class DHSubclass extends BaseDataItem { const spellcastTrait = this.spellcastingTrait ? game.i18n.localize(CONFIG.DH.ACTOR.abilities[this.spellcastingTrait].label) : null; + + // Preload all subclass features for acquisition from the cache + // todo: make feature acquisition async and replace feature helpers for methods + await fromUuids(this._source.features.map(f => f.item)); + const foundationFeatures = await getFeaturesHTMLData(this.foundationFeatures); const specializationFeatures = await getFeaturesHTMLData(this.specializationFeatures); const masteryFeatures = await getFeaturesHTMLData(this.masteryFeatures); diff --git a/module/data/levelup.mjs b/module/data/levelup.mjs index 4dc1c058..e30bf52d 100644 --- a/module/data/levelup.mjs +++ b/module/data/levelup.mjs @@ -19,12 +19,12 @@ export class DhLevelup extends foundry.abstract.DataModel { const initialAchievements = i === tier.levels.start ? tier.initialAchievements : {}; const experiences = initialAchievements.experience ? [...Array(initialAchievements.experience.nr).keys()].reduce((acc, _) => { - acc[foundry.utils.randomID()] = { - name: '', - modifier: initialAchievements.experience.modifier - }; - return acc; - }, {}) + acc[foundry.utils.randomID()] = { + name: '', + modifier: initialAchievements.experience.modifier + }; + return acc; + }, {}) : {}; const domainCards = [...Array(tier.domainCardByLevel).keys()].reduce((acc, _) => { @@ -298,9 +298,9 @@ export class DhLevelupLevel extends foundry.abstract.DataModel { experiences: levelData.achievements?.experiences ?? achievements.experiences ?? {}, domainCards: levelData.achievements?.domainCards ? levelData.achievements.domainCards.reduce((acc, card, index) => { - acc[index] = { ...card }; - return acc; - }, {}) + acc[index] = { ...card }; + return acc; + }, {}) : (achievements.domainCards ?? {}), proficiency: levelData.achievements?.proficiency ?? achievements.proficiency ?? null }, diff --git a/module/dice/d20Roll.mjs b/module/dice/d20Roll.mjs index 509f5d69..b1d3bd0b 100644 --- a/module/dice/d20Roll.mjs +++ b/module/dice/d20Roll.mjs @@ -1,4 +1,5 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; +import { triggerChatRollFx } from '../helpers/utils.mjs'; import DHRoll from './dhRoll.mjs'; export default class D20Roll extends DHRoll { @@ -224,4 +225,15 @@ export default class D20Roll extends DHRoll { resetFormula() { return (this._formula = this.constructor.getFormula(this.terms)); } + + async reroll(options) { + const result = await super.reroll(options); + if (this instanceof game.system.api.dice.DualityRoll) return result; + + if (options?.liveRoll) { + await triggerChatRollFx([result]); + } + + return result; + } } diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index 98fd8401..ef810ed7 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -1,5 +1,5 @@ import DamageDialog from '../applications/dialogs/damageDialog.mjs'; -import { parseRallyDice } from '../helpers/utils.mjs'; +import { parseRallyDice, triggerChatRollFx } from '../helpers/utils.mjs'; import DHRoll from './dhRoll.mjs'; export default class DamageRoll extends DHRoll { @@ -18,7 +18,12 @@ export default class DamageRoll extends DHRoll { if (config.evaluate !== false) for (const roll of config.roll) await roll.roll.evaluate(); roll._evaluated = true; - const parts = config.roll.map(r => this.postEvaluate(r)); + + const parts = []; + for (const roll of config.roll) { + parts.push(this.postEvaluate(roll)); + roll.roll = JSON.stringify(roll.roll.toJSON()); + } config.damage = this.unifyDamageRoll(parts); } @@ -38,25 +43,24 @@ export default class DamageRoll extends DHRoll { const chatMessage = config.source?.message ? ui.chat.collection.get(config.source.message) : getDocumentClass('ChatMessage').applyMode({}, config.rollMode ?? 'public'); + + const diceRolls = []; if (game.modules.get('dice-so-nice')?.active) { - const pool = foundry.dice.terms.PoolTerm.fromRolls( - Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll)) - ), - diceRoll = Roll.fromTerms([pool]); - await game.dice3d.showForRoll( - diceRoll, - game.user, - true, - chatMessage.whisper?.length > 0 ? chatMessage.whisper : null, - chatMessage.blind - ); config.mute = true; + const pool = foundry.dice.terms.PoolTerm.fromRolls( + Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll)) + ); + diceRolls.push(Roll.fromTerms([pool])); } + + await triggerChatRollFx(diceRolls, { + whisper: chatMessage.whisper?.length > 0 ? chatMessage.whisper : null, + blind: chatMessage.blind + }); await super.buildPost(roll, config, message); + if (config.source?.message) { chatMessage.update({ 'system.damage': config.damage }); - - if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); } } @@ -319,9 +323,10 @@ export default class DamageRoll extends DHRoll { const newIndex = parsedDiceTerms[dice].results.length; await term.reroll(`/r1=${termResult.result}`); + const diceRolls = []; if (game.modules.get('dice-so-nice')?.active) { const newResult = parsedDiceTerms[dice].results[newIndex]; - const diceSoNiceRoll = { + diceRolls.push({ _evaluated: true, dice: [ new foundry.dice.terms.Die({ @@ -332,11 +337,10 @@ export default class DamageRoll extends DHRoll { }) ], options: { appearance: {} } - }; - - await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true); + }); } + await triggerChatRollFx(diceRolls); await parsedRoll.evaluate(); const results = parsedRoll.dice[dice].results.map(result => ({ diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index d6975f71..c28db98f 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -1,4 +1,5 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; +import { triggerChatRollFx } from '../helpers/utils.mjs'; export default class DHRoll extends Roll { baseTerms = []; @@ -36,6 +37,7 @@ export default class DHRoll extends Roll { static async buildConfigure(config = {}, message = {}) { config.hooks = [...this.getHooks(), '']; config.dialog ??= {}; + config.damageOptions ??= {}; for (const hook of config.hooks) { if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null; @@ -75,9 +77,7 @@ export default class DHRoll extends Roll { } if (config.skips?.createMessage) { - if (game.modules.get('dice-so-nice')?.active) { - await game.dice3d.showForRoll(roll, game.user, true); - } + await triggerChatRollFx([roll]); } else if (!config.source?.message) { config.message = await this.toMessage(roll, config); } @@ -85,6 +85,7 @@ export default class DHRoll extends Roll { static postEvaluate(roll, config = {}) { return { + ...roll.options.roll, total: roll.total, formula: roll.formula, dice: roll.dice.map(d => ({ @@ -103,9 +104,9 @@ export default class DHRoll extends Roll { if (action?.chatDisplay) { actionDescription = action ? await foundry.applications.ux.TextEditor.implementation.enrichHTML(action.description, { - relativeTo: config.data, - rollData: config.data.getRollData?.() ?? {} - }) + relativeTo: config.data, + rollData: config.data.getRollData?.() ?? {} + }) : null; config.actionChatMessageHandled = true; } diff --git a/module/dice/die/dualityDie.mjs b/module/dice/die/dualityDie.mjs index 83229425..cc7ee75e 100644 --- a/module/dice/die/dualityDie.mjs +++ b/module/dice/die/dualityDie.mjs @@ -1,4 +1,4 @@ -import { ResourceUpdateMap } from '../../data/action/baseAction.mjs'; +import { updateResourcesForDualityReroll } from '../helpers.mjs'; export default class DualityDie extends foundry.dice.terms.Die { constructor(options) { @@ -12,24 +12,6 @@ export default class DualityDie extends foundry.dice.terms.Die { return roll.withHope ? 1 : roll.withFear ? -1 : 0; } - #updateResources(oldDuality, newDuality, actor) { - const { hopeFear } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); - if (game.user.isGM ? !hopeFear.gm : !hopeFear.players) return; - - const updates = []; - const hope = (newDuality >= 0 ? 1 : 0) - (oldDuality >= 0 ? 1 : 0); - const stress = (newDuality === 0 ? 1 : 0) - (oldDuality === 0 ? 1 : 0); - const fear = (newDuality === -1 ? 1 : 0) - (oldDuality === -1 ? 1 : 0); - - if (hope !== 0) updates.push({ key: 'hope', value: hope, total: -1 * hope, enabled: true }); - if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, total: stress, enabled: true }); - if (fear !== 0) updates.push({ key: 'fear', value: fear, total: -1 * fear, enabled: true }); - - const resourceUpdates = new ResourceUpdateMap(actor); - resourceUpdates.addResources(updates); - resourceUpdates.updateResources(); - } - async reroll(modifier, options) { const oldDuality = this.#getDualityState(options.liveRoll.roll); await super.reroll(modifier, options); @@ -57,7 +39,7 @@ export default class DualityDie extends foundry.dice.terms.Die { if (options.liveRoll.isReaction) return; const newDuality = this.#getDualityState(options.liveRoll.roll); - this.#updateResources(oldDuality, newDuality, options.liveRoll.actor); + updateResourcesForDualityReroll(oldDuality, newDuality, options.liveRoll.actor); } } diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index d58811fe..1cfed094 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -1,6 +1,8 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; import D20Roll from './d20Roll.mjs'; import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; +import { getDiceSoNicePresets } from '../config/generalConfig.mjs'; +import { updateResourcesForDualityReroll } from './helpers.mjs'; export default class DualityRoll extends D20Roll { _advantageNumber = 1; @@ -107,10 +109,10 @@ export default class DualityRoll extends D20Roll { const label = this.guaranteedCritical ? 'DAGGERHEART.GENERAL.guaranteedCriticalSuccess' : this.isCritical - ? 'DAGGERHEART.GENERAL.criticalSuccess' - : this.withHope - ? 'DAGGERHEART.GENERAL.hope' - : 'DAGGERHEART.GENERAL.fear'; + ? 'DAGGERHEART.GENERAL.criticalSuccess' + : this.withHope + ? 'DAGGERHEART.GENERAL.hope' + : 'DAGGERHEART.GENERAL.fear'; return game.i18n.localize(label); } @@ -130,20 +132,14 @@ export default class DualityRoll extends D20Roll { } createBaseDice() { - if ( - this.dice[0] instanceof game.system.api.dice.diceTypes.HopeDie && - this.dice[1] instanceof game.system.api.dice.diceTypes.FearDie - ) { - this.terms = [this.terms[0], this.terms[1], this.terms[2]]; - return; - } + this.terms = [this.terms[0], this.terms[1], this.terms[2]]; this.terms[0] = new game.system.api.dice.diceTypes.HopeDie({ - faces: this.data.rules.dualityRoll?.defaultHopeDice ?? 12 + faces: this.terms[0]?.faces ?? this.data.rules.dualityRoll?.defaultHopeDice ?? 12 }); this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' }); this.terms[2] = new game.system.api.dice.diceTypes.FearDie({ - faces: this.data.rules.dualityRoll?.defaultFearDice ?? 12 + faces: this.terms[2]?.faces ?? this.data.rules.dualityRoll?.defaultFearDice ?? 12 }); } @@ -151,8 +147,8 @@ export default class DualityRoll extends D20Roll { const advDieClass = this.hasAdvantage ? game.system.api.dice.diceTypes.AdvantageDie : this.hasDisadvantage - ? game.system.api.dice.diceTypes.DisadvantageDie - : null; + ? game.system.api.dice.diceTypes.DisadvantageDie + : null; if (advDieClass) { const advDie = new advDieClass({ faces: this.advantageFaces, number: this.advantageNumber }); if (this.terms.length < 4) { @@ -388,4 +384,40 @@ export default class DualityRoll extends D20Roll { if (currentCombatant?.actorId == config.data.id) ui.combat.setCombatantSpotlight(currentCombatant.id); } } + + async reroll(options) { + const oldDuality = this.withHope ? 1 : this.withFear ? -1 : 0; + const rerolled = DualityRoll.fromData((await super.reroll(options)).toJSON()); + + if (options?.liveRoll) { + if (game.modules.get('dice-so-nice')?.active) { + const diceAppearance = await getDiceSoNicePresets( + rerolled, + rerolled.dHope.denomination, + rerolled.dFear.denomination + ); + rerolled.dHope.options.appearance = diceAppearance.hope.appearance; + rerolled.dFear.options.appearance = diceAppearance.fear.appearance; + if (rerolled.dAdvantage) rerolled.dAdvantage.options.appearance = diceAppearance.advantage.appearance; + if (rerolled.dDisadvantage) + rerolled.dDisadvantage.options.appearance = diceAppearance.disadvantage.appearance; + + await game.dice3d.showForRoll(rerolled, game.user, true); + } else { + foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); + } + + if (this.options.actionType === 'reaction') return; + + const newDuality = rerolled.withHope ? 1 : rerolled.withFear ? -1 : 0; + const actor = await foundry.utils.fromUuid(this.options.source.actor); + updateResourcesForDualityReroll(oldDuality, newDuality, actor); + } + + return rerolled; + } + + fromJSON(json) { + return super.fromJSON(json); + } } diff --git a/module/dice/helpers.mjs b/module/dice/helpers.mjs new file mode 100644 index 00000000..35adb8b7 --- /dev/null +++ b/module/dice/helpers.mjs @@ -0,0 +1,19 @@ +import { ResourceUpdateMap } from '../data/action/baseAction.mjs'; + +export function updateResourcesForDualityReroll(oldDuality, newDuality, actor) { + const { hopeFear } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); + if (game.user.isGM ? !hopeFear.gm : !hopeFear.players) return; + + const updates = []; + const hope = (newDuality >= 0 ? 1 : 0) - (oldDuality >= 0 ? 1 : 0); + const stress = (newDuality === 0 ? 1 : 0) - (oldDuality === 0 ? 1 : 0); + const fear = (newDuality === -1 ? 1 : 0) - (oldDuality === -1 ? 1 : 0); + + if (hope !== 0) updates.push({ key: 'hope', value: hope, total: -1 * hope, enabled: true }); + if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, total: stress, enabled: true }); + if (fear !== 0) updates.push({ key: 'fear', value: fear, total: -1 * fear, enabled: true }); + + const resourceUpdates = new ResourceUpdateMap(actor); + resourceUpdates.addResources(updates); + resourceUpdates.updateResources(); +} diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index f9239a90..3518210b 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -175,8 +175,8 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { return model instanceof documentClass ? model : model.parent - ? this.#resolveParentDocument(model.parent, documentClass) - : null; + ? this.#resolveParentDocument(model.parent, documentClass) + : null; } static getChangeValue(model, change, effect) { diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index e4c11a5c..fb10435f 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -65,6 +65,11 @@ export default class DhpActor extends Actor { }; } + static createDialog(data, createOptions, options, renderOptions) { + options.classes = [options.classes ?? [], 'actor-create'].flat(); // handled in hook + return super.createDialog(data, createOptions, options, renderOptions); + } + /* -------------------------------------------- */ /** @inheritDoc */ @@ -109,6 +114,14 @@ export default class DhpActor extends Actor { }); } + if (this.type === 'npc') { + Object.assign(update, { + prototypeToken: { + disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY + } + }); + } + this.updateSource(update); } diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 78bab016..480f8c69 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -9,9 +9,9 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { actor && this.isContentVisible ? actor : { - img: this.author.avatar ? this.author.avatar : 'icons/svg/mystery-man.svg', - name: '' - }; + img: this.author.avatar ? this.author.avatar : 'icons/svg/mystery-man.svg', + name: '' + }; /* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */ const html = await super.renderHTML({ actor: actorData, author: this.author }); @@ -183,7 +183,11 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { if (pendingingSaves.length) { const confirm = await foundry.applications.api.DialogV2.confirm({ window: { title: game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.title') }, - content: `

${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.unfinishedRolls')}

${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.confirmation')}

${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.warning')}

` + content: ` +

${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.unfinishedRolls')}

+

${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.warning')}

+

${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.confirmation')}

+ ` }); if (!confirm) return; } @@ -247,8 +251,24 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { const targets = this.filterPermTargets(this.system.hitTargets), config = foundry.utils.deepClone(this.system); config.event = event; + if (targets.length === 0) - ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm')); + return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm')); + else if (config.hasSave) { + const pendingingSaves = targets.filter(t => t.saved.success === null); + if (pendingingSaves.length) { + const confirm = await foundry.applications.api.DialogV2.confirm({ + window: { title: game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.title') }, + content: ` +

${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.unfinishedRolls')}

+

${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.warning')}

+

${game.i18n.localize('DAGGERHEART.APPLICATIONS.PendingReactionsDialog.confirmation')}

+ ` + }); + if (!confirm) return; + } + } + this.consumeOnSuccess(); this.system.action?.workflow.get('effects')?.execute(config, targets, true); } @@ -270,14 +290,14 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { behaviors: effects.length > 0 ? [ - { - name: game.i18n.localize('TYPES.RegionBehavior.applyActiveEffect'), - type: 'applyActiveEffect', - system: { - effects: effects - } - } - ] + { + name: game.i18n.localize('TYPES.RegionBehavior.applyActiveEffect'), + type: 'applyActiveEffect', + system: { + effects: effects + } + } + ] : [], displayMeasurements: true, locked: false, diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 603ca594..4716068d 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -82,6 +82,7 @@ export default class DHItem extends foundry.documents.Item { /** @inheritdoc */ static async createDialog(data = {}, createOptions = {}, options = {}) { const { folders, types, template, context = {}, ...dialogOptions } = options; + dialogOptions.classes = [options.classes ?? [], 'item-create'].flat(); // handled in hook if (types?.length === 0) { throw new Error('The array of sub-types to restrict to must not be empty.'); @@ -97,8 +98,8 @@ export default class DHItem extends foundry.documents.Item { isInventoryItem === true ? 'Inventory Items' //TODO localize : isInventoryItem === false - ? 'Character Items' //TODO localize - : 'Other'; //TODO localize + ? 'Character Items' //TODO localize + : 'Other'; //TODO localize return { value: type, label, group }; } diff --git a/module/documents/token.mjs b/module/documents/token.mjs index 30862724..8e91d4f0 100644 --- a/module/documents/token.mjs +++ b/module/documents/token.mjs @@ -324,7 +324,7 @@ export default class DHToken extends CONFIG.Token.documentClass { } let x = 0.5 * bottom; let y = 0.25; - for (let k = width - bottom; k--; ) { + for (let k = width - bottom; k--;) { points.push(x, y); x += 0.5; y -= 0.25; @@ -333,7 +333,7 @@ export default class DHToken extends CONFIG.Token.documentClass { y += 0.25; } points.push(x, y); - for (let k = bottom; k--; ) { + for (let k = bottom; k--;) { y += 0.5; points.push(x, y); x += 0.5; @@ -341,14 +341,14 @@ export default class DHToken extends CONFIG.Token.documentClass { points.push(x, y); } y += 0.5; - for (let k = top; k--; ) { + for (let k = top; k--;) { points.push(x, y); x -= 0.5; y += 0.25; points.push(x, y); y += 0.5; } - for (let k = width - top; k--; ) { + for (let k = width - top; k--;) { points.push(x, y); x -= 0.5; y += 0.25; @@ -357,7 +357,7 @@ export default class DHToken extends CONFIG.Token.documentClass { y -= 0.25; } points.push(x, y); - for (let k = top; k--; ) { + for (let k = top; k--;) { y -= 0.5; points.push(x, y); x -= 0.5; @@ -365,7 +365,7 @@ export default class DHToken extends CONFIG.Token.documentClass { points.push(x, y); } y -= 0.5; - for (let k = bottom; k--; ) { + for (let k = bottom; k--;) { points.push(x, y); x += 0.5; y -= 0.25; diff --git a/module/documents/tooltipManager.mjs b/module/documents/tooltipManager.mjs index 18c03169..3e3f4a16 100644 --- a/module/documents/tooltipManager.mjs +++ b/module/documents/tooltipManager.mjs @@ -3,7 +3,6 @@ import { AdversaryBPPerEncounter, BaseBPPerEncounter } from '../config/encounter export default class DhTooltipManager extends foundry.helpers.interaction.TooltipManager { #wide = false; #bordered = false; - #active = false; async activate(element, options = {}) { const { TextEditor } = foundry.applications.ux; diff --git a/module/enrichers/DamageEnricher.mjs b/module/enrichers/DamageEnricher.mjs index e3f9c42a..db0e8729 100644 --- a/module/enrichers/DamageEnricher.mjs +++ b/module/enrichers/DamageEnricher.mjs @@ -1,7 +1,7 @@ import { parseInlineParams } from './parser.mjs'; export default function DhDamageEnricher(match, _options) { - const { value, type, inline } = parseInlineParams(match[1]); + const { value, type, inline } = parseInlineParams(match[1], { first: 'value' }); if (!value || !type) return match[0]; return getDamageMessage(value, type, inline, match[0]); } @@ -59,7 +59,7 @@ export const renderDamageButton = async event => { { formula: value, applyTo: CONFIG.DH.GENERAL.healingTypes.hitPoints.id, - type: type + damageTypes: type } ] }; diff --git a/module/enrichers/DualityRollEnricher.mjs b/module/enrichers/DualityRollEnricher.mjs index 5b66179f..a7db01a4 100644 --- a/module/enrichers/DualityRollEnricher.mjs +++ b/module/enrichers/DualityRollEnricher.mjs @@ -15,8 +15,8 @@ function getDualityMessage(roll, flavor) { (roll?.trait ? game.i18n.format('DAGGERHEART.GENERAL.rollWith', { roll: trait }) : roll?.reaction - ? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll') - : game.i18n.localize('DAGGERHEART.GENERAL.duality')); + ? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll') + : game.i18n.localize('DAGGERHEART.GENERAL.duality')); const dataLabel = trait ? game.i18n.localize(abilities[roll.trait].label) @@ -25,14 +25,14 @@ function getDualityMessage(roll, flavor) { const advantage = roll?.advantage ? CONFIG.DH.ACTIONS.advantageState.advantage.value : roll?.disadvantage - ? CONFIG.DH.ACTIONS.advantageState.disadvantage.value - : undefined; + ? CONFIG.DH.ACTIONS.advantageState.disadvantage.value + : undefined; const advantageLabel = advantage === CONFIG.DH.ACTIONS.advantageState.advantage.value ? 'Advantage' : advantage === CONFIG.DH.ACTIONS.advantageState.disadvantage.value - ? 'Disadvantage' - : undefined; + ? 'Disadvantage' + : undefined; const dualityElement = document.createElement('span'); dualityElement.innerHTML = ` diff --git a/module/enrichers/TemplateEnricher.mjs b/module/enrichers/TemplateEnricher.mjs index 8db3ec14..bbe93b17 100644 --- a/module/enrichers/TemplateEnricher.mjs +++ b/module/enrichers/TemplateEnricher.mjs @@ -8,8 +8,8 @@ export default function DhTemplateEnricher(match, _options) { const range = params.range && Number.isNaN(Number(params.range)) ? Object.values(CONFIG.DH.GENERAL.templateRanges).find( - x => x.id.toLowerCase() === params.range || x.short === params.range - )?.id + x => x.id.toLowerCase() === params.range || x.short === params.range + )?.id : params.range; if (!CONFIG.DH.GENERAL.templateTypes[type] || !range) return match[0]; diff --git a/module/enrichers/parser.mjs b/module/enrichers/parser.mjs index 365caec9..76ea0b73 100644 --- a/module/enrichers/parser.mjs +++ b/module/enrichers/parser.mjs @@ -8,7 +8,7 @@ export function parseInlineParams(paramString, { first } = {}) { const parts = paramString.split('|').map(x => x.trim()); const params = {}; for (const [idx, param] of parts.entries()) { - if (first && idx === 0) { + if (first && idx === 0 && !param.includes(':')) { params[first] = param; } else { const parts = param.split(':'); diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 7bc5fa25..af6c2777 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -108,9 +108,9 @@ export const tagifyElement = (element, baseOptions, onChange, tagifyOptions = {} const options = Array.isArray(baseOptions) ? baseOptions : Object.keys(baseOptions).map(optionKey => ({ - ...baseOptions[optionKey], - id: optionKey - })); + ...baseOptions[optionKey], + id: optionKey + })); const tagifyElement = new Tagify(element, { tagTextProp: 'name', @@ -605,8 +605,8 @@ export function calculateExpectedValue(formulaOrTerms) { const terms = Array.isArray(formulaOrTerms) ? formulaOrTerms : typeof formulaOrTerms === 'string' - ? parseTermsFromSimpleFormula(formulaOrTerms) - : [formulaOrTerms]; + ? parseTermsFromSimpleFormula(formulaOrTerms) + : [formulaOrTerms]; return terms.reduce((r, t) => r + (t.bonus ?? 0) + (t.diceQuantity ? (t.diceQuantity * (t.faces + 1)) / 2 : 0), 0); } @@ -656,8 +656,8 @@ export async function RefreshFeatures( 'resource.value': increasing ? 0 : game.system.api.documents.DhActiveEffect.effectSafeEval( - Roll.replaceFormulaData(item.system.resource.max, actor.getRollData()) - ) + Roll.replaceFormulaData(item.system.resource.max, actor.getRollData()) + ) }; } if (item.system.metadata?.hasActions) { @@ -864,3 +864,58 @@ export function camelize(str) { }) .replace(/\s+/g, ''); } + +/** Bulk load a list of documents using uuids. Returns the documents in the same order */ +export async function fromUuids(uuids) { + // Set up base entries. Each step works on a sublist of these objects + const entries = uuids.map(uuid => ({ + uuid, + parsed: foundry.utils.parseUuid(uuid), + value: foundry.utils.fromUuidSync(uuid) + })); + + // Handle missing uuids for embedded documents first + // A value may be index data, so we check if its a document + const packEmbeddedEntries = entries.filter( + e => + !(e.value instanceof Document) && + e.parsed && + e.parsed.collection instanceof foundry.documents.collections.CompendiumCollection && + e.parsed.embedded.length > 0 + ); + await Promise.all( + packEmbeddedEntries.map(async e => { + e.value = await fromUuid(e.uuid); + return true; + }) + ); + + // Handle missing top level pack stuff, by batching per pack + const missingTopLevel = entries.filter(e => !(e.value instanceof Document) && e.value?.pack); + for (const packGroup of Object.values(Object.groupBy(missingTopLevel, e => e.value.pack))) { + const pack = game.packs.get(packGroup[0].value.pack); + if (!pack) continue; + + const ids = packGroup.map(p => p.parsed?.id).filter(id => !!id); + const documents = await pack.getDocuments({ _id__in: ids }); + for (const p of packGroup) { + p.value = documents.find(d => d.id === p.parsed.id) ?? p.value; + } + } + + return entries.map(e => e.value); +} +/** + * Triggers DiceSoNice rolls or dice roll audio for rolls. Not used for duality rolls. + * @param { Roll[] } rolls + * @return { void } + */ +export async function triggerChatRollFx(rolls, options = { whisper: false, blind: false }) { + const { whisper, blind } = options; + if (game.modules.get('dice-so-nice')?.active) { + const rerollPromises = rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true, whisper, blind)); + await Promise.allSettled(rerollPromises); + } else { + foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); + } +} diff --git a/module/systemRegistration/migrations.mjs b/module/systemRegistration/migrations.mjs index b4c446b2..b718a127 100644 --- a/module/systemRegistration/migrations.mjs +++ b/module/systemRegistration/migrations.mjs @@ -24,8 +24,8 @@ export async function runMigrations() { const { originItemType, isMulticlass, identifier } = item.system; const base = originItemType ? actor.items.find( - x => x.type === originItemType && Boolean(isMulticlass) === Boolean(x.system.isMulticlass) - ) + x => x.type === originItemType && Boolean(isMulticlass) === Boolean(x.system.isMulticlass) + ) : null; if (base) { const feature = base.system.features.find(x => x.item && x.item.uuid === item.uuid); diff --git a/package-lock.json b/package-lock.json index 28223032..dee096eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,18 +13,20 @@ "rollup": "^4.40.0" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@foundryvtt/foundryvtt-cli": "^1.0.2", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", + "@stylistic/eslint-plugin": "^5.10.0", "concurrently": "^8.2.2", "eslint": "^10.2.1", - "eslint-plugin-prettier": "^5.5.5", "globals": "^17.5.0", "husky": "^9.1.7", "lint-staged": "^16.4.0", "postcss": "^8.4.32", - "prettier": "^3.5.3", - "rollup-plugin-postcss": "^4.0.2" + "rollup-plugin-postcss": "^4.0.2", + "typescript": "^6.0.3", + "typescript-eslint": "^8.60.1" } }, "node_modules/@babel/runtime": { @@ -158,6 +160,27 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, "node_modules/@eslint/object-schema": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", @@ -420,19 +443,6 @@ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - } - }, "node_modules/@rollup/plugin-commonjs": { "version": "25.0.8", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz", @@ -761,6 +771,58 @@ "util": "^0.12.4" } }, + "node_modules/@stylistic/eslint-plugin": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz", + "integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/types": "^8.56.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.0.0 || ^10.0.0" + } + }, + "node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@stylistic/eslint-plugin/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -795,6 +857,288 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", + "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/type-utils": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.60.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", + "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", + "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.60.1", + "@typescript-eslint/types": "^8.60.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", + "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", + "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", + "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", + "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", + "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.60.1", + "@typescript-eslint/tsconfig-utils": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", + "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", + "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@yaireo/tagify": { "version": "4.35.1", "resolved": "https://registry.npmjs.org/@yaireo/tagify/-/tagify-4.35.1.tgz", @@ -1853,10 +2197,11 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -2241,37 +2586,6 @@ } } }, - "node_modules/eslint-plugin-prettier": { - "version": "5.5.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", - "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.1", - "synckit": "^0.11.12" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, "node_modules/eslint-scope": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", @@ -2511,13 +2825,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", @@ -2554,6 +2861,24 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -5217,34 +5542,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", - "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", - "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -6053,22 +6350,6 @@ "node": ">= 10" } }, - "node_modules/synckit": { - "version": "0.11.12", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", - "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.2.9" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" - } - }, "node_modules/teex": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", @@ -6116,6 +6397,23 @@ "node": ">=18" } }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6147,6 +6445,19 @@ "tree-kill": "cli.js" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -6171,6 +6482,44 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.1.tgz", + "integrity": "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.60.1", + "@typescript-eslint/parser": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", diff --git a/package.json b/package.json index 73a7fe99..ede90401 100644 --- a/package.json +++ b/package.json @@ -24,18 +24,20 @@ "prepare": "husky" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@foundryvtt/foundryvtt-cli": "^1.0.2", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", + "@stylistic/eslint-plugin": "^5.10.0", "concurrently": "^8.2.2", "eslint": "^10.2.1", - "eslint-plugin-prettier": "^5.5.5", "globals": "^17.5.0", "husky": "^9.1.7", "lint-staged": "^16.4.0", "postcss": "^8.4.32", - "prettier": "^3.5.3", - "rollup-plugin-postcss": "^4.0.2" + "rollup-plugin-postcss": "^4.0.2", + "typescript": "^6.0.3", + "typescript-eslint": "^8.60.1" }, "lint-staged": { "**/*": "eslint --fix" diff --git a/styles/daggerheart.less b/styles/daggerheart.less index 187402fb..4da2e043 100755 --- a/styles/daggerheart.less +++ b/styles/daggerheart.less @@ -1,19 +1,11 @@ @import './less/sheets/index.less'; @import './less/sheets-settings/index.less'; - @import './less/dialog/index.less'; - -@import './less//hud/index.less'; - -@import './less/utils/colors.less'; -@import './less/utils/fonts.less'; - +@import './less/hud/index.less'; +@import './less/utils/index.less'; @import './less/global/index.less'; - @import './less/ui/index.less'; - @import './less/ux/index.less'; @import '../build/tagify.css'; -@import './less/utils/mixin.less'; diff --git a/styles/less/dialog/actions/index.less b/styles/less/dialog/actions/index.less new file mode 100644 index 00000000..e9cc0401 --- /dev/null +++ b/styles/less/dialog/actions/index.less @@ -0,0 +1 @@ +@import "./action-list.less"; \ No newline at end of file diff --git a/styles/less/dialog/attribution/index.less b/styles/less/dialog/attribution/index.less new file mode 100644 index 00000000..2f8eaf45 --- /dev/null +++ b/styles/less/dialog/attribution/index.less @@ -0,0 +1 @@ +@import "./sheet.less"; \ No newline at end of file diff --git a/styles/less/dialog/beastform/index.less b/styles/less/dialog/beastform/index.less new file mode 100644 index 00000000..2f8eaf45 --- /dev/null +++ b/styles/less/dialog/beastform/index.less @@ -0,0 +1 @@ +@import "./sheet.less"; \ No newline at end of file diff --git a/styles/less/dialog/character-creation/index.less b/styles/less/dialog/character-creation/index.less new file mode 100644 index 00000000..adf8d57a --- /dev/null +++ b/styles/less/dialog/character-creation/index.less @@ -0,0 +1,4 @@ +@import "./sheet.less"; +@import "./creation-action-footer.less"; +@import "./selections-container.less"; +@import "./tab-navigation.less"; \ No newline at end of file diff --git a/styles/less/dialog/character-reset/index.less b/styles/less/dialog/character-reset/index.less new file mode 100644 index 00000000..1c574e81 --- /dev/null +++ b/styles/less/dialog/character-reset/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/dialog/compendiumBrowserPackDialog/index.less b/styles/less/dialog/compendiumBrowserPackDialog/index.less new file mode 100644 index 00000000..1c574e81 --- /dev/null +++ b/styles/less/dialog/compendiumBrowserPackDialog/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/dialog/damage-reduction/index.less b/styles/less/dialog/damage-reduction/index.less new file mode 100644 index 00000000..0b8e94a8 --- /dev/null +++ b/styles/less/dialog/damage-reduction/index.less @@ -0,0 +1,2 @@ +@import './sheets.less'; +@import './damage-reduction-container.less'; \ No newline at end of file diff --git a/styles/less/dialog/damage-selection/index.less b/styles/less/dialog/damage-selection/index.less new file mode 100644 index 00000000..1c574e81 --- /dev/null +++ b/styles/less/dialog/damage-selection/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/dialog/death-move/index.less b/styles/less/dialog/death-move/index.less new file mode 100644 index 00000000..8a8a16c4 --- /dev/null +++ b/styles/less/dialog/death-move/index.less @@ -0,0 +1 @@ +@import './death-move-container.less'; \ No newline at end of file diff --git a/styles/less/dialog/dice-roll/index.less b/styles/less/dialog/dice-roll/index.less new file mode 100644 index 00000000..8e0af6e0 --- /dev/null +++ b/styles/less/dialog/dice-roll/index.less @@ -0,0 +1 @@ +@import './roll-selection.less'; \ No newline at end of file diff --git a/styles/less/dialog/downtime/index.less b/styles/less/dialog/downtime/index.less new file mode 100644 index 00000000..09cc2dfe --- /dev/null +++ b/styles/less/dialog/downtime/index.less @@ -0,0 +1 @@ +@import './downtime-container.less'; \ No newline at end of file diff --git a/styles/less/dialog/group-roll-dialog/_common.less b/styles/less/dialog/group-roll-dialog/_common.less deleted file mode 100644 index f74ab8a0..00000000 --- a/styles/less/dialog/group-roll-dialog/_common.less +++ /dev/null @@ -1,44 +0,0 @@ -h1 { - color: @color-text-emphatic; - font: 700 var(--font-size-24) var(--dh-font-subtitle); - text-align: center; -} - -header { - --bar-color: light-dark(@dark-blue, @golden); - color: light-dark(@dark, @beige); - display: flex; - justify-content: center; - align-items: center; - - &:not(:first-child) { - margin-top: var(--spacer-8); - } - - span { - padding: 0 10px; - } - - &:before { - content: ' '; - flex: 1; - height: 1px; - background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, var(--bar-color) 100%); - } - - &:after { - content: ' '; - flex: 1; - height: 1px; - background: linear-gradient(90deg, var(--bar-color) 0%, rgba(0, 0, 0, 0) 100%); - } -} - -img.portrait { - border-radius: 50%; - border: none; - object-fit: cover; - object-position: center top; - width: 2.5rem; - height: 2.5rem; -} diff --git a/styles/less/dialog/group-roll-dialog/index.less b/styles/less/dialog/group-roll-dialog/index.less index 27925fa2..f90b57dc 100644 --- a/styles/less/dialog/group-roll-dialog/index.less +++ b/styles/less/dialog/group-roll-dialog/index.less @@ -1,8 +1,3 @@ -.daggerheart.dialog.dh-style.views.group-roll-dialog { - .window-content { - @import "./_common.less"; - } -} - -@import "./initialization.less"; -@import "./main.less"; +@import './sheet.less'; +@import './initialization.less'; +@import './main.less'; diff --git a/styles/less/dialog/group-roll-dialog/sheet.less b/styles/less/dialog/group-roll-dialog/sheet.less new file mode 100644 index 00000000..938710c9 --- /dev/null +++ b/styles/less/dialog/group-roll-dialog/sheet.less @@ -0,0 +1,48 @@ +.daggerheart.dialog.dh-style.views.group-roll-dialog { + .window-content { + h1 { + color: @color-text-emphatic; + font: 700 var(--font-size-24) var(--dh-font-subtitle); + text-align: center; + } + + header { + --bar-color: light-dark(@dark-blue, @golden); + color: light-dark(@dark, @beige); + display: flex; + justify-content: center; + align-items: center; + + &:not(:first-child) { + margin-top: var(--spacer-8); + } + + span { + padding: 0 10px; + } + + &:before { + content: ' '; + flex: 1; + height: 1px; + background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, var(--bar-color) 100%); + } + + &:after { + content: ' '; + flex: 1; + height: 1px; + background: linear-gradient(90deg, var(--bar-color) 0%, rgba(0, 0, 0, 0) 100%); + } + } + + img.portrait { + border-radius: 50%; + border: none; + object-fit: cover; + object-position: center top; + width: 2.5rem; + height: 2.5rem; + } + } +} \ No newline at end of file diff --git a/styles/less/dialog/image-select/index.less b/styles/less/dialog/image-select/index.less new file mode 100644 index 00000000..1c574e81 --- /dev/null +++ b/styles/less/dialog/image-select/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/dialog/index.less b/styles/less/dialog/index.less index 11d9635e..4ce4834e 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -1,44 +1,20 @@ -@import './attribution/sheet.less'; -@import './level-up/index.less'; - -@import './resource-dice/sheet.less'; - -@import './actions/action-list.less'; - -@import './damage-selection/sheet.less'; - -@import './downtime/downtime-container.less'; - -@import './death-move/death-move-container.less'; - -@import './beastform/sheet.less'; - -@import './character-creation/creation-action-footer.less'; -@import './character-creation/selections-container.less'; -@import './character-creation/sheet.less'; -@import './character-creation/tab-navigation.less'; - -@import './dice-roll/roll-selection.less'; -@import './damage-reduction/damage-reduction-container.less'; -@import './damage-reduction/sheets.less'; - -@import './multiclass-choice/sheet.less'; - -@import './reroll-dialog/sheet.less'; - -@import './tag-team-dialog/initialization.less'; -@import './tag-team-dialog/sheet.less'; - +@import './actions/index.less'; +@import './attribution/index.less'; +@import './beastform/index.less'; +@import './character-creation/index.less'; +@import './character-reset/index.less'; +@import './compendiumBrowserPackDialog/index.less'; +@import './damage-reduction/index.less'; +@import './damage-selection/index.less'; +@import './death-move/index.less'; +@import './dice-roll/index.less'; +@import './downtime/index.less'; @import './group-roll-dialog/index.less'; - -@import './image-select/sheet.less'; - -@import './item-transfer/sheet.less'; - -@import './settings/change-currency-icon.less'; - -@import './risk-it-all/sheet.less'; - -@import './character-reset/sheet.less'; - -@import './compendiumBrowserPackDialog/sheet.less'; +@import './level-up/index.less'; +@import './resource-dice/index.less'; +@import './multiclass-choice/index.less'; +@import './tag-team-dialog/index.less'; +@import './image-select/index.less'; +@import './item-transfer/index.less'; +@import './settings/index.less'; +@import './risk-it-all/index.less'; \ No newline at end of file diff --git a/styles/less/dialog/item-transfer/index.less b/styles/less/dialog/item-transfer/index.less new file mode 100644 index 00000000..1c574e81 --- /dev/null +++ b/styles/less/dialog/item-transfer/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/dialog/level-up/sheet.less b/styles/less/dialog/level-up/sheet.less index c663f304..9ebd9331 100644 --- a/styles/less/dialog/level-up/sheet.less +++ b/styles/less/dialog/level-up/sheet.less @@ -14,8 +14,7 @@ .tab.active { flex: 1; overflow: auto; - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; + .with-scroll-shadows(); } div[data-application-part='form'] { diff --git a/styles/less/dialog/level-up/summary-container.less b/styles/less/dialog/level-up/summary-container.less index 97353ba7..de7c9f4a 100644 --- a/styles/less/dialog/level-up/summary-container.less +++ b/styles/less/dialog/level-up/summary-container.less @@ -18,7 +18,7 @@ overflow: auto; padding: 10px 0; max-height: 700px; - mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); + .with-scroll-shadows(); .level-achievements-container, .level-advancements-container { diff --git a/styles/less/dialog/multiclass-choice/index.less b/styles/less/dialog/multiclass-choice/index.less new file mode 100644 index 00000000..1c574e81 --- /dev/null +++ b/styles/less/dialog/multiclass-choice/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/dialog/reroll-dialog/sheet.less b/styles/less/dialog/reroll-dialog/sheet.less deleted file mode 100644 index 71c94d80..00000000 --- a/styles/less/dialog/reroll-dialog/sheet.less +++ /dev/null @@ -1,125 +0,0 @@ -.daggerheart.dialog.dh-style.views.reroll-dialog { - .window-content { - max-width: 648px; - } - - .reroll-outer-container { - h2 { - margin: 0; - } - - .dices-container { - display: flex; - flex-wrap: wrap; - gap: 8px; - } - - .dice-outer-container { - width: 300px; - - legend { - display: flex; - align-items: center; - gap: 4px; - - i { - margin-right: 4px; - } - } - - .dice-container { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr; - - .result-container { - position: relative; - aspect-ratio: 1; - display: flex; - align-items: center; - justify-content: center; - font-size: 1.375rem; - opacity: 0.8; - - &.selected { - opacity: 1; - border: 1px solid; - border-radius: 6px; - border-color: light-dark(@dark-blue, @golden); - filter: drop-shadow(0 0 3px @golden); - } - - &:before { - content: ' '; - position: absolute; - width: 100%; - height: 100%; - z-index: -1; - mask: var(--svg-die) no-repeat center; - mask-size: contain; - background: linear-gradient(139.01deg, #efe6d8 3.51%, #372e1f 96.49%); - } - - &.d4:before { - --svg-die: url(../assets/icons/dice/default/d4.svg); - } - &.d6:before { - --svg-die: url(../assets/icons/dice/default/d6.svg); - } - &.d8:before { - --svg-die: url(../assets/icons/dice/default/d8.svg); - } - &.d10:before { - --svg-die: url(../assets/icons/dice/default/d10.svg); - } - &.d12:before { - --svg-die: url('../assets/icons/dice/default/d12.svg'); - } - &.d20:before { - --svg-die: url(../assets/icons/dice/default/d20.svg); - } - - .to-reroll-result { - position: absolute; - bottom: -7px; - gap: 2px; - border: 1px solid; - border-radius: 6px; - background-image: url(../assets/parchments/dh-parchment-dark.png); - display: flex; - align-items: center; - padding: 2px 6px; - - input { - margin: 0; - height: 12px; - line-height: 0px; - position: relative; - top: 1px; - - &:before, - &:after { - line-height: 12px; - font-size: var(--font-size-12); - } - } - - i { - font-size: var(--font-size-10); - } - } - } - } - } - } - - footer { - margin-top: 8px; - display: flex; - justify-content: space-between; - - .controls { - display: flex; - gap: 8px; - } - } -} diff --git a/styles/less/dialog/resource-dice/index.less b/styles/less/dialog/resource-dice/index.less new file mode 100644 index 00000000..1c574e81 --- /dev/null +++ b/styles/less/dialog/resource-dice/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/dialog/risk-it-all/index.less b/styles/less/dialog/risk-it-all/index.less new file mode 100644 index 00000000..1c574e81 --- /dev/null +++ b/styles/less/dialog/risk-it-all/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/dialog/settings/index.less b/styles/less/dialog/settings/index.less new file mode 100644 index 00000000..235f3b9c --- /dev/null +++ b/styles/less/dialog/settings/index.less @@ -0,0 +1 @@ +@import './change-currency-icon.less'; \ No newline at end of file diff --git a/styles/less/dialog/tag-team-dialog/index.less b/styles/less/dialog/tag-team-dialog/index.less new file mode 100644 index 00000000..8bf56824 --- /dev/null +++ b/styles/less/dialog/tag-team-dialog/index.less @@ -0,0 +1,2 @@ +@import './sheet.less'; +@import './initialization.less'; \ No newline at end of file diff --git a/styles/less/dialog/tag-team-dialog/sheet.less b/styles/less/dialog/tag-team-dialog/sheet.less index 3a112146..82bc0270 100644 --- a/styles/less/dialog/tag-team-dialog/sheet.less +++ b/styles/less/dialog/tag-team-dialog/sheet.less @@ -194,6 +194,7 @@ .roll-selection-container { display: flex; + gap: 16px; .select-roll-button { margin-top: 8px; diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index 7af8becd..e57ba50d 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -283,7 +283,7 @@ } &.fit-height { - height: 95%; + flex: 1; } &.flex { diff --git a/styles/less/global/prose-mirror.less b/styles/less/global/prose-mirror.less index 8412235d..e4b1249f 100644 --- a/styles/less/global/prose-mirror.less +++ b/styles/less/global/prose-mirror.less @@ -40,6 +40,11 @@ ul { list-style: disc; } + } + // Fixes centering and makes it not render over scrollbar + &:hover button.toggle:enabled { + display: flex; + right: 12px; } } } diff --git a/styles/less/global/tab-navigation.less b/styles/less/global/tab-navigation.less index 038a9749..3d143b4c 100755 --- a/styles/less/global/tab-navigation.less +++ b/styles/less/global/tab-navigation.less @@ -3,8 +3,7 @@ .daggerheart.dh-style { .tab-navigation { - margin: 5px 0; - height: 40px; + margin: 5px 0 10px 0; width: 100%; .navigation-container { @@ -21,6 +20,10 @@ a { color: @color-text-emphatic; + + &.empty:not(.active) { + opacity: 0.4; + } } } } diff --git a/styles/less/hud/index.less b/styles/less/hud/index.less index 459f8fd7..f1f4602e 100644 --- a/styles/less/hud/index.less +++ b/styles/less/hud/index.less @@ -1 +1 @@ -@import './token-hud/token-hud.less'; +@import './token-hud/index.less'; diff --git a/styles/less/hud/token-hud/index.less b/styles/less/hud/token-hud/index.less new file mode 100644 index 00000000..c86d0939 --- /dev/null +++ b/styles/less/hud/token-hud/index.less @@ -0,0 +1 @@ +@import './token-hud.less'; \ No newline at end of file diff --git a/styles/less/hud/token-hud/token-hud.less b/styles/less/hud/token-hud/token-hud.less index 3cb94e1e..3b998f4e 100644 --- a/styles/less/hud/token-hud/token-hud.less +++ b/styles/less/hud/token-hud/token-hud.less @@ -38,6 +38,9 @@ } .status-effects { + // TODO: Remove when the issue https://github.com/foundryvtt/foundryvtt/issues/14410 is resolved and Foundry handles it cleanly themselves. + grid-template-rows: min-content; + .effect-control-container { position: relative; diff --git a/styles/less/sheets-settings/adversary-settings/index.less b/styles/less/sheets-settings/adversary-settings/index.less new file mode 100644 index 00000000..5968577d --- /dev/null +++ b/styles/less/sheets-settings/adversary-settings/index.less @@ -0,0 +1,3 @@ +@import './sheet.less'; +@import './experiences.less'; +@import './features.less'; \ No newline at end of file diff --git a/styles/less/sheets-settings/adversary-settings/sheet.less b/styles/less/sheets-settings/adversary-settings/sheet.less index b4b0683b..e6eb8d0b 100644 --- a/styles/less/sheets-settings/adversary-settings/sheet.less +++ b/styles/less/sheets-settings/adversary-settings/sheet.less @@ -7,7 +7,7 @@ &.attack.active { display: flex; flex-direction: column; - gap: 16px; + gap: 12px; } .fieldsets-section { diff --git a/styles/less/sheets-settings/character-settings/index.less b/styles/less/sheets-settings/character-settings/index.less new file mode 100644 index 00000000..1c574e81 --- /dev/null +++ b/styles/less/sheets-settings/character-settings/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/sheets-settings/environment-settings/index.less b/styles/less/sheets-settings/environment-settings/index.less new file mode 100644 index 00000000..1e6ee34d --- /dev/null +++ b/styles/less/sheets-settings/environment-settings/index.less @@ -0,0 +1,2 @@ +@import './adversaries.less'; +@import './features.less'; \ No newline at end of file diff --git a/styles/less/sheets-settings/index.less b/styles/less/sheets-settings/index.less index f575f848..53a03868 100644 --- a/styles/less/sheets-settings/index.less +++ b/styles/less/sheets-settings/index.less @@ -1,8 +1,4 @@ @import './header.less'; -@import './adversary-settings/sheet.less'; -@import './adversary-settings/experiences.less'; -@import './adversary-settings/features.less'; -@import './character-settings/sheet.less'; - -@import './environment-settings/features.less'; -@import './environment-settings/adversaries.less'; +@import './adversary-settings/index.less'; +@import './character-settings/index.less'; +@import './environment-settings/index.less'; diff --git a/styles/less/sheets/actions/index.less b/styles/less/sheets/actions/index.less new file mode 100644 index 00000000..29ef8645 --- /dev/null +++ b/styles/less/sheets/actions/index.less @@ -0,0 +1 @@ +@import './actions.less'; \ No newline at end of file diff --git a/styles/less/sheets/activeEffects/index.less b/styles/less/sheets/activeEffects/index.less new file mode 100644 index 00000000..19f8a3a7 --- /dev/null +++ b/styles/less/sheets/activeEffects/index.less @@ -0,0 +1 @@ +@import './activeEffects.less'; \ No newline at end of file diff --git a/styles/less/sheets/actors/actor-sheet-shared.less b/styles/less/sheets/actors/actor-sheet-shared.less index 470067ca..b3eb0469 100644 --- a/styles/less/sheets/actors/actor-sheet-shared.less +++ b/styles/less/sheets/actors/actor-sheet-shared.less @@ -38,11 +38,23 @@ } .tab.inventory { - .search-section { - display: flex; + .gold-section { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; gap: 10px; - align-items: center; + padding: 10px 10px 0; + + .input { + color: light-dark(@dark, @beige); + } } + } + + .search-section { + display: flex; + gap: 10px; + align-items: center; + justify-content: space-between; .search-bar { position: relative; color: light-dark(@dark-blue-50, @beige-50); @@ -72,22 +84,11 @@ height: 32px; position: absolute; right: 20px; - font-size: 16px; + font-size: var(--font-size-16); z-index: 1; color: light-dark(@dark-blue-50, @beige-50); } } - - .gold-section { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr; - gap: 10px; - padding: 10px 10px 0; - - .input { - color: light-dark(@dark, @beige); - } - } } &.limited { diff --git a/styles/less/sheets/actors/adversary/effects.less b/styles/less/sheets/actors/adversary/effects.less index fbf74249..4aa44e51 100644 --- a/styles/less/sheets/actors/adversary/effects.less +++ b/styles/less/sheets/actors/adversary/effects.less @@ -1,4 +1,5 @@ @import '../../../utils/colors.less'; +@import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.adversary { .tab.effects { @@ -7,8 +8,8 @@ flex-direction: column; gap: 10px; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%); padding-bottom: 20px; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/actors/adversary/actions.less b/styles/less/sheets/actors/adversary/features.less similarity index 82% rename from styles/less/sheets/actors/adversary/actions.less rename to styles/less/sheets/actors/adversary/features.less index af870d9b..447d050e 100644 --- a/styles/less/sheets/actors/adversary/actions.less +++ b/styles/less/sheets/actors/adversary/features.less @@ -1,5 +1,6 @@ @import '../../../utils/colors.less'; @import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.adversary { .tab.features { @@ -8,8 +9,8 @@ flex-direction: column; gap: 10px; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%); padding-bottom: 20px; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/actors/adversary/header.less b/styles/less/sheets/actors/adversary/header.less index 8bd3fcee..1e5e4fa5 100644 --- a/styles/less/sheets/actors/adversary/header.less +++ b/styles/less/sheets/actors/adversary/header.less @@ -35,7 +35,7 @@ .tags { display: flex; gap: 10px; - padding-bottom: 16px; + padding-bottom: 8px; .tag { display: flex; @@ -67,11 +67,5 @@ gap: 12px; padding: 16px 0; } - - .adversary-navigation { - display: flex; - gap: 8px; - align-items: center; - } } } diff --git a/styles/less/sheets/actors/adversary/index.less b/styles/less/sheets/actors/adversary/index.less new file mode 100644 index 00000000..28ff9d22 --- /dev/null +++ b/styles/less/sheets/actors/adversary/index.less @@ -0,0 +1,7 @@ +@import './features.less'; +@import './header.less'; +@import './sheet.less'; +@import './sidebar.less'; +@import './effects.less'; +@import './notes.less'; + diff --git a/styles/less/sheets/actors/adversary/notes.less b/styles/less/sheets/actors/adversary/notes.less new file mode 100644 index 00000000..a95d070e --- /dev/null +++ b/styles/less/sheets/actors/adversary/notes.less @@ -0,0 +1,3 @@ +.application.sheet.daggerheart.actor.dh-style.adversary .tab.notes.active { + padding-bottom: 20px; +} diff --git a/styles/less/sheets/actors/adversary/sidebar.less b/styles/less/sheets/actors/adversary/sidebar.less index b1bb51db..5db9f5e9 100644 --- a/styles/less/sheets/actors/adversary/sidebar.less +++ b/styles/less/sheets/actors/adversary/sidebar.less @@ -286,9 +286,8 @@ overflow-y: hidden; padding-top: 10px; padding-bottom: 20px; - mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); - scrollbar-gutter: stable; + .with-scroll-shadows(); &:hover { overflow-y: auto; diff --git a/styles/less/sheets/actors/character/biography.less b/styles/less/sheets/actors/character/biography.less index f8d56735..8548a2fb 100644 --- a/styles/less/sheets/actors/character/biography.less +++ b/styles/less/sheets/actors/character/biography.less @@ -1,5 +1,6 @@ @import '../../../utils/colors.less'; @import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.character { .tab.biography { @@ -9,10 +10,10 @@ gap: 10px; height: 100%; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 98%, transparent 100%); padding-top: 8px; padding-bottom: 20px; height: 100%; + .with-scroll-shadows(); } .characteristics-section { diff --git a/styles/less/sheets/actors/character/effects.less b/styles/less/sheets/actors/character/effects.less index ae49fa2d..0ab1007d 100644 --- a/styles/less/sheets/actors/character/effects.less +++ b/styles/less/sheets/actors/character/effects.less @@ -1,5 +1,6 @@ @import '../../../utils/colors.less'; @import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.character { .tab.effects { @@ -8,8 +9,8 @@ flex-direction: column; gap: 10px; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%); padding-bottom: 20px; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/actors/character/features.less b/styles/less/sheets/actors/character/features.less index 017254a3..52b41826 100644 --- a/styles/less/sheets/actors/character/features.less +++ b/styles/less/sheets/actors/character/features.less @@ -1,5 +1,6 @@ @import '../../../utils/colors.less'; @import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.character { .tab.features { @@ -8,8 +9,8 @@ flex-direction: column; gap: 10px; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%); padding-bottom: 20px; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/actors/character/index.less b/styles/less/sheets/actors/character/index.less new file mode 100644 index 00000000..edefe0a1 --- /dev/null +++ b/styles/less/sheets/actors/character/index.less @@ -0,0 +1,8 @@ +@import './biography.less'; +@import './effects.less'; +@import './features.less'; +@import './header.less'; +@import './inventory.less'; +@import './loadout.less'; +@import './sheet.less'; +@import './sidebar.less'; diff --git a/styles/less/sheets/actors/character/inventory.less b/styles/less/sheets/actors/character/inventory.less index c8d2b584..fcfbbee9 100644 --- a/styles/less/sheets/actors/character/inventory.less +++ b/styles/less/sheets/actors/character/inventory.less @@ -1,5 +1,6 @@ @import '../../../utils/colors.less'; @import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.character { .tab.inventory { @@ -8,8 +9,9 @@ flex-direction: column; gap: 10px; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); - padding: 20px 0; + margin-top: 20px; + padding-bottom: 20px; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/actors/character/loadout.less b/styles/less/sheets/actors/character/loadout.less index a896b92e..fa3e0176 100644 --- a/styles/less/sheets/actors/character/loadout.less +++ b/styles/less/sheets/actors/character/loadout.less @@ -1,48 +1,10 @@ @import '../../../utils/colors.less'; @import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.character { .tab.loadout { .search-section { - display: flex; - align-items: center; - justify-content: space-between; - - .search-bar { - position: relative; - color: light-dark(@dark-blue-50, @beige-50); - width: 80%; - padding-top: 5px; - - input { - border-radius: 50px; - background: light-dark(@dark-blue-10, @golden-10); - border: none; - outline: 2px solid transparent; - transition: all 0.3s ease; - padding: 0 20px; - - &:hover { - outline: 2px solid light-dark(@dark, @golden); - } - - &::-webkit-search-cancel-button { - -webkit-appearance: none; - display: none; - } - } - - .icon { - align-content: center; - height: 32px; - position: absolute; - right: 20px; - font-size: var(--font-size-16); - z-index: 1; - color: light-dark(@dark-blue-50, @beige-50); - } - } - .btn-toggle-view { background: light-dark(@dark-blue-10, @dark-blue); border: 1px solid @color-border; @@ -90,8 +52,9 @@ gap: 10px; height: 100%; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 10%, black 98%, transparent 100%); - padding: 20px 0; + margin-top: 20px; + padding-bottom: 20px; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/actors/character/sidebar.less b/styles/less/sheets/actors/character/sidebar.less index 3c358d8f..c76ee9ff 100644 --- a/styles/less/sheets/actors/character/sidebar.less +++ b/styles/less/sheets/actors/character/sidebar.less @@ -549,8 +549,8 @@ overflow-y: hidden; padding-top: 10px; padding-bottom: 20px; - mask-image: linear-gradient(0deg, transparent 0%, black 5%); scrollbar-gutter: stable; + .with-scroll-shadows(); &:hover { overflow-y: auto; diff --git a/styles/less/sheets/actors/companion/effects.less b/styles/less/sheets/actors/companion/effects.less index 6d7fe061..c0cac669 100644 --- a/styles/less/sheets/actors/companion/effects.less +++ b/styles/less/sheets/actors/companion/effects.less @@ -7,8 +7,8 @@ flex-direction: column; gap: 10px; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%); padding-bottom: 20px; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/actors/companion/header.less b/styles/less/sheets/actors/companion/header.less index b4df96bf..aca789a6 100644 --- a/styles/less/sheets/actors/companion/header.less +++ b/styles/less/sheets/actors/companion/header.less @@ -148,10 +148,8 @@ } .companion-navigation { - display: flex; - gap: 8px; - align-items: baseline; width: 100%; + padding: 0 10px; } } } diff --git a/styles/less/sheets/actors/companion/index.less b/styles/less/sheets/actors/companion/index.less new file mode 100644 index 00000000..c4931814 --- /dev/null +++ b/styles/less/sheets/actors/companion/index.less @@ -0,0 +1,4 @@ +@import './details.less'; +@import './header.less'; +@import './sheet.less'; +@import './effects.less'; diff --git a/styles/less/sheets/actors/companion/sheet.less b/styles/less/sheets/actors/companion/sheet.less index f31679ba..8bf8a0b9 100644 --- a/styles/less/sheets/actors/companion/sheet.less +++ b/styles/less/sheets/actors/companion/sheet.less @@ -10,3 +10,16 @@ background: url('../assets/parchments/dh-parchment-light.png'); } }); + +.application.sheet.daggerheart.actor.dh-style.companion { + .window-content { + display: flex; + } + + .tab.active { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + } +} diff --git a/styles/less/sheets/actors/environment/actions.less b/styles/less/sheets/actors/environment/features.less similarity index 74% rename from styles/less/sheets/actors/environment/actions.less rename to styles/less/sheets/actors/environment/features.less index cc8a345a..84cf26f8 100644 --- a/styles/less/sheets/actors/environment/actions.less +++ b/styles/less/sheets/actors/environment/features.less @@ -1,5 +1,6 @@ @import '../../../utils/colors.less'; @import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.environment { .tab.features { @@ -8,8 +9,8 @@ flex-direction: column; gap: 10px; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%); - padding-bottom: 20px; + padding-bottom: 4px; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/actors/environment/header.less b/styles/less/sheets/actors/environment/header.less index 85471af4..da6954e0 100644 --- a/styles/less/sheets/actors/environment/header.less +++ b/styles/less/sheets/actors/environment/header.less @@ -138,10 +138,8 @@ } .environment-navigation { - display: flex; - gap: 20px; - align-items: baseline; padding: 0 20px; + .tab-navigation { margin-top: 0; } diff --git a/styles/less/sheets/actors/environment/index.less b/styles/less/sheets/actors/environment/index.less new file mode 100644 index 00000000..211c8e60 --- /dev/null +++ b/styles/less/sheets/actors/environment/index.less @@ -0,0 +1,4 @@ +@import './features.less'; +@import './header.less'; +@import './potentialAdversaries.less'; +@import './sheet.less'; diff --git a/styles/less/sheets/actors/environment/potentialAdversaries.less b/styles/less/sheets/actors/environment/potentialAdversaries.less index f3c5776a..f112c0d2 100644 --- a/styles/less/sheets/actors/environment/potentialAdversaries.less +++ b/styles/less/sheets/actors/environment/potentialAdversaries.less @@ -7,8 +7,8 @@ flex-direction: column; gap: 10px; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%); - padding-bottom: 20px; + padding-bottom: 4px; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/actors/index.less b/styles/less/sheets/actors/index.less new file mode 100644 index 00000000..959bc0f5 --- /dev/null +++ b/styles/less/sheets/actors/index.less @@ -0,0 +1,7 @@ +@import './actor-sheet-shared.less'; +@import './adversary/index.less'; +@import './character/index.less'; +@import './companion/index.less'; +@import './environment/index.less'; +@import './npc/index.less'; +@import './party/index.less'; \ No newline at end of file diff --git a/styles/less/sheets/actors/npc/features.less b/styles/less/sheets/actors/npc/features.less new file mode 100644 index 00000000..a579d9f8 --- /dev/null +++ b/styles/less/sheets/actors/npc/features.less @@ -0,0 +1,18 @@ +.application.sheet.daggerheart.actor.dh-style.npc { + .tab.features { + &.active { + overflow: hidden; + display: flex; + flex-direction: column; + } + + .feature-section { + display: flex; + flex-direction: column; + gap: 10px; + overflow-y: auto; + padding-bottom: 4px; + .with-scroll-shadows(); + } + } +} diff --git a/styles/less/sheets/actors/npc/header.less b/styles/less/sheets/actors/npc/header.less new file mode 100644 index 00000000..086a254c --- /dev/null +++ b/styles/less/sheets/actors/npc/header.less @@ -0,0 +1,83 @@ +.application.sheet.daggerheart.actor.dh-style.npc { + .npc-header-sheet { + width: 100%; + display: flex; + + .portrait { + cursor: pointer; + max-width: 275px; + + img { + height: 275px; + } + } + + .tags { + display: flex; + gap: 10px; + padding-bottom: 8px; + + .tag { + display: flex; + flex-direction: row; + gap: 4px; + justify-content: center; + align-items: center; + padding: 3px 5px; + font-size: var(--font-size-12); + font: @font-body; + + background: light-dark(@dark-15, @beige-15); + border: 1px solid light-dark(@dark, @beige); + border-radius: 3px; + } + + .label { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + font-size: var(--font-size-12); + } + } + + .info-section { + flex: 1; + padding: 0 15px; + padding-top: var(--header-height); + display: flex; + flex-direction: column; + + .name-row { + display: flex; + gap: 5px; + align-items: center; + justify-content: space-between; + padding: 8px 0; + + h1 { + display: flex; + flex: 1; + padding: 6px 0 0 0; + font-size: var(--font-size-32); + text-align: start; + border: 1px solid transparent; + outline: 2px solid transparent; + transition: all 0.3s ease; + word-break: break-word; + + &:hover { + outline: 2px solid light-dark(@dark, @golden); + } + } + } + + .npc-info { + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px 0; + } + } + } +} \ No newline at end of file diff --git a/styles/less/sheets/actors/npc/index.less b/styles/less/sheets/actors/npc/index.less new file mode 100644 index 00000000..2d7d54e3 --- /dev/null +++ b/styles/less/sheets/actors/npc/index.less @@ -0,0 +1,3 @@ +@import './sheet.less'; +@import './header.less'; +@import './features.less'; \ No newline at end of file diff --git a/styles/less/sheets/actors/npc/sheet.less b/styles/less/sheets/actors/npc/sheet.less new file mode 100644 index 00000000..8ba3b7a9 --- /dev/null +++ b/styles/less/sheets/actors/npc/sheet.less @@ -0,0 +1,10 @@ +.application.sheet.daggerheart.actor.dh-style.npc { + .window-content { + display: grid; + grid-template-rows: auto auto 1fr; + } + + .npc-navigation { + padding: 0 15px; + } +} \ No newline at end of file diff --git a/styles/less/sheets/actors/party/index.less b/styles/less/sheets/actors/party/index.less new file mode 100644 index 00000000..56f7a457 --- /dev/null +++ b/styles/less/sheets/actors/party/index.less @@ -0,0 +1,4 @@ +@import './header.less'; +@import './party-members.less'; +@import './sheet.less'; +@import './inventory.less'; diff --git a/styles/less/sheets/actors/party/inventory.less b/styles/less/sheets/actors/party/inventory.less index 8af37a79..444c6a57 100644 --- a/styles/less/sheets/actors/party/inventory.less +++ b/styles/less/sheets/actors/party/inventory.less @@ -1,5 +1,6 @@ @import '../../../utils/colors.less'; @import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; .application.sheet.daggerheart.actor.dh-style.party { .tab.inventory { @@ -8,8 +9,9 @@ flex-direction: column; gap: 10px; overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); - padding: 20px 0; + margin-top: 20px; + padding-bottom: 4px; + .with-scroll-shadows(); } } } diff --git a/styles/less/sheets/index.less b/styles/less/sheets/index.less index 7d595614..451ae03a 100644 --- a/styles/less/sheets/index.less +++ b/styles/less/sheets/index.less @@ -1,45 +1,5 @@ -@import './actions/actions.less'; - -@import './actors/actor-sheet-shared.less'; - -@import './actors/adversary/actions.less'; -@import './actors/adversary/header.less'; -@import './actors/adversary/sheet.less'; -@import './actors/adversary/sidebar.less'; -@import './actors/adversary/effects.less'; - -@import './actors/character/biography.less'; -@import './actors/character/effects.less'; -@import './actors/character/features.less'; -@import './actors/character/header.less'; -@import './actors/character/inventory.less'; -@import './actors/character/loadout.less'; -@import './actors/character/sheet.less'; -@import './actors/character/sidebar.less'; - -@import './actors/companion/details.less'; -@import './actors/companion/header.less'; -@import './actors/companion/sheet.less'; -@import './actors/companion/effects.less'; - -@import './actors/environment/actions.less'; -@import './actors/environment/header.less'; -@import './actors/environment/potentialAdversaries.less'; -@import './actors/environment/sheet.less'; - -@import './actors/party/header.less'; -@import './actors/party/party-members.less'; -@import './actors/party/sheet.less'; -@import './actors/party/inventory.less'; - -@import './items/beastform.less'; -@import './items/class.less'; -@import './items/domain-card.less'; -@import './items/feature.less'; -@import './items/heritage.less'; -@import './items/item-sheet-shared.less'; - -@import './rollTables/sheet.less'; -@import './actions/actions.less'; - -@import './activeEffects/activeEffects.less'; +@import './activeEffects/index.less'; +@import './actions/index.less'; +@import './actors/index.less'; +@import './items/index.less'; +@import './rollTables/index.less'; \ No newline at end of file diff --git a/styles/less/sheets/items/index.less b/styles/less/sheets/items/index.less new file mode 100644 index 00000000..7c40a2e3 --- /dev/null +++ b/styles/less/sheets/items/index.less @@ -0,0 +1,6 @@ +@import './beastform.less'; +@import './class.less'; +@import './domain-card.less'; +@import './feature.less'; +@import './heritage.less'; +@import './item-sheet-shared.less'; \ No newline at end of file diff --git a/styles/less/sheets/rollTables/index.less b/styles/less/sheets/rollTables/index.less new file mode 100644 index 00000000..1c574e81 --- /dev/null +++ b/styles/less/sheets/rollTables/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/ui/chat/index.less b/styles/less/ui/chat/index.less new file mode 100644 index 00000000..9cadbd04 --- /dev/null +++ b/styles/less/ui/chat/index.less @@ -0,0 +1,10 @@ +@import './sheet.less'; +@import './ability-use.less'; +@import './action.less'; +@import './chat.less'; +@import './damage-summary.less'; +@import './deathmoves.less'; +@import './downtime.less'; +@import './effect-summary.less'; +@import './group-roll.less'; +@import './refresh-message.less'; \ No newline at end of file diff --git a/styles/less/ui/combat-sidebar/index.less b/styles/less/ui/combat-sidebar/index.less new file mode 100644 index 00000000..786815ef --- /dev/null +++ b/styles/less/ui/combat-sidebar/index.less @@ -0,0 +1,5 @@ +@import './combat-sidebar.less'; +@import './combatant-controls.less'; +@import './encounter-controls.less'; +@import './spotlight-control.less'; +@import './token-actions.less'; \ No newline at end of file diff --git a/styles/less/ui/countdown/countdown.less b/styles/less/ui/countdown/countdown.less index 66a6c88a..63e539ba 100644 --- a/styles/less/ui/countdown/countdown.less +++ b/styles/less/ui/countdown/countdown.less @@ -18,7 +18,7 @@ border: 0; box-shadow: none; color: @color-text-primary; - width: 300px; + width: 18.75rem; pointer-events: all; align-self: flex-end; transition: 0.3s right ease-in-out; @@ -36,7 +36,7 @@ transition: opacity var(--ui-fade-duration); } - :not(.performance-low, .noblur) { + &:not(.performance-low, .noblur) { backdrop-filter: blur(5px); } @@ -49,8 +49,7 @@ } &.icon-only { - width: 180px; - min-width: 180px; + width: 12rem; } .countdowns-header, @@ -108,8 +107,8 @@ gap: 16px; img { - width: 44px; - height: 44px; + width: 2.75rem; + height: 2.75rem; border-radius: 6px; } @@ -127,7 +126,7 @@ .countdown-tool-controls { display: flex; align-items: center; - gap: 16px; + gap: var(--spacer-12); } .progress-tag { diff --git a/styles/less/ui/countdown/index.less b/styles/less/ui/countdown/index.less new file mode 100644 index 00000000..45ecda26 --- /dev/null +++ b/styles/less/ui/countdown/index.less @@ -0,0 +1,3 @@ +@import './sheet.less'; +@import './countdown-edit.less'; +@import './countdown.less'; \ No newline at end of file diff --git a/styles/less/ui/effects-display/index.less b/styles/less/ui/effects-display/index.less new file mode 100644 index 00000000..1c574e81 --- /dev/null +++ b/styles/less/ui/effects-display/index.less @@ -0,0 +1 @@ +@import './sheet.less'; \ No newline at end of file diff --git a/styles/less/ui/index.less b/styles/less/ui/index.less index 31ea8955..53a71b9b 100644 --- a/styles/less/ui/index.less +++ b/styles/less/ui/index.less @@ -1,40 +1,11 @@ -@import './chat/ability-use.less'; -@import './chat/action.less'; -@import './chat/chat.less'; -@import './chat/damage-summary.less'; -@import './chat/downtime.less'; -@import './chat/effect-summary.less'; -@import './chat/group-roll.less'; -@import './chat/refresh-message.less'; -@import './chat/deathmoves.less'; -@import './chat/sheet.less'; - -@import './combat-sidebar/combat-sidebar.less'; -@import './combat-sidebar/combatant-controls.less'; -@import './combat-sidebar/encounter-controls.less'; -@import './combat-sidebar/spotlight-control.less'; -@import './combat-sidebar/token-actions.less'; -@import './item-browser/item-browser.less'; - -@import './countdown/countdown.less'; -@import './countdown/countdown-edit.less'; -@import './countdown/sheet.less'; - -@import './ownership-selection/ownership-selection.less'; - -@import './resources/resources.less'; - -@import './settings/settings.less'; -@import './settings/homebrew-settings/domains.less'; -@import './settings/homebrew-settings/types.less'; -@import './settings/homebrew-settings/resources.less'; -@import './settings/appearance-settings/diceSoNice.less'; - -@import './sidebar/tabs.less'; -@import './sidebar/daggerheartMenu.less'; - -@import './scene-config/scene-config.less'; - -@import './effects-display/sheet.less'; - -@import './scene-navigation/scene-navigation.less'; +@import './chat/index.less'; +@import './combat-sidebar/index.less'; +@import './countdown/index.less'; +@import './effects-display/index.less'; +@import './item-browser/index.less'; +@import './ownership-selection/index.less'; +@import './resources/index.less'; +@import './scene-config/index.less'; +@import './scene-navigation/index.less'; +@import './settings/index.less'; +@import './sidebar/index.less'; \ No newline at end of file diff --git a/styles/less/ui/item-browser/index.less b/styles/less/ui/item-browser/index.less new file mode 100644 index 00000000..842f716b --- /dev/null +++ b/styles/less/ui/item-browser/index.less @@ -0,0 +1 @@ +@import './item-browser.less'; \ No newline at end of file diff --git a/styles/less/ui/item-browser/item-browser.less b/styles/less/ui/item-browser/item-browser.less index 1387f444..aac63d7a 100644 --- a/styles/less/ui/item-browser/item-browser.less +++ b/styles/less/ui/item-browser/item-browser.less @@ -1,5 +1,6 @@ @import '../../utils/colors.less'; @import '../../utils/fonts.less'; +@import '../../utils/mixin.less'; .application.daggerheart.dh-style.compendium-browser { border: initial; @@ -242,6 +243,7 @@ .compendium-sidebar > .folder-list { overflow-y: auto; scrollbar-gutter: stable; + .with-scroll-shadows(); } .item-list-header, diff --git a/styles/less/ui/ownership-selection/index.less b/styles/less/ui/ownership-selection/index.less new file mode 100644 index 00000000..9646670a --- /dev/null +++ b/styles/less/ui/ownership-selection/index.less @@ -0,0 +1 @@ +@import './ownership-selection.less'; \ No newline at end of file diff --git a/styles/less/ui/resources/index.less b/styles/less/ui/resources/index.less new file mode 100644 index 00000000..a7d08785 --- /dev/null +++ b/styles/less/ui/resources/index.less @@ -0,0 +1 @@ +@import './resources.less'; \ No newline at end of file diff --git a/styles/less/ui/scene-config/index.less b/styles/less/ui/scene-config/index.less new file mode 100644 index 00000000..4e3af363 --- /dev/null +++ b/styles/less/ui/scene-config/index.less @@ -0,0 +1 @@ +@import './scene-config.less'; \ No newline at end of file diff --git a/styles/less/ui/scene-navigation/index.less b/styles/less/ui/scene-navigation/index.less new file mode 100644 index 00000000..c0765ae7 --- /dev/null +++ b/styles/less/ui/scene-navigation/index.less @@ -0,0 +1 @@ +@import './scene-navigation.less'; \ No newline at end of file diff --git a/styles/less/ui/settings/appearance-settings/index.less b/styles/less/ui/settings/appearance-settings/index.less new file mode 100644 index 00000000..8b1c109a --- /dev/null +++ b/styles/less/ui/settings/appearance-settings/index.less @@ -0,0 +1 @@ +@import './diceSoNice.less'; \ No newline at end of file diff --git a/styles/less/ui/settings/homebrew-settings/index.less b/styles/less/ui/settings/homebrew-settings/index.less new file mode 100644 index 00000000..f0a8bfc1 --- /dev/null +++ b/styles/less/ui/settings/homebrew-settings/index.less @@ -0,0 +1,3 @@ +@import './domains.less'; +@import './resources.less'; +@import './types.less'; \ No newline at end of file diff --git a/styles/less/ui/settings/index.less b/styles/less/ui/settings/index.less new file mode 100644 index 00000000..4e1aa798 --- /dev/null +++ b/styles/less/ui/settings/index.less @@ -0,0 +1,3 @@ +@import './settings.less'; +@import './appearance-settings/index.less'; +@import './homebrew-settings/index.less'; \ No newline at end of file diff --git a/styles/less/ui/sidebar/index.less b/styles/less/ui/sidebar/index.less new file mode 100644 index 00000000..b4961b41 --- /dev/null +++ b/styles/less/ui/sidebar/index.less @@ -0,0 +1,2 @@ +@import './daggerheartMenu.less'; +@import './tabs.less'; \ No newline at end of file diff --git a/styles/less/utils/index.less b/styles/less/utils/index.less new file mode 100644 index 00000000..37b096d3 --- /dev/null +++ b/styles/less/utils/index.less @@ -0,0 +1,4 @@ +@import './colors.less'; +@import './fonts.less'; +@import './mixin.less'; +@import './spacing.less'; \ No newline at end of file diff --git a/styles/less/utils/mixin.less b/styles/less/utils/mixin.less index 237a5acb..18b1f9a6 100644 --- a/styles/less/utils/mixin.less +++ b/styles/less/utils/mixin.less @@ -160,3 +160,47 @@ @destination @length ); } + +// Scroll shadows, but only if the browser supports. At the time of writing, this doesn't work on firefox +@supports ((animation-timeline: scroll()) and (animation-range: 0% 100%)) { + @property --fade-start { + syntax: ""; + inherits: false; + initial-value: 0; + } + + @property --fade-end { + syntax: ""; + inherits: false; + initial-value: 0; + } + + @keyframes scrollfade { + 0% { + --fade-start: 0; + } + 10%, 100% { + --fade-start: 12px; + } + 0%, 90% { + --fade-end: 12px; + } + 100% { + --fade-end: 0; + } + } +} + +.with-scroll-shadows() { + animation: scrollfade; + animation-timeline: --scrollfade; + animation-range: entry 0% exit 100%; + scroll-timeline: --scrollfade y; + mask-image: linear-gradient( + 0deg, + transparent 0%, + black var(--fade-end), + black calc(100% - var(--fade-start)), + transparent 100% + ); +} diff --git a/styles/less/ux/autocomplete/index.less b/styles/less/ux/autocomplete/index.less new file mode 100644 index 00000000..91007146 --- /dev/null +++ b/styles/less/ux/autocomplete/index.less @@ -0,0 +1 @@ +@import './autocomplete.less'; \ No newline at end of file diff --git a/styles/less/ux/index.less b/styles/less/ux/index.less index a73f2d1c..b6c9a2e7 100644 --- a/styles/less/ux/index.less +++ b/styles/less/ux/index.less @@ -1,10 +1,2 @@ -@import './tooltip/sheet.less'; -@import './tooltip/tooltip.less'; -@import './tooltip/armorManagement.less'; -@import './tooltip/battlepoints.less'; -@import './tooltip/bordered-tooltip.less'; -@import './tooltip/domain-cards.less'; - -@import './autocomplete/autocomplete.less'; - -@import './tooltip/resource-management.less'; +@import './autocomplete/index.less'; +@import './tooltip/index.less'; \ No newline at end of file diff --git a/styles/less/ux/tooltip/index.less b/styles/less/ux/tooltip/index.less new file mode 100644 index 00000000..eeec9354 --- /dev/null +++ b/styles/less/ux/tooltip/index.less @@ -0,0 +1,7 @@ +@import './sheet.less'; +@import './armorManagement.less'; +@import './battlepoints.less'; +@import './bordered-tooltip.less'; +@import './domain-cards.less'; +@import './resource-management.less'; +@import './tooltip.less'; \ No newline at end of file diff --git a/system.json b/system.json index 2acd7570..ed14a17b 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "id": "daggerheart", "title": "Daggerheart", "description": "An unofficial implementation of the Daggerheart system", - "version": "2.2.7", + "version": "2.3.2", "compatibility": { "minimum": "14.361", "verified": "14.363", @@ -10,7 +10,7 @@ }, "url": "https://github.com/Foundryborne/daggerheart", "manifest": "https://raw.githubusercontent.com/Foundryborne/daggerheart/v14/system.json", - "download": "https://github.com/Foundryborne/daggerheart/releases/download/2.2.7/system.zip", + "download": "https://github.com/Foundryborne/daggerheart/releases/download/2.3.2/system.zip", "authors": [ { "name": "WBHarry" @@ -244,11 +244,14 @@ "adversary": { "htmlFields": ["notes", "description"] }, + "npc": { + "htmlFields": ["notes"] + }, "environment": { "htmlFields": ["notes", "description"] }, "party": { - "htmlFields": ["notes"] + "htmlFields": ["notes", "description"] } }, "Item": { diff --git a/templates/dialogs/rerollDialog/damage/main.hbs b/templates/dialogs/rerollDialog/damage/main.hbs deleted file mode 100644 index 5b994bf6..00000000 --- a/templates/dialogs/rerollDialog/damage/main.hbs +++ /dev/null @@ -1,35 +0,0 @@ -
- {{#each damage}} -

{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' @key '.name')}}

- {{#each this}} -
- {{#each this}} -
- - - - {{this.selectedResults}}/{{this.maxSelected}} Selected - - -
- {{#each this.results}} -
- {{this.result}} - {{#if this.active}} - - - - - {{/if}} -
- {{/each}} -
-
- {{/each}} -
- {{/each}} - {{/each}} -
\ No newline at end of file diff --git a/templates/dialogs/rerollDialog/footer.hbs b/templates/dialogs/rerollDialog/footer.hbs deleted file mode 100644 index 5d4ae2b2..00000000 --- a/templates/dialogs/rerollDialog/footer.hbs +++ /dev/null @@ -1,4 +0,0 @@ -
- - -
\ No newline at end of file diff --git a/templates/dialogs/rerollDialog/main.hbs b/templates/dialogs/rerollDialog/main.hbs deleted file mode 100644 index 6f10ce33..00000000 --- a/templates/dialogs/rerollDialog/main.hbs +++ /dev/null @@ -1,35 +0,0 @@ -
- {{#each damage}} -

{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' @key '.name')}}

- {{#each this}} -
- {{#each this}} -
- - - - {{this.selectedResults}}/{{this.results.length}} Selected - - -
- {{#each this.results}} -
- {{this.result}} - {{#if this.active}} - - - - - {{/if}} -
- {{/each}} -
-
- {{/each}} -
- {{/each}} - {{/each}} -
\ No newline at end of file diff --git a/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs b/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs index 49fc8f4f..2a366269 100644 --- a/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs +++ b/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs @@ -16,7 +16,7 @@ {{/unless}} {{/each}} {{#if part.modifierTotal}} - {{#if (gte part.modifierTotal 0)}}+{{else}}-{{/if}} + {{#if part.dice.length}}{{#if (gte part.modifierTotal 0)}}+{{else}}-{{/if}}{{/if}} {{positive part.modifierTotal}} {{/if}} diff --git a/templates/sheets-settings/adversary-settings/details.hbs b/templates/sheets-settings/adversary-settings/details.hbs index dc2fd386..3160fbb9 100644 --- a/templates/sheets-settings/adversary-settings/details.hbs +++ b/templates/sheets-settings/adversary-settings/details.hbs @@ -18,6 +18,12 @@ {{formField systemFields.motivesAndTactics value=document._source.system.motivesAndTactics label=(localize "DAGGERHEART.ACTORS.Adversary.FIELDS.motivesAndTactics.label")}} +
+ {{localize "DAGGERHEART.GENERAL.DamageThresholds.title"}} + {{formGroup systemFields.damageThresholds.fields.major value=document._source.system.damageThresholds.major label=(localize "DAGGERHEART.GENERAL.DamageThresholds.majorThreshold")}} + {{formGroup systemFields.damageThresholds.fields.severe value=document._source.system.damageThresholds.severe label=(localize "DAGGERHEART.GENERAL.DamageThresholds.severeThreshold")}} +
+
{{localize "DAGGERHEART.GENERAL.Resource.plural"}} @@ -26,10 +32,4 @@ {{/each}}
- -
- {{localize "DAGGERHEART.GENERAL.DamageThresholds.title"}} - {{formGroup systemFields.damageThresholds.fields.major value=document._source.system.damageThresholds.major label=(localize "DAGGERHEART.GENERAL.DamageThresholds.majorThreshold")}} - {{formGroup systemFields.damageThresholds.fields.severe value=document._source.system.damageThresholds.severe label=(localize "DAGGERHEART.GENERAL.DamageThresholds.severeThreshold")}} -
diff --git a/templates/sheets-settings/npc-settings/details.hbs b/templates/sheets-settings/npc-settings/details.hbs new file mode 100644 index 00000000..0e18b488 --- /dev/null +++ b/templates/sheets-settings/npc-settings/details.hbs @@ -0,0 +1,13 @@ +
+
+ {{localize "DAGGERHEART.GENERAL.description"}} + {{formInput systemFields.description value=document._source.system.description}} +
+ + {{formGroup systemFields.motives value=document._source.system.motives}} + {{formGroup systemFields.difficulty value=document._source.system.difficulty localize=true}} +
diff --git a/templates/sheets-settings/npc-settings/features.hbs b/templates/sheets-settings/npc-settings/features.hbs new file mode 100644 index 00000000..2f2f5f47 --- /dev/null +++ b/templates/sheets-settings/npc-settings/features.hbs @@ -0,0 +1,29 @@ +
+ +
+ {{localize tabs.features.label}} +
    + {{#each @root.features as |feature|}} +
  • + +
    + {{feature.name}} +
    +
    + + +
    +
  • + {{/each}} +
+
+ {{localize "DAGGERHEART.GENERAL.dropFeaturesHere"}} +
+
+
\ No newline at end of file diff --git a/templates/sheets-settings/npc-settings/header.hbs b/templates/sheets-settings/npc-settings/header.hbs new file mode 100644 index 00000000..c9cb60fe --- /dev/null +++ b/templates/sheets-settings/npc-settings/header.hbs @@ -0,0 +1,3 @@ +
+

{{document.name}}

+
\ No newline at end of file diff --git a/templates/sheets/actors/adversary/header.hbs b/templates/sheets/actors/adversary/header.hbs index fba96980..5adc235a 100644 --- a/templates/sheets/actors/adversary/header.hbs +++ b/templates/sheets/actors/adversary/header.hbs @@ -44,10 +44,9 @@ -
- {{> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}} + {{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}} -
+ {{/ 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}} \ No newline at end of file diff --git a/templates/sheets/actors/companion/header.hbs b/templates/sheets/actors/companion/header.hbs index d10c0640..9c324709 100644 --- a/templates/sheets/actors/companion/header.hbs +++ b/templates/sheets/actors/companion/header.hbs @@ -50,9 +50,10 @@
- {{> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}} - + {{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}} + + {{/ 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
\ No newline at end of file diff --git a/templates/sheets/actors/environment/header.hbs b/templates/sheets/actors/environment/header.hbs index 2c6bbb5a..1b4073c7 100644 --- a/templates/sheets/actors/environment/header.hbs +++ b/templates/sheets/actors/environment/header.hbs @@ -44,9 +44,10 @@
- {{> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}} - + {{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}} + + {{/ 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}}
\ No newline at end of file diff --git a/templates/sheets/actors/npc/features.hbs b/templates/sheets/actors/npc/features.hbs new file mode 100644 index 00000000..3b495e74 --- /dev/null +++ b/templates/sheets/actors/npc/features.hbs @@ -0,0 +1,14 @@ +
+
+ {{> 'daggerheart.inventory-items' + title=tabs.features.label + type='feature' + collection=@root.features + hideContextMenu=true + hideModifyControls=true + canCreate=@root.editable + showActions=@root.editable + }} +
+
\ No newline at end of file diff --git a/templates/sheets/actors/npc/header.hbs b/templates/sheets/actors/npc/header.hbs new file mode 100644 index 00000000..fce300da --- /dev/null +++ b/templates/sheets/actors/npc/header.hbs @@ -0,0 +1,42 @@ +
+
+ {{source.name}} +
+
+ + +
+

{{source.name}}

+
+ + {{#if source.system.difficulty}} +
+
+ {{localize "DAGGERHEART.GENERAL.difficulty"}} + {{source.system.difficulty}} +
+
+ {{/if}} + + + +
+ + {{{description}}} + + {{#if source.system.motives}} +
+ {{localize 'DAGGERHEART.ACTORS.NPC.FIELDS.motives.label'}}: + {{source.system.motives}} +
+ {{/if}} +
+
+
\ No newline at end of file diff --git a/templates/sheets/actors/npc/navigation.hbs b/templates/sheets/actors/npc/navigation.hbs new file mode 100644 index 00000000..ae684f0d --- /dev/null +++ b/templates/sheets/actors/npc/navigation.hbs @@ -0,0 +1,7 @@ +
+ {{#> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}} + + {{/'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}} +
\ No newline at end of file diff --git a/templates/sheets/actors/npc/notes.hbs b/templates/sheets/actors/npc/notes.hbs new file mode 100644 index 00000000..bc9ac3cf --- /dev/null +++ b/templates/sheets/actors/npc/notes.hbs @@ -0,0 +1,11 @@ +
+ {{formInput notes.field value=notes.value enriched=notes.enriched toggled=true}} + + {{#if (and showAttribution document.system.attribution.artist)}} + + {{/if}} +
\ No newline at end of file diff --git a/templates/sheets/actors/party/party-members.hbs b/templates/sheets/actors/party/party-members.hbs index c7870bc6..55cacb79 100644 --- a/templates/sheets/actors/party/party-members.hbs +++ b/templates/sheets/actors/party/party-members.hbs @@ -42,7 +42,7 @@ {{#if member.difficulty includeZero=true}}
{{member.difficulty}}
{{/if}} - {{#unless (eq member.type 'companion')}} + {{#unless (or (eq member.type 'companion') (eq member.type 'npc'))}}

{{localize "DAGGERHEART.ACTORS.Party.Thresholds.minor"}}

{{member.damageThresholds.major}}

@@ -58,7 +58,7 @@
- {{#unless (or (eq member.type 'companion') (eq member.type 'adversary')) }} + {{#unless (or (eq member.type 'companion') (eq member.type 'adversary') (eq member.type 'npc')) }}

{{localize "DAGGERHEART.GENERAL.hope"}}

{{#times member.resources.hope.max}} @@ -79,7 +79,7 @@
- {{#unless (eq member.type 'companion') }} + {{#unless (or (eq member.type 'companion') (eq member.type 'npc')) }}
@@ -101,25 +101,27 @@
{{/unless}} -
-
- - - - - {{member.resources.stress.value}} - / - {{member.resources.stress.max}} - -
-
- {{#times member.resources.stress.max}} - + {{#unless (eq member.type 'npc')}} +
+
+ + - {{/times}} + + {{member.resources.stress.value}} + / + {{member.resources.stress.max}} + +
+
+ {{#times member.resources.stress.max}} + + + {{/times}} +
-
+ {{/unless}} {{#if member.armorScore.max}}
diff --git a/templates/sheets/global/tabs/tab-navigation.hbs b/templates/sheets/global/tabs/tab-navigation.hbs index f9a31d3e..8af1f140 100755 --- a/templates/sheets/global/tabs/tab-navigation.hbs +++ b/templates/sheets/global/tabs/tab-navigation.hbs @@ -4,7 +4,7 @@
- {{#if roll.difficulty}} - - {{!-- {{#if canViewSecret}} --}} - difficulty {{roll.difficulty}} - {{!-- {{else}} - {{localize (ifThen roll.success "DAGGERHEART.GENERAL.success" "DAGGERHEART.GENERAL.failure")}} - {{/if}} --}} + {{#if roll.options.roll.difficulty}} + + {{localize "DAGGERHEART.GENERAL.difficulty"}} {{roll.options.roll.difficulty}} {{/if}}
diff --git a/templates/ui/combatTracker/combatTrackerHeader.hbs b/templates/ui/combatTracker/combatTrackerHeader.hbs index 803286ab..9f5a7561 100644 --- a/templates/ui/combatTracker/combatTrackerHeader.hbs +++ b/templates/ui/combatTracker/combatTrackerHeader.hbs @@ -50,6 +50,11 @@ {{/if}} + {{!-- Encounter Name --}} + {{#if combat.name}} +

{{ combat.name }}

+ {{/if}} +
{{!-- Combat Status --}} diff --git a/templates/ui/countdowns.hbs b/templates/ui/countdowns.hbs index 95067826..faaffdc5 100644 --- a/templates/ui/countdowns.hbs +++ b/templates/ui/countdowns.hbs @@ -11,18 +11,20 @@
{{#each countdowns as | countdown id |}} -
+
- {{#unless ../iconOnly}}{{/unless}} + {{#unless ../iconOnly}} +
{{countdown.name}}
+ {{/unless}}
- {{#if countdown.editable}}{{/if}} + {{#if countdown.editable}}{{/if}}
{{countdown.progress.current}}/{{countdown.progress.start}}
- {{#if countdown.editable}}{{/if}} + {{#if countdown.editable}}{{/if}}
{{#if (not ../iconOnly)}} @@ -31,7 +33,7 @@ {{/if}} {{#unless (eq countdown.progress.looping "noLooping")}} - + {{#if (eq countdown.progress.looping "increasing")}} diff --git a/templates/ui/sidebar/actor-document-partial.hbs b/templates/ui/sidebar/actor-document-partial.hbs index 2a9f47fa..1bd3ff9a 100644 --- a/templates/ui/sidebar/actor-document-partial.hbs +++ b/templates/ui/sidebar/actor-document-partial.hbs @@ -6,6 +6,8 @@ {{name}} {{#if (or (eq type "adversary") (eq type "environment"))}} {{localize "DAGGERHEART.UI.Sidebar.actorDirectory.tier" tier=system.tier type=(@root.getTypeLabel this)}} + {{else if (eq type "npc")}} + {{localize "TYPES.Actor.npc"}} {{else if (eq type "character")}} {{localize "DAGGERHEART.UI.Sidebar.actorDirectory.character" level=system.levelData.level.current}} {{else if (eq type "companion")}} diff --git a/tools/analyze-damage.mjs b/tools/analyze-damage.mjs index 7b3fb9e5..31c254ce 100644 --- a/tools/analyze-damage.mjs +++ b/tools/analyze-damage.mjs @@ -82,7 +82,7 @@ function getMean(numbers) { } function getMedianAverageDeviation(numbers, { median }) { - const residuals = allDamage.map(d => Math.abs(d - median)); + const residuals = numbers.map(d => Math.abs(d - median)); return getMedian(residuals); } @@ -98,8 +98,8 @@ function parseDamage(damage) { p.value.custom.enabled ? p.value.custom.formula : [p.value.flatMultiplier ? `${p.value.flatMultiplier}${p.value.dice}` : 0, p.value.bonus ?? 0] - .filter(p => !!p) - .join('+') + .filter(p => !!p) + .join('+') ) .join('+'); return getExpectedDamage(formula); diff --git a/tools/create-symlink.mjs b/tools/create-symlink.mjs index fd828c73..4e14d5df 100644 --- a/tools/create-symlink.mjs +++ b/tools/create-symlink.mjs @@ -2,6 +2,8 @@ import fs from 'fs'; import path from 'path'; import readline from 'readline'; +console.log('Creates a foundry symlink in the base folder for type purposes\n'); + const askQuestion = question => { const rl = readline.createInterface({ input: process.stdin, diff --git a/tools/eslint.config.mjs b/tools/eslint.config.mjs new file mode 100644 index 00000000..36a5174a --- /dev/null +++ b/tools/eslint.config.mjs @@ -0,0 +1,20 @@ +import globals from 'globals'; +import { defineConfig, globalIgnores } from 'eslint/config'; +import { stylisticRules } from '../eslint.config.mjs'; +import stylistic from '@stylistic/eslint-plugin'; + +export default defineConfig([ + globalIgnores(['foundry/**/*']), + { + files: ['**/*.{js,mjs,cjs}'], + plugins: { + '@stylistic': stylistic + }, + languageOptions: { globals: globals.node }, + rules: { + 'no-undef': 'error', + 'no-unused-vars': 0, + ...stylisticRules + } + } +]);