[V14] Effect Stacking (#1667)

* Added the ability for effects to have stacks

* Fixed effect stacking

* Improved token overlay spacing

* Compendium updaetes

* Simplify effect click event (#1748)

* Fixed a bunch of deprecations

* Corrected AgileScout Beastform json data

* Updated TokenHUD to the new v14

* Removed DestroyOnEmpty from consumables

* Fixed so that tooltips don't get stuck (#1745)

* [Feature] TagTeam Partial Rendering (#1735)

* I done did it, I think

* Think I fixed the partial rendering bug for gm->player

* [V14] 1743 - Damage Update Error (#1746)

* Fixed DamageParts causing errors on update

* Fixed ActionBaseConfig error when no damage present on the action

* Fix removal of damage field

* Removed unneccessary default value function for parts

---------

Co-authored-by: Carlos Fernandez <cfern1990@gmail.com>

* Simplify effect click event

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
Co-authored-by: WBHarry <89362246+WBHarry@users.noreply.github.com>

* Fixed stacking-value pointer event

* Set the stacking value in EffectsDisplay to be tabular-nums for monospacing

* Made baseEffect.stacking nullable instead of having an enabled property

* .

* Fixed so that actor._onUpdateDescantDocuments re-renders the EffectDisplay if effects were updated

---------

Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>
This commit is contained in:
WBHarry 2026-03-25 13:54:14 +01:00 committed by GitHub
parent 64ce615116
commit aa1d117c43
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 349 additions and 204 deletions

View file

@ -153,7 +153,8 @@
"Config": {
"rangeDependence": {
"title": "Range Dependence"
}
},
"stacking": { "title": "Stacking" }
},
"RangeDependance": {
"hint": "Settings for an optional distance at which this effect should activate",
@ -2987,6 +2988,8 @@
},
"EffectsDisplay": {
"removeThing": "[Right Click] Remove {thing}",
"increaseStacks": "[Left Click] Increment Stacks",
"decreaseStacks": "[Right Click] Decrement Stacks",
"appliedBy": "Applied By: {by}"
},
"ItemBrowser": {

View file

@ -151,6 +151,10 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
});
});
htmlElement
.querySelector('.stacking-change-checkbox')
?.addEventListener('change', this.stackingChangeToggle.bind(this));
htmlElement
.querySelector('.armor-change-checkbox')
?.addEventListener('change', this.armorChangeToggle.bind(this));
@ -209,6 +213,16 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac
return partContext;
}
stackingChangeToggle(event) {
const stackingFields = this.document.system.schema.fields.stacking.fields;
const systemData = {
stacking: event.target.checked
? { value: stackingFields.value.initial, max: stackingFields.max.initial }
: null
};
return this.submit({ updateData: { system: systemData } });
}
armorChangeToggle(event) {
if (event.target.checked) {
this.addArmorChange();

View file

@ -49,11 +49,9 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
if (this.element) {
this.element.querySelectorAll('.effect-container a').forEach(element => {
element.addEventListener('contextmenu', this.removeEffect.bind(this));
});
for (const element of this.element?.querySelectorAll('.effect-container a') ?? []) {
element.addEventListener('click', e => this.#onClickEffect(e));
element.addEventListener('contextmenu', e => this.#onClickEffect(e, -1));
}
}
@ -87,11 +85,21 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica
this.render();
}
async removeEffect(event) {
async #onClickEffect(event, delta = 1) {
const element = event.target.closest('.effect-container');
const effects = DhEffectsDisplay.getTokenEffects();
const effect = effects.find(x => x.id === element.dataset.effectId);
if (!effect || (delta >= 0 && !effect.system.stacking)) {
return;
}
const maxValue = effect.system.stacking?.max ?? Infinity;
const newValue = Math.clamp((effect.system.stacking?.value ?? 1) + delta, 0, maxValue);
if (newValue > 0) {
await effect.update({ 'system.stacking.value': newValue });
} else {
await effect.delete();
}
this.render();
}

View file

@ -1,56 +1,3 @@
/**
* @typedef ContextMenuEntry
* @property {string} name The context menu label. Can be localized.
* @property {string} [icon] A string containing an HTML icon element for the menu item.
* @property {string} [classes] Additional CSS classes to apply to this menu item.
* @property {string} [group] An identifier for a group this entry belongs to.
* @property {ContextMenuJQueryCallback} callback The function to call when the menu item is clicked.
* @property {ContextMenuCondition|boolean} [condition] A function to call or boolean value to determine if this entry
* appears in the menu.
*/
/**
* @callback ContextMenuCondition
* @param {jQuery|HTMLElement} html The element of the context menu entry.
* @returns {boolean} Whether the entry should be rendered in the context menu.
*/
/**
* @callback ContextMenuCallback
* @param {HTMLElement} target The element that the context menu has been triggered for.
* @returns {unknown}
*/
/**
* @callback ContextMenuJQueryCallback
* @param {HTMLElement|jQuery} target The element that the context menu has been triggered for. Will
* either be a jQuery object or an HTMLElement instance, depending
* on how the ContextMenu was configured.
* @returns {unknown}
*/
/**
* @typedef ContextMenuOptions
* @property {string} [eventName="contextmenu"] Optionally override the triggering event which can spawn the menu. If
* the menu is using fixed positioning, this event must be a MouseEvent.
* @property {ContextMenuCallback} [onOpen] A function to call when the context menu is opened.
* @property {ContextMenuCallback} [onClose] A function to call when the context menu is closed.
* @property {boolean} [fixed=false] If true, the context menu is given a fixed position rather than being
* injected into the target.
* @property {boolean} [jQuery=true] If true, callbacks will be passed jQuery objects instead of HTMLElement
* instances.
*/
/**
* @typedef ContextMenuRenderOptions
* @property {Event} [event] The event that triggered the context menu opening.
* @property {boolean} [animate=true] Animate the context menu opening.
*/
/**
* A subclass of ContextMenu.
* @extends {foundry.applications.ux.ContextMenu}
*/
export default class DHContextMenu extends foundry.applications.ux.ContextMenu {
/**
* Trigger a context menu event in response to a normal click on a additional options button.

View file

@ -30,8 +30,8 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
if (!effect.img) continue;
const promise =
effect === overlayEffect
? this._drawOverlay(effect.img, effect.tint)
: this._drawEffect(effect.img, effect.tint);
? this._drawOverlay(effect.img, effect.tint, effect)
: this._drawEffect(effect.img, effect.tint, effect);
promises.push(
promise.then(e => {
if (e) e.zIndex = i;
@ -45,6 +45,39 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token {
this.renderFlags.set({ refreshEffects: true });
}
/**@inheritdoc */
async _drawEffect(src, tint, effect) {
if (!src) return;
const tex = await foundry.canvas.loadTexture(src, { fallback: 'icons/svg/hazard.svg' });
const icon = new PIXI.Sprite(tex);
icon.tint = tint ?? 0xffffff;
if (effect.system.stacking?.value > 1) {
const stackOverlay = new PIXI.Text(effect.system.stacking.value, {
fill: '#f3c267',
stroke: '#000000',
fontSize: 96,
strokeThickness: 4
});
const nrDigits = Math.floor(Math.log10(effect.system.stacking.value)) + 1;
stackOverlay.y = -8;
/* This does not account for 1:s being much less wide than other digits. I don't think it's desired however as it makes it look jumpy */
stackOverlay.x = icon.width - 8 - nrDigits * 56;
stackOverlay.anchor.set(0, 0);
icon.addChild(stackOverlay);
}
return this.effects.addChild(icon);
}
async _drawOverlay(src, tint, effect) {
const icon = await this._drawEffect(src, tint, effect);
if (icon) icon.alpha = 0.8;
this.effects.overlay = icon ?? null;
return icon;
}
/**
* Returns the distance from this token to another token object.
* This value is corrected to handle alternate token sizes and other grid types

View file

@ -80,7 +80,20 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
initial: CONFIG.DH.GENERAL.range.melee.id,
label: 'DAGGERHEART.GENERAL.range'
})
})
}),
stacking: new fields.SchemaField(
{
value: new fields.NumberField({
initial: 1,
min: 1,
integer: true,
nullable: false,
label: 'DAGGERHEART.GENERAL.value'
}),
max: new fields.NumberField({ integer: true, label: 'DAGGERHEART.GENERAL.max' })
},
{ nullable: true, initial: null }
)
};
}

View file

@ -106,22 +106,11 @@ export default class EffectsField extends fields.ArrayField {
}
/**
* Apply an Effect to a target or enable it if already on it
* Apply an Effect to a target
* @param {object} effect Effect object containing ActiveEffect UUID
* @param {object} actor Actor Document
*/
static async applyEffect(effect, actor) {
const existingEffect = actor.effects.find(e => e.origin === effect.uuid);
if (existingEffect) {
return effect.update(
foundry.utils.mergeObject({
...effect.constructor.getInitialDuration(),
disabled: false
})
);
}
// Otherwise, create a new effect on the target
const effectData = foundry.utils.mergeObject({
...(effect.toObject?.() ?? effect),
disabled: false,

View file

@ -108,6 +108,18 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
update.img = 'icons/magic/life/heart-cross-blue.webp';
}
const existingEffect = this.actor.effects.find(x => x.origin === data.origin);
const stacks = Boolean(data.system?.stacking);
if (existingEffect && !stacks) return false;
if (existingEffect && stacks) {
const incrementedValue = existingEffect.system.stacking.value + 1;
await existingEffect.update({
'system.stacking.value': Math.min(incrementedValue, existingEffect.system.stacking.max ?? Infinity)
});
return false;
}
const statuses = Object.keys(data.statuses ?? {});
const immuneStatuses =
statuses.filter(
@ -184,7 +196,10 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
} catch (_) {}
}
const evalValue = this.effectSafeEval(itemAbleRollParse(key, parseModel, effect.parent));
const stackingParsedValue = effect.system.stacking
? Roll.replaceFormulaData(key, { stacks: effect.system.stacking.value })
: key;
const evalValue = itemAbleRollParse(stackingParsedValue, parseModel, effect.parent);
return evalValue ?? key;
}

View file

@ -122,6 +122,14 @@ export default class DhpActor extends Actor {
}
}
_onUpdateDescendantDocuments(parent, collection, documents, changes, options, userId) {
if (collection === 'effects') {
ui.effectsDisplay.render();
}
super._onUpdateDescendantDocuments(parent, collection, documents, changes, options, userId);
}
async updateLevel(newLevel) {
if (!['character', 'companion'].includes(this.type) || newLevel === this.system.levelData.level.changed) return;

View file

@ -71,13 +71,17 @@
"changes": [
{
"key": "system.bonuses.roll.attack.bonus",
"value": 1,
"type": "add",
"value": "@stacks",
"priority": null,
"type": "add"
"phase": "initial"
}
],
"duration": {
"type": "shortRest"
},
"stacking": {
"max": null
}
},
"disabled": false,

View file

@ -69,14 +69,18 @@
"changes": [
{
"key": "system.evasion",
"value": 2,
"type": "add",
"value": "2 * @stacks",
"priority": null,
"type": "add"
"phase": "initial"
}
],
"duration": {
"type": "temporary",
"description": "<p><span style=\"color: rgb(239, 230, 216); font-family: Montserrat, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgba(24, 22, 46, 0.565); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;\">Until the next time an attack succeeds against you.</span></p>"
},
"stacking": {
"max": null
}
},
"disabled": false,

View file

@ -127,7 +127,7 @@
"sort": 3400000,
"effects": [
{
"name": "Corroded (1 stack)",
"name": "Corroded",
"img": "icons/magic/acid/dissolve-bone-white.webp",
"origin": "Compendium.daggerheart.domains.Item.qJaSNTuDfbPVr8Lb",
"transfer": false,
@ -139,27 +139,31 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
},
"changes": [
{
"key": "system.difficulty",
"mode": 2,
"value": "-1",
"priority": null
"type": "add",
"value": "-@stack",
"priority": null,
"phase": "initial"
}
],
"stacking": {
"max": null
},
"duration": {
"type": ""
}
},
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "<p><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">While a target is </span><span style=\"box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgb(93, 20, 43) rgba(0, 0, 0, 0);color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial\">Corroded</span><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">, they gain a 1 penalty to their </span><span class=\"tooltip-convert\" style=\"box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgb(93, 20, 43) rgba(0, 0, 0, 0);color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial\">Difficulty</span><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\"> for every 2 Stress you spent. This </span><span class=\"tooltip-convert\" style=\"box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgb(93, 20, 43) rgba(0, 0, 0, 0);color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial\">condition</span><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\"> can stack.</p>",
"description": "<p><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">While a target is </span><span style=\"box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgb(93, 20, 43) rgba(0, 0, 0, 0);color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial\">Corroded</span><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\">, they gain a 1 penalty to their </span><span style=\"box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgb(93, 20, 43) rgba(0, 0, 0, 0);color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial\" class=\"tooltip-convert\">Difficulty</span><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\"> for every 2 Stress you spent. This </span><span style=\"box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgb(93, 20, 43) rgba(0, 0, 0, 0);color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial\" class=\"tooltip-convert\">condition</span><span style=\"color:rgb(239, 230, 216);font-family:Montserrat, sans-serif;font-size:14px;font-style:normal;font-variant-ligatures:normal;font-variant-caps:normal;font-weight:400;letter-spacing:normal;orphans:2;text-align:start;text-indent:0px;text-transform:none;widows:2;word-spacing:0px;-webkit-text-stroke-width:0px;white-space:normal;background-color:rgba(24, 22, 46, 0.376);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;display:inline !important;float:none\"> can stack.</span></p>",
"tint": "#ffffff",
"statuses": [
"corrode"
@ -169,6 +173,16 @@
"_stats": {
"compendiumSource": null
},
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"showIcon": 1,
"folder": null,
"_key": "!items.effects!qJaSNTuDfbPVr8Lb.zB95bjSSdVlApQnR"
}
],

View file

@ -42,44 +42,8 @@
"type": "self",
"amount": null
},
"name": "Mark 1 Stress",
"img": "icons/magic/control/silhouette-aura-energy.webp",
"range": "self"
},
"fKY9NcYBwCFwMsgV": {
"type": "effect",
"_id": "fKY9NcYBwCFwMsgV",
"systemPath": "actions",
"description": "<p>You can <strong>mark a Stress</strong> to gain a bonus to your damage roll equal to twice your Strength.</p>",
"chatDisplay": true,
"actionType": "action",
"cost": [
{
"scalable": false,
"key": "stress",
"value": 2,
"step": null,
"consumeOnSuccess": false
}
],
"uses": {
"value": null,
"max": "",
"recovery": null,
"consumeOnSuccess": false
},
"effects": [
{
"_id": "t6SIjQxB6UBUJ98f",
"onSave": false
}
],
"target": {
"type": "self",
"amount": null
},
"name": "Mark 2 Stress",
"img": "icons/magic/control/silhouette-aura-energy.webp",
"name": "Mark Stress",
"img": "icons/skills/wounds/injury-face-impact-orange.webp",
"range": "self"
}
},
@ -94,8 +58,8 @@
"sort": 3400000,
"effects": [
{
"name": "Rage Up (1)",
"img": "systems/daggerheart/assets/icons/domains/domain-card/blade.png",
"name": "Rage Up",
"img": "icons/skills/wounds/injury-face-impact-orange.webp",
"origin": "Compendium.daggerheart.domains.Item.GRL0cvs96vrTDckZ",
"transfer": false,
"_id": "bq1MhcmoP6Wo5CXF",
@ -106,33 +70,38 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
},
"changes": [
{
"key": "system.bonuses.damage.magical.bonus",
"mode": 2,
"value": "2*@system.traits.strength.value",
"priority": 21
"type": "add",
"value": "2*@system.traits.strength.value*@stacks",
"priority": 21,
"phase": "initial"
},
{
"key": "system.bonuses.damage.physical.bonus",
"mode": 2,
"value": "2*@system.traits.strength.value",
"priority": 21
"type": "add",
"value": "2*@system.traits.strength.value*@stacks",
"priority": 21,
"phase": "initial"
}
],
"stacking": {
"max": 2
},
"duration": {
"type": ""
}
},
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "",
"description": "<p>For your next attack you have a bonus to your damage roll equal to twice your Strength.</p>",
"tint": "#ffffff",
"statuses": [],
"sort": 0,
@ -140,6 +109,16 @@
"_stats": {
"compendiumSource": null
},
"start": {
"time": 0,
"combat": null,
"combatant": null,
"initiative": null,
"round": null,
"turn": null
},
"showIcon": 1,
"folder": null,
"_key": "!items.effects!GRL0cvs96vrTDckZ.bq1MhcmoP6Wo5CXF"
},
{
@ -155,31 +134,28 @@
"type": "withinRange",
"target": "hostile",
"range": "melee"
}
},
"changes": [
{
"key": "system.bonuses.damage.magical.bonus",
"mode": 2,
"value": "4*@system.traits.strength.value",
"priority": 21
"priority": 21,
"type": "add"
},
{
"key": "system.bonuses.damage.physical.bonus",
"mode": 2,
"value": "4*@system.traits.strength.value",
"priority": 21
"priority": 21,
"type": "add"
}
],
]
},
"disabled": false,
"duration": {
"startTime": null,
"combat": null,
"seconds": null,
"rounds": null,
"turns": null,
"startRound": null,
"startTurn": null
"value": null,
"units": "seconds",
"expiry": null,
"expired": false
},
"description": "",
"tint": "#ffffff",
@ -189,6 +165,9 @@
"_stats": {
"compendiumSource": null
},
"start": null,
"showIcon": 1,
"folder": null,
"_key": "!items.effects!GRL0cvs96vrTDckZ.t6SIjQxB6UBUJ98f"
}
],

