diff --git a/lang/en.json b/lang/en.json index d2386744..32717fcb 100755 --- a/lang/en.json +++ b/lang/en.json @@ -1801,6 +1801,7 @@ "plural": "Costs" }, "Damage": { + "massive": "Massive", "severe": "Severe", "major": "Major", "minor": "Minor", @@ -2440,9 +2441,12 @@ }, "currency": { "title": "Currency Overrides", + "changeIcon": "Change Currency Icon", "currencyName": "Currency Name", "coinName": "Coin Name", "handfulName": "Handful Name", + "iconName": "Icon Name", + "iconNameHint": "Icons are from fontawesome", "bagName": "Bag Name", "chestName": "Chest Name" }, @@ -2504,6 +2508,11 @@ "hint": "Apply variant rules from the Daggerheart system", "name": "Variant Rules", "actionTokens": "Action Tokens" + }, + "SpotlightRequestQueue": { + "name": "Spotlight Request Queue", + "label": "Spotlight Request Queue", + "hint": "Adds more structure to spotlight requests by ordering them from oldest to newest" } }, "Resources": { @@ -2517,6 +2526,10 @@ "actionTokens": { "enabled": { "label": "Enabled" }, "tokens": { "label": "Tokens" } + }, + "massiveDamage":{ + "title":"Massive Damage", + "enabled": { "label": "Enabled" } } } }, diff --git a/module/applications/hud/tokenHUD.mjs b/module/applications/hud/tokenHUD.mjs index f90c26be..b1136995 100644 --- a/module/applications/hud/tokenHUD.mjs +++ b/module/applications/hud/tokenHUD.mjs @@ -197,6 +197,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { for (const effect of activeEffects) { for (const statusId of effect.statuses) { const status = choices[statusId]; + if (!status) continue; + status.instances = 1 + (status.instances ?? 0); status.locked = status.locked || effect.condition || status.instances > 1; if (!status) continue; diff --git a/module/applications/settings/homebrewSettings.mjs b/module/applications/settings/homebrewSettings.mjs index 8e566106..6d36a2b3 100644 --- a/module/applications/settings/homebrewSettings.mjs +++ b/module/applications/settings/homebrewSettings.mjs @@ -32,6 +32,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli icon: 'fa-solid fa-gears' }, actions: { + editCurrencyIcon: this.changeCurrencyIcon, addItem: this.addItem, editItem: this.editItem, removeItem: this.removeItem, @@ -115,6 +116,45 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli this.render(); } + static async changeCurrencyIcon(_, target) { + const type = target.dataset.currency; + const currentIcon = this.settings.currency[type].icon; + const icon = await foundry.applications.api.DialogV2.input({ + classes: ['daggerheart', 'dh-style', 'change-currency-icon'], + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/settings/homebrew-settings/change-currency-icon.hbs', + { currentIcon } + ), + window: { + title: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.currency.changeIcon'), + icon: 'fa-solid fa-coins' + }, + render: (_, dialog) => { + const icon = dialog.element.querySelector('.displayed-icon i'); + const input = dialog.element.querySelector('input'); + const reset = dialog.element.querySelector('button[data-action=reset]'); + input.addEventListener('input', () => { + icon.classList.value = input.value; + }); + reset.addEventListener('click', () => { + const currencyField = DhHomebrew.schema.fields.currency.fields[type]; + const initial = currencyField.fields.icon.getInitialValue(); + input.value = icon.classList.value = initial; + }); + }, + ok: { + callback: (_, button) => button.form.elements.icon.value + } + }); + + if (icon !== null) { + await this.settings.updateSource({ + [`currency.${type}.icon`]: icon + }); + this.render(); + } + } + static async addItem(_, target) { const { type } = target.dataset; if (['shortRest', 'longRest'].includes(type)) { diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 016cff13..953a0cf6 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -685,8 +685,6 @@ export default class CharacterSheet extends DHBaseActorSheet { ability: abilityLabel }) }); - - if (result) game.system.api.fields.ActionFields.CostField.execute.call(this, result); } //TODO: redo toggleEquipItem method diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 449d6723..b11fc779 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -178,6 +178,60 @@ export default function DHApplicationMixin(Base) { _attachPartListeners(partId, htmlElement, options) { super._attachPartListeners(partId, htmlElement, options); this._dragDrop.forEach(d => d.bind(htmlElement)); + + for (const deltaInput of htmlElement.querySelectorAll('input[data-allow-delta]')) { + deltaInput.dataset.numValue = deltaInput.value; + deltaInput.inputMode = 'numeric'; + deltaInput.pattern = '^[+=\\-]?\d*'; + + const handleUpdate = (delta = 0) => { + const min = Number(deltaInput.min) || 0; + const max = Number(deltaInput.max) || Infinity; + const current = Number(deltaInput.dataset.numValue); + const rawNumber = Number(deltaInput.value); + if (Number.isNaN(rawNumber)) { + deltaInput.value = delta ? Math.clamp(current + delta, min, max) : current; + return; + } + + const newValue = + deltaInput.value.startsWith('+') || deltaInput.value.startsWith('-') + ? Math.clamp(current + rawNumber + delta, min, max) + : Math.clamp(rawNumber + delta, min, max); + deltaInput.value = deltaInput.dataset.numValue = newValue; + }; + + // Force valid characters while inputting + deltaInput.addEventListener('input', () => { + deltaInput.value = /[+=\-]?\d*/.exec(deltaInput.value)?.at(0) ?? deltaInput.value; + }); + + // Recreate Keyup/Keydown support + deltaInput.addEventListener('keydown', event => { + const step = event.key === 'ArrowUp' ? 1 : event.key === 'ArrowDown' ? -1 : 0; + if (step !== 0) { + handleUpdate(step); + deltaInput.dispatchEvent(new Event("change", { bubbles: true })); + } + }); + + // Mousewheel while focused support + deltaInput.addEventListener( + 'wheel', + event => { + if (deltaInput === document.activeElement) { + event.preventDefault(); + handleUpdate(Math.sign(-1 * event.deltaY)); + deltaInput.dispatchEvent(new Event("change", { bubbles: true })); + } + }, + { passive: false } + ); + + deltaInput.addEventListener('change', () => { + handleUpdate(); + }); + } } /**@inheritdoc */ diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index 871ff173..47dfe500 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -245,7 +245,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo }); if (!result) return; - await game.system.api.fields.ActionFields.CostField.execute.call({ actor }, result); const newMessageData = foundry.utils.deepClone(message.system); foundry.utils.setProperty(newMessageData, `${path}.result`, result.roll); diff --git a/module/applications/ui/combatTracker.mjs b/module/applications/ui/combatTracker.mjs index b1e658a5..2ed0e52b 100644 --- a/module/applications/ui/combatTracker.mjs +++ b/module/applications/ui/combatTracker.mjs @@ -42,13 +42,13 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C const modifierBP = this.combats .find(x => x.active) - ?.system?.extendedBattleToggles?.reduce((acc, toggle) => acc + toggle.category, 0) ?? 0; + ?.system?.extendedBattleToggles?.reduce((acc, toggle) => (acc ?? 0) + toggle.category, null) ?? null; const maxBP = CONFIG.DH.ENCOUNTER.BaseBPPerEncounter(context.characters.length) + modifierBP; const currentBP = AdversaryBPPerEncounter(context.adversaries, context.characters); Object.assign(context, { fear: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), - battlepoints: { max: maxBP, current: currentBP, hasModifierBP: Boolean(modifierBP) } + battlepoints: { max: maxBP, current: currentBP, hasModifierBP: modifierBP !== null } }); } @@ -57,21 +57,21 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C const adversaries = context.turns?.filter(x => x.isNPC) ?? []; const characters = context.turns?.filter(x => !x.isNPC) ?? []; + const spotlightQueueEnabled = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.SpotlightRequestQueue); const spotlightRequests = characters - ?.filter(x => !x.isNPC) + ?.filter(x => !x.isNPC && spotlightQueueEnabled) .filter(x => x.system.spotlight.requestOrderIndex > 0) .sort((a, b) => { const valueA = a.system.spotlight.requestOrderIndex; const valueB = b.system.spotlight.requestOrderIndex; - return valueA - valueB; }); Object.assign(context, { actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens, adversaries, - characters: characters?.filter(x => !x.isNPC).filter(x => x.system.spotlight.requestOrderIndex == 0), + characters: characters?.filter(x => !x.isNPC).filter(x => !spotlightQueueEnabled || x.system.spotlight.requestOrderIndex == 0), spotlightRequests }); } @@ -161,9 +161,11 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C if (this.viewed.turn !== toggleTurn) { const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns; - await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.spotlight.id); if (combatant.actor.type === 'character') { - await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.characterSpotlight.id); + await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.spotlight.id, + CONFIG.DH.GENERAL.countdownProgressionTypes.characterSpotlight.id); + } else { + await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.spotlight.id); } const autoPoints = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).actionPoints; diff --git a/module/applications/ui/countdowns.mjs b/module/applications/ui/countdowns.mjs index 96315b17..42920a4a 100644 --- a/module/applications/ui/countdowns.mjs +++ b/module/applications/ui/countdowns.mjs @@ -245,14 +245,20 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application return super.close(options); } - static async updateCountdowns(progressType) { + /** + * Sends updates of the countdowns to the GM player. Since this is asynchronous, be sure to + * update all the countdowns at the same time. + * + * @param {...any} progressTypes Countdowns to be updated + */ + static async updateCountdowns(...progressTypes) { const { countdownAutomation } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); if (!countdownAutomation) return; const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); const updatedCountdowns = Object.keys(countdownSetting.countdowns).reduce((acc, key) => { const countdown = countdownSetting.countdowns[key]; - if (countdown.progress.type === progressType && countdown.progress.current > 0) { + if (progressTypes.indexOf(countdown.progress.type) !== -1 && countdown.progress.current > 0) { acc.push(key); } @@ -260,7 +266,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application }, []); const countdownData = countdownSetting.toObject(); - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, { + const settings = { ...countdownData, countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => { const countdown = foundry.utils.deepClone(countdownData.countdowns[key]); @@ -271,14 +277,12 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application acc[key] = countdown; return acc; }, {}) + }; + await emitAsGM(GMUpdateEvent.UpdateCountdowns, + DhCountdowns.gmSetSetting.bind(settings), + settings, null, { + refreshType: RefreshType.Countdown }); - - const data = { refreshType: RefreshType.Countdown }; - await game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.Refresh, - data - }); - Hooks.callAll(socketEvent.Refresh, data); } async _onRender(context, options) { diff --git a/module/applications/ui/effectsDisplay.mjs b/module/applications/ui/effectsDisplay.mjs index 7f90e30b..0875e783 100644 --- a/module/applications/ui/effectsDisplay.mjs +++ b/module/applications/ui/effectsDisplay.mjs @@ -87,7 +87,7 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica async removeEffect(event) { const element = event.target.closest('.effect-container'); const effects = DhEffectsDisplay.getTokenEffects(); - const effect = effects.find(x => x.id === element.id); + const effect = effects.find(x => x.id === element.dataset.effectId); await effect.delete(); this.render(); } diff --git a/module/config/encounterConfig.mjs b/module/config/encounterConfig.mjs index 96efd745..0269b5c1 100644 --- a/module/config/encounterConfig.mjs +++ b/module/config/encounterConfig.mjs @@ -21,7 +21,7 @@ export const AdversaryBPPerEncounter = (adversaries, characters) => { if (type.partyAmountPerBP) { acc += characters.length === 0 ? 0 : Math.ceil(entry.nr / characters.length); } else { - acc += bpCost; + acc += bpCost * entry.nr; } return acc; diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index aea9bc48..3d993949 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -28,7 +28,8 @@ export const gameSettings = { LevelTiers: 'LevelTiers', Countdowns: 'Countdowns', LastMigrationVersion: 'LastMigrationVersion', - TagTeamRoll: 'TagTeamRoll' + TagTeamRoll: 'TagTeamRoll', + SpotlightRequestQueue: 'SpotlightRequestQueue', }; export const actionAutomationChoices = { diff --git a/module/data/settings/Homebrew.mjs b/module/data/settings/Homebrew.mjs index ead3c962..6f280cbd 100644 --- a/module/data/settings/Homebrew.mjs +++ b/module/data/settings/Homebrew.mjs @@ -1,14 +1,15 @@ import { defaultRestOptions } from '../../config/generalConfig.mjs'; import { ActionsField } from '../fields/actionField.mjs'; -const currencyField = (initial, label) => +const currencyField = (initial, label, icon) => new foundry.data.fields.SchemaField({ enabled: new foundry.data.fields.BooleanField({ required: true, initial: true }), label: new foundry.data.fields.StringField({ required: true, initial, label - }) + }), + icon: new foundry.data.fields.StringField({ required: true, nullable: false, blank: true, initial: icon }) }); export default class DhHomebrew extends foundry.abstract.DataModel { @@ -45,10 +46,22 @@ export default class DhHomebrew extends foundry.abstract.DataModel { initial: 'Gold', label: 'DAGGERHEART.SETTINGS.Homebrew.currency.currencyName' }), - coins: currencyField('Coins', 'DAGGERHEART.SETTINGS.Homebrew.currency.coinName'), - handfuls: currencyField('Handfuls', 'DAGGERHEART.SETTINGS.Homebrew.currency.handfulName'), - bags: currencyField('Bags', 'DAGGERHEART.SETTINGS.Homebrew.currency.bagName'), - chests: currencyField('Chests', 'DAGGERHEART.SETTINGS.Homebrew.currency.chestName') + coins: currencyField( + 'Coins', + 'DAGGERHEART.SETTINGS.Homebrew.currency.coinName', + 'fa-solid fa-coin-front' + ), + handfuls: currencyField( + 'Handfuls', + 'DAGGERHEART.SETTINGS.Homebrew.currency.handfulName', + 'fa-solid fa-coins' + ), + bags: currencyField('Bags', 'DAGGERHEART.SETTINGS.Homebrew.currency.bagName', 'fa-solid fa-sack'), + chests: currencyField( + 'Chests', + 'DAGGERHEART.SETTINGS.Homebrew.currency.chestName', + 'fa-solid fa-treasure-chest' + ) }), restMoves: new fields.SchemaField({ longRest: new fields.SchemaField({ @@ -139,22 +152,10 @@ export default class DhHomebrew extends foundry.abstract.DataModel { /** @inheritDoc */ _initializeSource(source, options = {}) { source = super._initializeSource(source, options); - source.currency.coins = { - enabled: source.currency.coins.enabled ?? true, - label: source.currency.coins.label || source.currency.coins - }; - source.currency.handfuls = { - enabled: source.currency.handfuls.enabled ?? true, - label: source.currency.handfuls.label || source.currency.handfuls - }; - source.currency.bags = { - enabled: source.currency.bags.enabled ?? true, - label: source.currency.bags.label || source.currency.bags - }; - source.currency.chests = { - enabled: source.currency.chests.enabled ?? true, - label: source.currency.chests.label || source.currency.chests - }; + for (const type of ['coins', 'handfuls', 'bags', 'chests']) { + const initial = this.schema.fields.currency.fields[type].getInitialValue(); + source.currency[type] = foundry.utils.mergeObject(initial, source.currency[type], { inplace: false }); + } return source; } } diff --git a/module/data/settings/VariantRules.mjs b/module/data/settings/VariantRules.mjs index ad7d707a..41c63be2 100644 --- a/module/data/settings/VariantRules.mjs +++ b/module/data/settings/VariantRules.mjs @@ -39,6 +39,13 @@ export default class DhVariantRules extends foundry.abstract.DataModel { label: 'DAGGERHEART.CONFIG.Range.close.name' }), far: new fields.NumberField({ required: true, initial: 60, label: 'DAGGERHEART.CONFIG.Range.far.name' }) + }), + massiveDamage: new fields.SchemaField({ + enabled: new fields.BooleanField({ + required: true, + initial: false, + label: 'DAGGERHEART.SETTINGS.VariantRules.FIELDS.massiveDamage.enabled.label' + }) }) }; } diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index e6856dab..ce39ed6a 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -237,6 +237,51 @@ export default class DHRoll extends Roll { } } +async function automateHopeFear(config) { + const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); + const hopeFearAutomation = automationSettings.hopeFear; + if (!config.source?.actor || + (game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) || + config.actionType === 'reaction' || + config.tagTeamSelected || + config.skips?.resources) + return; + const actor = await fromUuid(config.source.actor); + let updates = []; + if (!actor) return; + + if (config.rerolledRoll) { + if (config.roll.result.duality != config.rerolledRoll.result.duality) { + const hope = (config.roll.isCritical || config.roll.result.duality === 1 ? 1 : 0) + - (config.rerolledRoll.isCritical || config.rerolledRoll.result.duality === 1 ? 1 : 0); + const stress = (config.roll.isCritical ? 1 : 0) - (config.rerolledRoll.isCritical ? 1 : 0); + const fear = (config.roll.result.duality === -1 ? 1 : 0) + - (config.rerolledRoll.result.duality === -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 }); + } + } else { + if (config.roll.isCritical || config.roll.result.duality === 1) + updates.push({ key: 'hope', value: 1, total: -1, enabled: true }); + if (config.roll.isCritical) + updates.push({ key: 'stress', value: -1, total: 1, enabled: true }); + if (config.roll.result.duality === -1) + updates.push({ key: 'fear', value: 1, total: -1, enabled: true }); + } + + if (updates.length) { + const target = actor.system.partner ?? actor; + if (!['dead', 'defeated', 'unconscious'].some(x => actor.statuses.has(x))) { + await target.modifyResource(updates); + } + } +} + export const registerRollDiceHooks = () => { Hooks.on(`${CONFIG.DH.id}.postRollDuality`, async (config, message) => { const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); @@ -247,45 +292,16 @@ export const registerRollDiceHooks = () => { !config.skips?.updateCountdowns ) { const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns; - await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.actionRoll.id); if (config.roll.result.duality === -1) { - await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.fear.id); + await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.actionRoll.id, + CONFIG.DH.GENERAL.countdownProgressionTypes.fear.id); + } else { + await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.actionRoll.id); } } - const hopeFearAutomation = automationSettings.hopeFear; - if ( - !config.source?.actor || - (game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) || - config.actionType === 'reaction' || - config.tagTeamSelected || - config.skips?.resources - ) - return; - const actor = await fromUuid(config.source.actor); - let updates = []; - if (!actor) return; - if (config.roll.isCritical || config.roll.result.duality === 1) - updates.push({ key: 'hope', value: 1, total: -1, enabled: true }); - if (config.roll.isCritical) updates.push({ key: 'stress', value: 1, total: -1, enabled: true }); - if (config.roll.result.duality === -1) updates.push({ key: 'fear', value: 1, total: -1, enabled: true }); - - if (config.rerolledRoll) { - if (config.rerolledRoll.isCritical || config.rerolledRoll.result.duality === 1) - updates.push({ key: 'hope', value: -1, total: 1, enabled: true }); - if (config.rerolledRoll.isCritical) updates.push({ key: 'stress', value: -1, total: 1, enabled: true }); - if (config.rerolledRoll.result.duality === -1) - updates.push({ key: 'fear', value: -1, total: 1, enabled: true }); - } - - if (updates.length) { - const target = actor.system.partner ?? actor; - if (!['dead', 'defeated', 'unconscious'].some(x => actor.statuses.has(x))) { - if (config.rerolledRoll) target.modifyResource(updates); - else config.costs = [...(config.costs ?? []), ...updates]; - } - } + await automateHopeFear(config); if (!config.roll.hasOwnProperty('success') && !config.targets?.length) return; @@ -296,7 +312,5 @@ export const registerRollDiceHooks = () => { const currentCombatant = game.combat.combatants.get(game.combat.current?.combatantId); if (currentCombatant?.actorId == actor.id) ui.combat.setCombatantSpotlight(currentCombatant.id); } - - return; }); }; diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index 813c913b..59cb6e02 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -262,8 +262,7 @@ export default class DualityRoll extends D20Roll { targets: message.system.targets, tagTeamSelected: Object.values(tagTeamSettings.members).some(x => x.messageId === message._id), roll: newRoll, - rerolledRoll: - newRoll.result.duality !== message.system.roll.result.duality ? message.system.roll : undefined + rerolledRoll: message.system.roll }); return { newRoll, parsedRoll }; } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 20339375..c269b686 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -679,6 +679,10 @@ export default class DhpActor extends Actor { return updates; } + /** + * Resources are modified asynchronously, so be careful not to update the same resource in + * quick succession. + */ async modifyResource(resources) { if (!resources?.length) return; @@ -761,6 +765,10 @@ export default class DhpActor extends Actor { } convertDamageToThreshold(damage) { + const massiveDamageEnabled=game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).massiveDamage.enabled; + if (massiveDamageEnabled && damage >= (this.system.damageThresholds.severe * 2)) { + return 4; + } return damage >= this.system.damageThresholds.severe ? 3 : damage >= this.system.damageThresholds.major ? 2 : 1; } @@ -854,7 +862,7 @@ export default class DhpActor extends Actor { acc.push(effect); const currentStatusActiveEffects = acc.filter( - x => x.statuses.size === 1 && x.name === game.i18n.localize(statusMap.get(x.statuses.first()).name) + x => x.statuses.size === 1 && x.name === game.i18n.localize(statusMap.get(x.statuses.first())?.name) ); for (var status of effect.statuses) { diff --git a/module/documents/tooltipManager.mjs b/module/documents/tooltipManager.mjs index 23e04ac1..b0a107b9 100644 --- a/module/documents/tooltipManager.mjs +++ b/module/documents/tooltipManager.mjs @@ -10,6 +10,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti let html = options.html; if (element.dataset.tooltip?.startsWith('#battlepoints#')) { this.#wide = true; + this.#bordered = true; html = await this.getBattlepointHTML(element.dataset.combatId); options.direction = this._determineItemTooltipDirection(element); @@ -22,6 +23,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti return; } else { this.#wide = false; + this.#bordered = false; } if (element.dataset.tooltip === '#effect-display#') { @@ -73,7 +75,8 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti { item: item, description: item.system?.enrichedDescription ?? item.enrichedDescription, - config: CONFIG.DH + config: CONFIG.DH, + allDomains: CONFIG.DH.DOMAIN.allDomains() } ); @@ -168,14 +171,6 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti super.activate(element, { ...options, html: html }); } - _setStyle(position = {}) { - super._setStyle(position); - - if (this.#bordered) { - this.tooltip.classList.add('bordered-tooltip'); - } - } - _determineItemTooltipDirection(element, prefered = this.constructor.TOOLTIP_DIRECTIONS.LEFT) { const pos = element.getBoundingClientRect(); const dirs = this.constructor.TOOLTIP_DIRECTIONS; @@ -247,12 +242,17 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti if (this.#wide) { this.tooltip.classList.add('wide'); } + + if (this.#bordered) { + this.tooltip.classList.add('bordered-tooltip'); + } } /**@inheritdoc */ lockTooltip() { const clone = super.lockTooltip(); - clone.classList.add('wide'); + if (this.#wide) clone.classList.add('wide'); + if (this.#bordered) clone.classList.add('bordered-tooltip'); return clone; } diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 39d5298d..396ed2fa 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -198,7 +198,7 @@ foundry.dice.terms.Die.prototype.selfCorrecting = function (modifier) { }; export const getDamageKey = damage => { - return ['none', 'minor', 'major', 'severe', 'any'][damage]; + return ['none', 'minor', 'major', 'severe', 'massive','any'][damage]; }; export const getDamageLabel = damage => { @@ -211,7 +211,8 @@ export const damageKeyToNumber = key => { minor: 1, major: 2, severe: 3, - any: 4 + massive: 4, + any: 5 }[key]; }; diff --git a/module/systemRegistration/settings.mjs b/module/systemRegistration/settings.mjs index 6954730f..d08d65d1 100644 --- a/module/systemRegistration/settings.mjs +++ b/module/systemRegistration/settings.mjs @@ -13,6 +13,16 @@ export const registerDHSettings = () => { registerMenuSettings(); registerMenus(); registerNonConfigSettings(); + + game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.SpotlightRequestQueue, { + name: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.SpotlightRequestQueue.name'), + label: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.SpotlightRequestQueue.label'), + hint: game.i18n.localize('DAGGERHEART.SETTINGS.Menu.SpotlightRequestQueue.hint'), + scope: 'world', + config: true, + type: Boolean, + onChange: () => ui.combat.render(), + }) }; const registerMenuSettings = () => { diff --git a/src/packs/adversaries/adversary_Adult_Flickerfly_G7jiltRjgvVhZewm.json b/src/packs/adversaries/adversary_Adult_Flickerfly_G7jiltRjgvVhZewm.json index 95e6c243..0e3a89c6 100644 --- a/src/packs/adversaries/adversary_Adult_Flickerfly_G7jiltRjgvVhZewm.json +++ b/src/packs/adversaries/adversary_Adult_Flickerfly_G7jiltRjgvVhZewm.json @@ -427,7 +427,8 @@ } } ], - "includeBase": false + "includeBase": false, + "direct": true }, "target": { "type": "any", diff --git a/src/packs/adversaries/adversary_Brawny_Zombie_2UeZ0tEe7AzgSJNd.json b/src/packs/adversaries/adversary_Brawny_Zombie_2UeZ0tEe7AzgSJNd.json index d77d3379..cf6583e4 100644 --- a/src/packs/adversaries/adversary_Brawny_Zombie_2UeZ0tEe7AzgSJNd.json +++ b/src/packs/adversaries/adversary_Brawny_Zombie_2UeZ0tEe7AzgSJNd.json @@ -307,7 +307,8 @@ } } ], - "includeBase": false + "includeBase": false, + "direct": true }, "target": { "type": "any", @@ -319,7 +320,7 @@ "trait": null, "difficulty": null, "bonus": null, - "advState": "neutral", + "advState": "advantage", "diceRolling": { "multiplier": "prof", "flatMultiplier": 1, diff --git a/src/packs/adversaries/adversary_Cave_Ogre_8Zkqk1jU09nKL2fy.json b/src/packs/adversaries/adversary_Cave_Ogre_8Zkqk1jU09nKL2fy.json index 3ccac5dd..fd73ee36 100644 --- a/src/packs/adversaries/adversary_Cave_Ogre_8Zkqk1jU09nKL2fy.json +++ b/src/packs/adversaries/adversary_Cave_Ogre_8Zkqk1jU09nKL2fy.json @@ -39,7 +39,8 @@ "experiences": { "7GpgCWSe6hNwnOO7": { "name": "Throw", - "value": 2 + "value": 2, + "description": "" } }, "bonuses": { @@ -105,7 +106,8 @@ }, "base": false } - ] + ], + "direct": true }, "name": "Club", "img": "icons/weapons/clubs/club-banded-barbed-black.webp", @@ -337,10 +339,11 @@ { "value": { "custom": { - "enabled": false + "enabled": false, + "formula": "" }, "flatMultiplier": 1, - "dice": "d12", + "dice": "d10", "bonus": 2, "multiplier": "flat" }, @@ -356,12 +359,14 @@ "dice": "d6", "bonus": null, "custom": { - "enabled": false + "enabled": false, + "formula": "" } } } ], - "includeBase": false + "includeBase": false, + "direct": true }, "target": { "type": "any", @@ -528,7 +533,8 @@ } } ], - "includeBase": false + "includeBase": false, + "direct": true }, "target": { "type": "any", diff --git a/src/packs/adversaries/adversary_Demon_of_Jealousy_SxSOkM4bcVOFyjbo.json b/src/packs/adversaries/adversary_Demon_of_Jealousy_SxSOkM4bcVOFyjbo.json index 08b22a0b..7ea12036 100644 --- a/src/packs/adversaries/adversary_Demon_of_Jealousy_SxSOkM4bcVOFyjbo.json +++ b/src/packs/adversaries/adversary_Demon_of_Jealousy_SxSOkM4bcVOFyjbo.json @@ -107,7 +107,8 @@ }, "base": false } - ] + ], + "direct": true }, "img": "icons/magic/symbols/rune-sigil-rough-white-teal.webp", "type": "attack", diff --git a/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json b/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json index 13b9d624..72ec986d 100644 --- a/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json +++ b/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json @@ -108,7 +108,8 @@ }, "base": false } - ] + ], + "direct": true }, "type": "attack", "chatDisplay": false @@ -358,7 +359,8 @@ } } ], - "includeBase": false + "includeBase": false, + "direct": true }, "target": { "type": "any", diff --git a/src/packs/adversaries/adversary_Dire_Wolf_wNzeuQLfLUMvgHlQ.json b/src/packs/adversaries/adversary_Dire_Wolf_wNzeuQLfLUMvgHlQ.json index e3b34aea..a900aa7b 100644 --- a/src/packs/adversaries/adversary_Dire_Wolf_wNzeuQLfLUMvgHlQ.json +++ b/src/packs/adversaries/adversary_Dire_Wolf_wNzeuQLfLUMvgHlQ.json @@ -377,7 +377,8 @@ } } ], - "includeBase": false + "includeBase": false, + "direct": true }, "target": { "type": "any", diff --git a/src/packs/adversaries/adversary_Master_Assassin_dNta0cUzr96xcFhf.json b/src/packs/adversaries/adversary_Master_Assassin_dNta0cUzr96xcFhf.json index 943559e1..557ef607 100644 --- a/src/packs/adversaries/adversary_Master_Assassin_dNta0cUzr96xcFhf.json +++ b/src/packs/adversaries/adversary_Master_Assassin_dNta0cUzr96xcFhf.json @@ -233,7 +233,89 @@ "system": { "description": "
The Assassin deals direct damage while they’re Hidden.
", "resource": null, - "actions": {}, + "actions": { + "xFBE0jLf96fbCY7K": { + "type": "attack", + "_id": "xFBE0jLf96fbCY7K", + "systemPath": "actions", + "baseAction": false, + "description": "The Assassin deals direct damage while they’re Hidden.
", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "damage": { + "parts": [ + { + "value": { + "custom": { + "enabled": false, + "formula": "" + }, + "flatMultiplier": 2, + "dice": "d10", + "bonus": 2, + "multiplier": "flat" + }, + "applyTo": "hitPoints", + "type": [ + "physical" + ], + "base": false, + "resultBased": false, + "valueAlt": { + "multiplier": "prof", + "flatMultiplier": 1, + "dice": "d6", + "bonus": null, + "custom": { + "enabled": false, + "formula": "" + } + } + } + ], + "includeBase": false, + "direct": true + }, + "target": { + "type": "any", + "amount": null + }, + "effects": [], + "roll": { + "type": "attack", + "trait": null, + "difficulty": null, + "bonus": null, + "advState": "neutral", + "diceRolling": { + "multiplier": "prof", + "flatMultiplier": 1, + "dice": "d6", + "compare": null, + "treshold": null + }, + "useDefault": false + }, + "save": { + "trait": null, + "difficulty": null, + "damageMod": "none" + }, + "name": "Hidden attack", + "img": "icons/magic/perception/silhouette-stealth-shadow.webp", + "range": "close" + } + }, "originItemType": null, "originId": null }, diff --git a/src/packs/adversaries/adversary_Minotaur_Wrecker_rM9qCIYeWg9I0B4l.json b/src/packs/adversaries/adversary_Minotaur_Wrecker_rM9qCIYeWg9I0B4l.json index 10f48d17..570db804 100644 --- a/src/packs/adversaries/adversary_Minotaur_Wrecker_rM9qCIYeWg9I0B4l.json +++ b/src/packs/adversaries/adversary_Minotaur_Wrecker_rM9qCIYeWg9I0B4l.json @@ -478,7 +478,8 @@ } } ], - "includeBase": false + "includeBase": false, + "direct": true }, "target": { "type": "any", diff --git a/src/packs/adversaries/adversary_Mortal_Hunter_mVV7a7KQAORoPMgZ.json b/src/packs/adversaries/adversary_Mortal_Hunter_mVV7a7KQAORoPMgZ.json index 74d7e2c3..721d8973 100644 --- a/src/packs/adversaries/adversary_Mortal_Hunter_mVV7a7KQAORoPMgZ.json +++ b/src/packs/adversaries/adversary_Mortal_Hunter_mVV7a7KQAORoPMgZ.json @@ -340,6 +340,87 @@ "name": "Curse", "img": "icons/magic/unholy/hand-marked-pink.webp", "range": "veryClose" + }, + "zLKfwa8a2YBRLKAF": { + "type": "attack", + "_id": "zLKfwa8a2YBRLKAF", + "systemPath": "actions", + "baseAction": false, + "description": "Attacks made by the Hunter against a Deathlocked target deal direct damage.
", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "damage": { + "parts": [ + { + "value": { + "custom": { + "enabled": false, + "formula": "" + }, + "flatMultiplier": 2, + "dice": "d12", + "bonus": 1, + "multiplier": "flat" + }, + "applyTo": "hitPoints", + "type": [ + "physical" + ], + "base": false, + "resultBased": false, + "valueAlt": { + "multiplier": "prof", + "flatMultiplier": 1, + "dice": "d6", + "bonus": null, + "custom": { + "enabled": false, + "formula": "" + } + } + } + ], + "includeBase": false, + "direct": true + }, + "target": { + "type": "any", + "amount": null + }, + "effects": [], + "roll": { + "type": "attack", + "trait": null, + "difficulty": null, + "bonus": null, + "advState": "neutral", + "diceRolling": { + "multiplier": "prof", + "flatMultiplier": 1, + "dice": "d6", + "compare": null, + "treshold": null + }, + "useDefault": false + }, + "save": { + "trait": null, + "difficulty": null, + "damageMod": "none" + }, + "name": "Deathlocked attack", + "img": "icons/magic/unholy/hand-marked-pink.webp", + "range": "veryClose" } }, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Tangle_Bramble_Swarm_PKSXFuaIHUCoH63A.json b/src/packs/adversaries/adversary_Tangle_Bramble_Swarm_PKSXFuaIHUCoH63A.json index d5f30dda..2a753812 100644 --- a/src/packs/adversaries/adversary_Tangle_Bramble_Swarm_PKSXFuaIHUCoH63A.json +++ b/src/packs/adversaries/adversary_Tangle_Bramble_Swarm_PKSXFuaIHUCoH63A.json @@ -312,9 +312,10 @@ { "value": { "custom": { - "enabled": false + "enabled": false, + "formula": "" }, - "flatMultiplier": 1, + "flatMultiplier": 2, "dice": "d6", "bonus": 8, "multiplier": "flat" @@ -331,12 +332,14 @@ "dice": "d6", "bonus": null, "custom": { - "enabled": false + "enabled": false, + "formula": "" } } } ], - "includeBase": false + "includeBase": false, + "direct": true }, "target": { "type": "any", diff --git a/src/packs/adversaries/adversary_Volcanic_Dragon__Molten_Scourge_eArAPuB38CNR0ZIM.json b/src/packs/adversaries/adversary_Volcanic_Dragon__Molten_Scourge_eArAPuB38CNR0ZIM.json index 056bf848..d04dab50 100644 --- a/src/packs/adversaries/adversary_Volcanic_Dragon__Molten_Scourge_eArAPuB38CNR0ZIM.json +++ b/src/packs/adversaries/adversary_Volcanic_Dragon__Molten_Scourge_eArAPuB38CNR0ZIM.json @@ -804,7 +804,8 @@ } } ], - "includeBase": false + "includeBase": false, + "direct": true }, "target": { "type": "any", diff --git a/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json b/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json index 81ce16f9..39070236 100644 --- a/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json +++ b/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json @@ -457,7 +457,8 @@ } } ], - "includeBase": false + "includeBase": false, + "direct": true }, "target": { "type": "any", diff --git a/src/packs/subclasses/feature_Iron_Will_7AVRNyBcd1Nffjtn.json b/src/packs/subclasses/feature_Iron_Will_7AVRNyBcd1Nffjtn.json index eda72fd7..bea62e2b 100644 --- a/src/packs/subclasses/feature_Iron_Will_7AVRNyBcd1Nffjtn.json +++ b/src/packs/subclasses/feature_Iron_Will_7AVRNyBcd1Nffjtn.json @@ -32,7 +32,7 @@ "img": "icons/equipment/chest/breastplate-helmet-metal.webp", "changes": [ { - "key": "system.rules.damageReduction.maxArmorMarked.stressExtra", + "key": "system.rules.damageReduction.maxArmorMarked.value", "mode": 2, "value": "1", "priority": null diff --git a/src/packs/subclasses/feature_Wings_of_Light_KkQH0tYhagIqe2MT.json b/src/packs/subclasses/feature_Wings_of_Light_KkQH0tYhagIqe2MT.json index 2fa32bec..f7157194 100644 --- a/src/packs/subclasses/feature_Wings_of_Light_KkQH0tYhagIqe2MT.json +++ b/src/packs/subclasses/feature_Wings_of_Light_KkQH0tYhagIqe2MT.json @@ -15,7 +15,16 @@ "description": "Mark a Stress to pick up and carry another willing creature approximately your size or smaller.
", "chatDisplay": true, "actionType": "action", - "cost": [], + "cost": [ + { + "scalable": false, + "key": "stress", + "value": 1, + "itemId": null, + "step": null, + "consumeOnSuccess": false + } + ], "uses": { "value": null, "max": "", diff --git a/styles/less/dialog/index.less b/styles/less/dialog/index.less index 91b2846b..b5ed8764 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -37,3 +37,5 @@ @import './image-select/sheet.less'; @import './item-transfer/sheet.less'; + +@import './settings/change-currency-icon.less'; \ No newline at end of file diff --git a/styles/less/dialog/settings/change-currency-icon.less b/styles/less/dialog/settings/change-currency-icon.less new file mode 100644 index 00000000..61870a4b --- /dev/null +++ b/styles/less/dialog/settings/change-currency-icon.less @@ -0,0 +1,13 @@ +.application.daggerheart.dialog.dh-style.change-currency-icon { + .displayed-icon { + height: 2.5rem; + text-align: center; + font-size: 2.5rem; + margin-bottom: 1.25rem; + } + .input-row { + display: flex; + gap: 4px; + align-items: center; + } +} \ 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 ac269172..46003975 100644 --- a/styles/less/hud/token-hud/token-hud.less +++ b/styles/less/hud/token-hud/token-hud.less @@ -56,7 +56,7 @@ .effect-locked { position: absolute; bottom: 2px; - right: 2px; + left: 11.5px; font-size: 12px; color: @golden; filter: drop-shadow(0 0 3px black); diff --git a/styles/less/utils/fonts.less b/styles/less/utils/fonts.less index 7d84366a..5c1e597a 100755 --- a/styles/less/utils/fonts.less +++ b/styles/less/utils/fonts.less @@ -2,14 +2,14 @@ @import './mixin.less'; :root { - --font-title: 'Cinzel Decorative'; - --font-subtitle: 'Cinzel'; - --font-body: 'Montserrat'; + --dh-font-title: 'Cinzel Decorative'; + --dh-font-subtitle: 'Cinzel'; + --dh-font-body: 'Montserrat'; } -@font-title: ~"var(--font-title, 'Cinzel Decorative'), serif"; -@font-subtitle: ~"var(--font-subtitle, 'Cinzel'), serif"; -@font-body: ~"var(--font-body, 'Montserrat'), sans-serif"; +@font-title: ~"var(--dh-font-title, 'Cinzel Decorative'), serif"; +@font-subtitle: ~"var(--dh-font-subtitle, 'Cinzel'), serif"; +@font-body: ~"var(--dh-font-body, 'Montserrat'), sans-serif"; .dh-style { .dh-typography(); diff --git a/styles/less/ux/tooltip/battlepoints.less b/styles/less/ux/tooltip/battlepoints.less index 61c7083d..9fe43a75 100644 --- a/styles/less/ux/tooltip/battlepoints.less +++ b/styles/less/ux/tooltip/battlepoints.less @@ -3,7 +3,12 @@ display: flex; flex-direction: column; gap: 8px; - margin-bottom: 16px; + + .battlepoint-categories-inner-container { + display: flex; + flex-direction: column; + gap: 8px; + } .battlepoint-grouping-container { display: flex; diff --git a/styles/less/ux/tooltip/bordered-tooltip.less b/styles/less/ux/tooltip/bordered-tooltip.less index a4779d71..78622377 100644 --- a/styles/less/ux/tooltip/bordered-tooltip.less +++ b/styles/less/ux/tooltip/bordered-tooltip.less @@ -1,4 +1,5 @@ -#tooltip.bordered-tooltip { +#tooltip.bordered-tooltip, +.locked-tooltip.bordered-tooltip { border: 1px solid @golden; background-image: url('../assets/parchments/dh-parchment-dark.png'); @@ -14,6 +15,7 @@ .tooltip-header { display: flex; flex-direction: column; + align-items: center; text-align: start; padding: 5px; gap: 0px; diff --git a/styles/less/ux/tooltip/tooltip.less b/styles/less/ux/tooltip/tooltip.less index 4579a3d8..2aa1c2c7 100644 --- a/styles/less/ux/tooltip/tooltip.less +++ b/styles/less/ux/tooltip/tooltip.less @@ -2,6 +2,10 @@ .locked-tooltip { &.wide { max-width: 480px; + + .daggerheart.dh-style.tooltip { + align-items: start; + } } .daggerheart.dh-style.tooltip { diff --git a/system.json b/system.json index 7e3c6b07..14a4efc8 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "id": "daggerheart", "title": "Daggerheart", "description": "An unofficial implementation of the Daggerheart system", - "version": "1.3.1", + "version": "1.3.2", "compatibility": { "minimum": "13.346", "verified": "13.351", diff --git a/templates/hud/tokenHUD.hbs b/templates/hud/tokenHUD.hbs index 09259d4a..f079e5d9 100644 --- a/templates/hud/tokenHUD.hbs +++ b/templates/hud/tokenHUD.hbs @@ -46,8 +46,12 @@