View file

@ -293,6 +293,20 @@
}
}
&.optional,
&.one-column.optional {
padding-top: 0;
padding-bottom: 4px;
min-height: auto;
row-gap: 0;
legend {
display: flex;
align-items: center;
padding-left: 3px;
}
}
.list-w-img {
padding: 5px;
label {
@ -469,6 +483,10 @@
&.even {
grid-template-columns: 1fr 1fr;
}
&.full-width {
width: 100%;
}
}
.three-columns {

View file

@ -33,16 +33,6 @@
}
.armor-change-container {
padding-top: 0;
padding-bottom: 4px;
row-gap: 0;
legend {
display: flex;
align-items: center;
padding-left: 3px;
}
header {
padding: 0;
left: -0.25rem; // TODO: Find why this header is offset 0.25rem to the right so this can be removed.

View file

@ -35,6 +35,21 @@
color: @golden;
filter: drop-shadow(0 0 3px black);
}
.stacking-value {
position: absolute;
top: 4px;
right: 4px;
font-size: 16px;
font-weight: bold;
font-variant-numeric: tabular-nums;
color: @golden;
background: black;
padding: 1px;
border-radius: 6px;
border: 1px solid @golden;
pointer-events: none;
}
}
}
}

View file

@ -6,6 +6,7 @@
.daggerheart.dh-style.tooltip {
display: flex;
flex-direction: column;
align-items: start;
text-align: start;
width: 100%;
gap: 5px;
@ -13,6 +14,7 @@
border-radius: 3px;
.tooltip-header {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
@ -35,12 +37,48 @@
}
}
.effect-stacks-outer-container {
display: flex;
flex-direction: column;
gap: 4px;
width: 100%;
.effect-stacks-title {
font-size: var(--font-size-20);
font-weight: bold;
text-align: center;
}
.effect-stacks-container {
display: flex;
justify-content: space-between;
.effect-stacks-inner-container {
display: flex;
flex-direction: column;
gap: 2px;
.effect-stack-title {
font-weight: bold;
}
}
}
}
.close-hints {
margin-top: 0.5rem;
display: flex;
flex-direction: column;
gap: 4px;
.close-hint {
border-radius: 3px;
padding: 3px;
background: @rustic-brown-80;
color: @golden;
font-size: 12px;
margin: 0;
}
}
.duration-container {

View file

@ -14,8 +14,12 @@
{{/each}}
</ol>
<fieldset class="armor-change-container">
<legend>{{localize "DAGGERHEART.GENERAL.armor"}} <input type="checkbox" class="armor-change-checkbox" data-index="{{typedChanges.armor.index}}" {{checked typedChanges.armor}} /></legend>
<fieldset class="armor-change-container optional">
<legend>
{{localize "DAGGERHEART.GENERAL.armor"}}
<input type="checkbox" class="armor-change-checkbox" data-index="{{typedChanges.armor.index}}" {{checked typedChanges.armor}} />
</legend>
{{#if typedChanges.armor}}
{{> "systems/daggerheart/templates/sheets/activeEffect/typeChanges/armorChange.hbs" typedChanges.armor fields=@root.systemFields.changes.element.types.armor.fields}}
{{/if}}

View file

@ -1,4 +1,18 @@
<section class="tab{{#if tab.active}} active{{/if}}" data-group="{{tab.group}}" data-tab="{{tab.id}}">
<fieldset class="one-column optional">
<legend>
{{localize "DAGGERHEART.ACTIVEEFFECT.Config.stacking.title"}}
<input type="checkbox" class="stacking-change-checkbox" {{checked source.system.stacking}} />
</legend>
{{#if source.system.stacking}}
<div class="two-columns even full-width">
{{formGroup systemFields.stacking.fields.value value=source.system.stacking.value localize=true }}
{{formGroup systemFields.stacking.fields.max value=source.system.stacking.max localize=true }}
</div>
{{/if}}
</fieldset>
<fieldset class="one-column">
<legend>{{localize "DAGGERHEART.ACTIVEEFFECT.Config.rangeDependence.title"}}</legend>

View file

@ -8,6 +8,9 @@
<a {{#if effect.condition}}disabled{{/if}}>
<img src="{{effect.img}}" />
</a>
{{#if (gt effect.system.stacking.value 1)}}
<span class="stacking-value">{{effect.system.stacking.value}}</span>
{{/if}}
{{#if effect.condition}}<i class="effect-locked fa-solid fa-lock"></i>{{/if}}
</span>
{{/each}}

View file

@ -17,7 +17,6 @@
{{/if}}
{{#if effect.system.duration.type}}
<div class="duration-container">
<div class="duration-inner-container">
<span>{{localize "EFFECT.DURATION.Label"}}:</span>
@ -26,9 +25,42 @@
</div>
{{/if}}
{{#if effect.system.stacking}}
<div class="effect-stacks-outer-container">
<div class="effect-stacks-title">{{localize "Stacks"}}</div>
<div class="effect-stacks-container">
<div class="effect-stacks-inner-container">
<span class="effect-stack-title">{{localize "Current"}}</span>
<span>{{effect.system.stacking.value}}</span>
</div>
<div class="effect-stacks-inner-container">
<span class="effect-stack-title">{{localize "Max"}}</span>
<span>{{effect.system.stacking.max}}</span>
</div>
</div>
</div>
{{/if}}
{{#unless effect.isLockedCondition}}
<div class="close-hints">
{{#if effect.system.stacking}}
<p class="close-hint">
<i class="fa-solid fa-computer-mouse"></i> {{localize "DAGGERHEART.UI.EffectsDisplay.increaseStacks"}}
</p>
{{#if (gt effect.system.stacking.value 1)}}
<p class="close-hint">
<i class="fa-solid fa-computer-mouse"></i> {{localize "DAGGERHEART.UI.EffectsDisplay.decreaseStacks"}}
</p>
{{else}}
<p class="close-hint">
<i class="fa-solid fa-computer-mouse"></i> {{localize "DAGGERHEART.UI.EffectsDisplay.removeThing" thing=(localize "DAGGERHEART.GENERAL.Effect.single")}}
</p>
{{/if}}
{{else}}
<p class="close-hint">
<i class="fa-solid fa-computer-mouse"></i> {{localize "DAGGERHEART.UI.EffectsDisplay.removeThing" thing=(localize "DAGGERHEART.GENERAL.Effect.single")}}
</p>
{{/if}}
</div>
{{/unless}}
</div>