Merged with development

This commit is contained in:
WBHarry 2025-10-29 19:13:46 +01:00
commit 6d3e5302eb
62 changed files with 1776 additions and 660 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg"
width="42.0746mm" height="50.8071mm"
viewBox="0 0 159 192">
<path id="Fear"
fill="#808080" stroke="black" stroke-width="1"
d="M 107.14,62.84
C 107.14,62.84 52.70,137.55 52.70,137.55
52.70,137.55 110.33,137.55 110.33,137.55
110.33,137.55 127.99,79.63 127.99,79.63
127.99,79.63 107.14,62.84 107.14,62.84 Z
M 48.07,144.21
C 48.07,144.21 13.61,192.28 13.61,192.28
14.01,191.90 30.68,177.55 47.81,176.06
47.81,176.06 64.48,176.30 64.48,176.30
64.69,176.22 79.31,180.77 79.31,180.77
79.31,180.77 126.04,165.15 126.04,165.15
126.04,165.15 110.91,143.92 110.91,143.92
110.91,143.92 48.07,144.21 48.07,144.21 Z
M 134.24,81.37
C 134.24,81.37 115.74,140.33 115.74,140.33
115.74,140.33 131.41,161.00 131.41,161.00
131.41,161.00 158.98,122.82 158.98,122.82
158.98,122.82 158.91,72.83 158.91,72.83
158.91,72.83 134.24,81.37 134.24,81.37 Z
M 130.43,30.45
C 130.43,30.45 110.41,59.27 110.41,59.27
110.41,59.27 131.77,75.21 131.77,75.21
131.77,75.21 157.51,67.23 157.51,67.23
157.25,67.32 130.42,30.45 130.43,30.45 Z" />
<path id="Hope"
fill="white" stroke="black" stroke-width="1"
d="M 143.84,1.80
C 143.84,1.80 105.59,54.92 105.59,54.92
105.59,54.92 83.04,39.72 83.04,39.72
83.04,39.72 82.87,13.08 82.87,13.08
82.23,14.32 111.40,18.59 112.13,17.16
113.68,18.32 145.11,2.74 143.84,1.80 Z
M 76.01,13.40
C 76.01,13.40 76.01,40.05 76.01,40.05
76.01,40.05 25.99,75.35 25.99,75.35
25.99,75.35 1.96,68.16 1.96,68.16
1.96,68.16 30.08,27.62 30.08,27.62
30.08,27.62 76.01,13.40 76.01,13.40 Z
M 79.11,44.62
C 79.11,44.62 101.34,60.81 101.34,60.81
101.34,60.81 48.38,133.54 48.38,133.54
48.38,133.54 30.57,80.09 30.57,80.09
30.57,80.09 79.11,44.62 79.11,44.62 Z
M 24.36,81.07
C 24.36,81.07 43.64,139.59 43.64,139.59
43.64,139.59 28.11,162.31 28.11,162.31
28.11,162.31 0.33,123.90 0.33,123.90
0.33,123.90 0.33,74.05 0.33,74.05
0.33,74.05 24.36,81.07 24.36,81.07 Z" />
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -8,10 +8,8 @@ import * as fields from './module/data/fields/_module.mjs';
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs'; import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs';
import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs'; import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs';
import { NarrativeCountdowns } from './module/applications/ui/countdowns.mjs';
import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll } from './module/dice/_module.mjs'; import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll } from './module/dice/_module.mjs';
import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs'; import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs';
import { registerCountdownHooks } from './module/data/countdowns.mjs';
import { import {
handlebarsRegistration, handlebarsRegistration,
runMigrations, runMigrations,
@ -144,6 +142,7 @@ Hooks.once('init', () => {
CONFIG.Token.rulerClass = placeables.DhTokenRuler; CONFIG.Token.rulerClass = placeables.DhTokenRuler;
CONFIG.ui.resources = applications.ui.DhFearTracker; CONFIG.ui.resources = applications.ui.DhFearTracker;
CONFIG.ui.countdowns = applications.ui.DhCountdowns;
CONFIG.ux.ContextMenu = applications.ux.DHContextMenu; CONFIG.ux.ContextMenu = applications.ux.DHContextMenu;
CONFIG.ux.TooltipManager = documents.DhTooltipManager; CONFIG.ux.TooltipManager = documents.DhTooltipManager;
@ -170,10 +169,12 @@ Hooks.on('ready', async () => {
if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear !== 'hide') if (game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance).displayFear !== 'hide')
ui.resources.render({ force: true }); ui.resources.render({ force: true });
ui.countdowns = new CONFIG.ui.countdowns();
ui.countdowns.render({ force: true });
if (!(ui.compendiumBrowser instanceof applications.ui.ItemBrowser)) if (!(ui.compendiumBrowser instanceof applications.ui.ItemBrowser))
ui.compendiumBrowser = new applications.ui.ItemBrowser(); ui.compendiumBrowser = new applications.ui.ItemBrowser();
registerCountdownHooks();
socketRegistration.registerSocketHooks(); socketRegistration.registerSocketHooks();
registerRollDiceHooks(); registerRollDiceHooks();
socketRegistration.registerUserQueries(); socketRegistration.registerUserQueries();
@ -246,29 +247,6 @@ Hooks.on('chatMessage', (_, message) => {
} }
}); });
Hooks.on('renderJournalDirectory', async (tab, html, _, options) => {
if (tab.id === 'journal') {
if (options.parts && !options.parts.includes('footer')) return;
const buttons = tab.element.querySelector('.directory-footer.action-buttons');
const title = game.i18n.format('DAGGERHEART.APPLICATIONS.Countdown.title', {
type: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.types.narrative')
});
buttons.insertAdjacentHTML(
'afterbegin',
`
<button id="narrative-countdown-button">
<i class="fa-solid fa-stopwatch"></i>
<span style="font-weight: 400; font-family: var(--font-sans);">${title}</span>
</button>`
);
buttons.querySelector('#narrative-countdown-button').onclick = async () => {
new NarrativeCountdowns().open();
};
}
});
Hooks.on('moveToken', async (movedToken, data) => { Hooks.on('moveToken', async (movedToken, data) => {
const effectsAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).effects; const effectsAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).effects;
if (!effectsAutomation.rangeDependent) return; if (!effectsAutomation.rangeDependent) return;

View file

@ -332,7 +332,8 @@
"label": { "label": "Label", "hint": "Used for custom" }, "label": { "label": "Label", "hint": "Used for custom" },
"value": { "label": "Value" } "value": { "label": "Value" }
} }
} },
"type": { "label": "Countdown Type" }
} }
} }
}, },
@ -347,6 +348,26 @@
"encounter": "Encounter" "encounter": "Encounter"
} }
}, },
"CountdownEdit": {
"title": "Countdown Edit",
"viewTitle": "Countdowns",
"editTitle": "Edit Countdowns",
"newCountdown": "New Countdown",
"removeCountdownTitle": "Remove Countdown",
"removeCountdownText": "Are you sure you want to remove the countdown: {name}?",
"current": "Current",
"max": "Max",
"currentCountdownValue": "Current: {value}",
"currentCountdownMax": "Max: {value}",
"category": "Category",
"type": "Type",
"defaultOwnershipTooltip": "The default player ownership of countdowns",
"hideNewCountdowns": "Hide New Countdowns"
},
"DaggerheartMenu": {
"title": "GM Tools",
"countdowns": "Edit Countdowns"
},
"DeleteConfirmation": { "DeleteConfirmation": {
"title": "Delete {type} - {name}", "title": "Delete {type} - {name}",
"text": "Are you sure you want to delete {name}?" "text": "Are you sure you want to delete {name}?"
@ -1898,6 +1919,10 @@
} }
} }
}, },
"SpotlightRequests": {
"singular": "Spotlight Request",
"plural": "Spotlight Requests"
},
"Tabs": { "Tabs": {
"details": "Details", "details": "Details",
"attack": "Attack", "attack": "Attack",
@ -2046,6 +2071,7 @@
"range": "Range", "range": "Range",
"reactionRoll": "Reaction Roll", "reactionRoll": "Reaction Roll",
"recovery": "Recovery", "recovery": "Recovery",
"refresh": "Refresh",
"reroll": "Reroll", "reroll": "Reroll",
"rerollThing": "Reroll {thing}", "rerollThing": "Reroll {thing}",
"resource": "Resource", "resource": "Resource",
@ -2298,6 +2324,9 @@
"label": "Apply Effects", "label": "Apply Effects",
"hint": "Automatically apply effects. Targets must be selected before the action is made and Reaction Roll Automation must be different than Never. Bypass users permissions." "hint": "Automatically apply effects. Targets must be selected before the action is made and Reaction Roll Automation must be different than Never. Bypass users permissions."
} }
},
"summaryMessages": {
"label": "Summary Messages"
} }
}, },
"defeated": { "defeated": {
@ -2419,6 +2448,7 @@
"action": { "action": {
"title": "Action" "title": "Action"
}, },
"appliedTo": "Applied To",
"applyEffect": { "applyEffect": {
"title": "Apply Effects - {name}" "title": "Apply Effects - {name}"
}, },
@ -2428,6 +2458,11 @@
"rollHealing": "Roll Healing", "rollHealing": "Roll Healing",
"applyEffect": "Apply Effects" "applyEffect": "Apply Effects"
}, },
"clearResource": "Clear {quantity} {resource}",
"damageSummary": {
"title": "Damage Applied",
"healingTitle": "Healing Applied"
},
"damageRoll": { "damageRoll": {
"title": "Damage - {damage}", "title": "Damage - {damage}",
"dealDamageToTargets": "Damage Hit Targets", "dealDamageToTargets": "Damage Hit Targets",
@ -2449,12 +2484,16 @@
"dualityRoll": { "dualityRoll": {
"abilityCheckTitle": "{ability} Check" "abilityCheckTitle": "{ability} Check"
}, },
"effectSummary": {
"title": "Effects Applied"
},
"featureTitle": "Class Feature", "featureTitle": "Class Feature",
"healingRoll": { "healingRoll": {
"title": "Heal - {damage}", "title": "Heal - {damage}",
"heal": "Heal", "heal": "Heal",
"applyHealing": "Apply Healing" "applyHealing": "Apply Healing"
}, },
"markResource": "Mark {quantity} {resource}",
"refreshMessage": { "refreshMessage": {
"title": "Feature Refresh", "title": "Feature Refresh",
"header": "Refreshed" "header": "Refreshed"
@ -2475,6 +2514,11 @@
"rerollDamage": "Reroll Damage", "rerollDamage": "Reroll Damage",
"assignTagRoll": "Assign as Tag Roll" "assignTagRoll": "Assign as Tag Roll"
}, },
"Countdowns": {
"title": "Countdowns",
"toggleIconMode": "Toggle Icon Only",
"noPlayerAccess": "This countdown isn't visible to any players"
},
"ItemBrowser": { "ItemBrowser": {
"title": "Daggerheart Compendium Browser", "title": "Daggerheart Compendium Browser",
"hint": "Select a Folder in sidebar to start browsing through the compendium", "hint": "Select a Folder in sidebar to start browsing through the compendium",
@ -2584,7 +2628,8 @@
"subclassesAlreadyPresent": "You already have a class and multiclass subclass", "subclassesAlreadyPresent": "You already have a class and multiclass subclass",
"noDiceSystem": "Your selected dice {system} does not have a {faces} dice", "noDiceSystem": "Your selected dice {system} does not have a {faces} dice",
"gmMenuRefresh": "You refreshed all actions and resources {types}", "gmMenuRefresh": "You refreshed all actions and resources {types}",
"subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class." "subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class.",
"gmRequired": "This action requires an online GM"
}, },
"Sidebar": { "Sidebar": {
"daggerheartMenu": { "daggerheartMenu": {

View file

@ -83,9 +83,9 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
static PARTS = { static PARTS = {
tabs: { template: 'systems/daggerheart/templates/characterCreation/tabs.hbs' }, tabs: { template: 'systems/daggerheart/templates/characterCreation/tabs.hbs' },
class: { template: 'systems/daggerheart/templates/characterCreation/tabs/class.hbs' },
ancestry: { template: 'systems/daggerheart/templates/characterCreation/tabs/ancestry.hbs' }, ancestry: { template: 'systems/daggerheart/templates/characterCreation/tabs/ancestry.hbs' },
community: { template: 'systems/daggerheart/templates/characterCreation/tabs/community.hbs' }, community: { template: 'systems/daggerheart/templates/characterCreation/tabs/community.hbs' },
class: { template: 'systems/daggerheart/templates/characterCreation/tabs/class.hbs' },
traits: { template: 'systems/daggerheart/templates/characterCreation/tabs/traits.hbs' }, traits: { template: 'systems/daggerheart/templates/characterCreation/tabs/traits.hbs' },
experience: { template: 'systems/daggerheart/templates/characterCreation/tabs/experience.hbs' }, experience: { template: 'systems/daggerheart/templates/characterCreation/tabs/experience.hbs' },
domainCards: { template: 'systems/daggerheart/templates/characterCreation/tabs/domainCards.hbs' }, domainCards: { template: 'systems/daggerheart/templates/characterCreation/tabs/domainCards.hbs' },
@ -95,6 +95,13 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
}; };
static TABS = { static TABS = {
class: {
active: false,
cssClass: '',
group: 'setup',
id: 'class',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.class'
},
ancestry: { ancestry: {
active: true, active: true,
cssClass: '', cssClass: '',
@ -109,13 +116,6 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
id: 'community', id: 'community',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.community' label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.community'
}, },
class: {
active: false,
cssClass: '',
group: 'setup',
id: 'class',
label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.tabs.class'
},
traits: { traits: {
active: false, active: false,
cssClass: '', cssClass: '',
@ -156,10 +156,10 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
v.cssClass = v.active ? 'active' : ''; v.cssClass = v.active ? 'active' : '';
switch (v.id) { switch (v.id) {
case 'community': case 'ancestry':
v.disabled = this.setup.visibility < 2; v.disabled = this.setup.visibility < 2;
break; break;
case 'class': case 'community':
v.disabled = this.setup.visibility < 3; v.disabled = this.setup.visibility < 3;
break; break;
case 'traits': case 'traits':
@ -192,7 +192,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
} }
async _prepareContext(_options) { async _prepareContext(_options) {
this.tabGroups.setup = this.tabGroups.setup ?? 'ancestry'; this.tabGroups.setup = this.tabGroups.setup ?? 'class';
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.tabs = this._getTabs(this.constructor.TABS); context.tabs = this._getTabs(this.constructor.TABS);
@ -266,13 +266,13 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
context.isLastTab = this.tabGroups.setup === 'equipment'; context.isLastTab = this.tabGroups.setup === 'equipment';
switch (this.tabGroups.setup) { switch (this.tabGroups.setup) {
case null: case null:
case 'ancestry': case 'class':
context.nextDisabled = this.setup.visibility === 1; context.nextDisabled = this.setup.visibility === 1;
break; break;
case 'community': case 'ancestry':
context.nextDisabled = this.setup.visibility === 2; context.nextDisabled = this.setup.visibility === 2;
break; break;
case 'class': case 'community':
context.nextDisabled = this.setup.visibility === 3; context.nextDisabled = this.setup.visibility === 3;
break; break;
case 'traits': case 'traits':
@ -363,11 +363,11 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
case 4: case 4:
return this.getNrSelectedTrait() === 6 ? 5 : 4; return this.getNrSelectedTrait() === 6 ? 5 : 4;
case 3: case 3:
return this.setup.class.uuid && this.setup.subclass.uuid ? 4 : 3; return this.setup.community.uuid ? 4 : 3;
case 2: case 2:
return this.setup.community.uuid ? 3 : 2; return this.setup.primaryAncestry.uuid ? 3 : 2;
case 1: case 1:
return this.setup.primaryAncestry.uuid ? 2 : 1; return this.setup.class.uuid && this.setup.subclass.uuid ? 2 : 1;
} }
} }
@ -473,10 +473,10 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
static setupGoNext() { static setupGoNext() {
switch (this.setup.visibility) { switch (this.setup.visibility) {
case 2: case 2:
this.tabGroups.setup = 'community'; this.tabGroups.setup = 'ancestry';
break; break;
case 3: case 3:
this.tabGroups.setup = 'class'; this.tabGroups.setup = 'community';
break; break;
case 4: case 4:
this.tabGroups.setup = 'traits'; this.tabGroups.setup = 'traits';

View file

@ -16,7 +16,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.action = this.action =
config.data.attack?._id == config.source.action config.data.attack?._id == config.source.action
? config.data.attack ? config.data.attack
: this.item.system.actions.get(config.source.action); : this.item.system.actionsList?.find(a => a.id === config.source.action);
} }
} }

View file

@ -1,18 +1,20 @@
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class OwnershipSelection extends HandlebarsApplicationMixin(ApplicationV2) { export default class OwnershipSelection extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(resolve, reject, name, ownership) { constructor(name, ownership, defaultOwnership) {
super({}); super({});
this.resolve = resolve;
this.reject = reject;
this.name = name; this.name = name;
this.ownership = ownership; this.ownership = foundry.utils.deepClone(ownership);
this.defaultOwnership = defaultOwnership;
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
tag: 'form', tag: 'form',
classes: ['daggerheart', 'views', 'ownership-selection'], classes: ['daggerheart', 'views', 'dialog', 'dh-style', 'ownership-selection'],
window: {
icon: 'fa-solid fa-users'
},
position: { position: {
width: 600, width: 600,
height: 'auto' height: 'auto'
@ -30,43 +32,48 @@ export default class OwnershipSelection extends HandlebarsApplicationMixin(Appli
return game.i18n.format('DAGGERHEART.APPLICATIONS.OwnershipSelection.title', { name: this.name }); return game.i18n.format('DAGGERHEART.APPLICATIONS.OwnershipSelection.title', { name: this.name });
} }
getOwnershipData(id) {
return this.ownership[id] ?? CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT;
}
async _prepareContext(_options) { async _prepareContext(_options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(_options);
context.ownershipOptions = Object.keys(CONST.DOCUMENT_OWNERSHIP_LEVELS).map(level => ({ context.ownershipDefaultOptions = CONFIG.DH.GENERAL.basicOwnershiplevels;
value: CONST.DOCUMENT_OWNERSHIP_LEVELS[level], context.ownershipOptions = CONFIG.DH.GENERAL.simpleOwnershiplevels;
label: game.i18n.localize(`OWNERSHIP.${level}`) context.defaultOwnership = this.defaultOwnership;
})); context.ownership = game.users.reduce((acc, user) => {
context.ownership = { if (!user.isGM) {
default: this.ownership.default, acc[user.id] = {
players: Object.keys(this.ownership.players).reduce((acc, x) => { ...user,
const user = game.users.get(x); img: user.character?.img ?? 'icons/svg/cowled.svg',
if (!user.isGM) { ownership: this.getOwnershipData(user.id)
acc[x] = { };
img: user.character?.img ?? 'icons/svg/cowled.svg', }
name: user.name,
ownership: this.ownership.players[x].value
};
}
return acc; return acc;
}, {}) }, {});
};
return context; return context;
} }
static async updateData(event, _, formData) { static async updateData(event, _, formData) {
const { ownership } = foundry.utils.expandObject(formData.object); const data = foundry.utils.expandObject(formData.object);
this.close(data);
this.resolve(ownership);
this.close(true);
} }
async close(fromSave) { async close(data) {
if (!fromSave) { if (data) {
this.reject(); this.saveData = data;
} }
await super.close(); await super.close();
} }
static async configure(name, ownership, defaultOwnership) {
return new Promise(resolve => {
const app = new this(name, ownership, defaultOwnership);
app.addEventListener('close', () => resolve(app.saveData), { once: true });
app.render({ force: true });
});
}
} }

View file

@ -99,6 +99,7 @@ export default function DHApplicationMixin(Base) {
deleteDoc: DHSheetV2.#deleteDoc, deleteDoc: DHSheetV2.#deleteDoc,
toChat: DHSheetV2.#toChat, toChat: DHSheetV2.#toChat,
useItem: DHSheetV2.#useItem, useItem: DHSheetV2.#useItem,
viewItem: DHSheetV2.#viewItem,
toggleEffect: DHSheetV2.#toggleEffect, toggleEffect: DHSheetV2.#toggleEffect,
toggleExtended: DHSheetV2.#toggleExtended, toggleExtended: DHSheetV2.#toggleExtended,
addNewItem: DHSheetV2.#addNewItem, addNewItem: DHSheetV2.#addNewItem,
@ -203,11 +204,19 @@ export default function DHApplicationMixin(Base) {
this.relatedDocs.filter(doc => doc).map(doc => delete doc.apps[this.id]); this.relatedDocs.filter(doc => doc).map(doc => delete doc.apps[this.id]);
} }
/** @inheritdoc */
async _renderHTML(context, options) {
const rendered = await super._renderHTML(context, options);
for (const result of Object.values(rendered)) {
await this.#prepareInventoryDescription(result);
}
return rendered;
}
/**@inheritdoc */ /**@inheritdoc */
async _onRender(context, options) { async _onRender(context, options) {
await super._onRender(context, options); await super._onRender(context, options);
this._createTagifyElements(this.options.tagifyConfigs); this._createTagifyElements(this.options.tagifyConfigs);
await this.#prepareInventoryDescription(context);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -215,8 +224,8 @@ export default function DHApplicationMixin(Base) {
/* -------------------------------------------- */ /* -------------------------------------------- */
/**@inheritdoc */ /**@inheritdoc */
_syncPartState(partId, newElement, priorElement, state) { _preSyncPartState(partId, newElement, priorElement, state) {
super._syncPartState(partId, newElement, priorElement, state); super._preSyncPartState(partId, newElement, priorElement, state);
for (const el of priorElement.querySelectorAll('.extensible.extended')) { for (const el of priorElement.querySelectorAll('.extensible.extended')) {
const { actionId, itemUuid } = el.parentElement.dataset; const { actionId, itemUuid } = el.parentElement.dataset;
const selector = `${actionId ? `[data-action-id="${actionId}"]` : `[data-item-uuid="${itemUuid}"]`} .extensible`; const selector = `${actionId ? `[data-action-id="${actionId}"]` : `[data-item-uuid="${itemUuid}"]`} .extensible`;
@ -496,11 +505,12 @@ export default function DHApplicationMixin(Base) {
/** /**
* Prepares and enriches an inventory item or action description for display. * Prepares and enriches an inventory item or action description for display.
* @param {HTMLElement} element the element to enrich the inventory items of
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async #prepareInventoryDescription(context) { async #prepareInventoryDescription(element) {
// Get all inventory item elements with a data-item-uuid attribute // Get all inventory item elements with a data-item-uuid attribute
const inventoryItems = this.element.querySelectorAll('.inventory-item[data-item-uuid]'); const inventoryItems = element.querySelectorAll('.inventory-item[data-item-uuid]');
for (const el of inventoryItems) { for (const el of inventoryItems) {
// Get the doc uuid from the element // Get the doc uuid from the element
const { itemUuid } = el?.dataset || {}; const { itemUuid } = el?.dataset || {};
@ -701,7 +711,7 @@ export default function DHApplicationMixin(Base) {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #toChat(_event, target) { static async #toChat(_event, target) {
let doc = await getDocFromElement(target); const doc = await getDocFromElement(target);
return doc.toChat(doc.uuid); return doc.toChat(doc.uuid);
} }
@ -710,10 +720,19 @@ export default function DHApplicationMixin(Base) {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #useItem(event, target) { static async #useItem(event, target) {
let doc = await getDocFromElement(target); const doc = await getDocFromElement(target);
await doc.use(event); await doc.use(event);
} }
/**
* View an item by opening its sheet
* @type {ApplicationClickAction}
*/
static async #viewItem(_, target) {
const doc = await getDocFromElement(target);
await doc.sheet.render({ force: true });
}
/** /**
* Toggle a ActiveEffect * Toggle a ActiveEffect
* @type {ApplicationClickAction} * @type {ApplicationClickAction}

View file

@ -66,7 +66,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
/**@inheritdoc */ /**@inheritdoc */
async _prepareContext(options) { async _prepareContext(options) {
const context = super._prepareContext(options); const context = await super._prepareContext(options);
context.showAttribution = !game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance) context.showAttribution = !game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
.hideAttribution; .hideAttribution;

View file

@ -1,10 +1,49 @@
export default class DhSidebar extends Sidebar { export default class DhSidebar extends foundry.applications.sidebar.Sidebar {
/** @override */ /** @override */
static TABS = { static TABS = {
...super.TABS, chat: {
documentName: 'ChatMessage'
},
combat: {
documentName: 'Combat'
},
scenes: {
documentName: 'Scene',
gmOnly: true
},
actors: {
documentName: 'Actor'
},
items: {
documentName: 'Item'
},
journal: {
documentName: 'JournalEntry',
tooltip: 'SIDEBAR.TabJournal'
},
tables: {
documentName: 'RollTable'
},
cards: {
documentName: 'Cards'
},
macros: {
documentName: 'Macro'
},
playlists: {
documentName: 'Playlist'
},
compendium: {
tooltip: 'SIDEBAR.TabCompendium',
icon: 'fa-solid fa-book-atlas'
},
daggerheartMenu: { daggerheartMenu: {
tooltip: 'DAGGERHEART.UI.Sidebar.daggerheartMenu.title', tooltip: 'DAGGERHEART.UI.Sidebar.daggerheartMenu.title',
img: 'systems/daggerheart/assets/logos/FoundryBorneLogoWhite.svg' img: 'systems/daggerheart/assets/logos/FoundryBorneLogoWhite.svg'
},
settings: {
tooltip: 'SIDEBAR.TabSettings',
icon: 'fa-solid fa-gears'
} }
}; };

View file

@ -28,8 +28,9 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
title: 'SIDEBAR.TabSettings' title: 'SIDEBAR.TabSettings'
}, },
actions: { actions: {
selectRefreshable: DaggerheartMenu.selectRefreshable, selectRefreshable: DaggerheartMenu.#selectRefreshable,
refreshActors: DaggerheartMenu.refreshActors refreshActors: DaggerheartMenu.#refreshActors,
editCountdowns: DaggerheartMenu.#editCountdowns
} }
}; };
@ -123,13 +124,13 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
/* Application Clicks Actions */ /* Application Clicks Actions */
/* -------------------------------------------- */ /* -------------------------------------------- */
static async selectRefreshable(_event, button) { static async #selectRefreshable(_event, button) {
const { type } = button.dataset; const { type } = button.dataset;
this.refreshSelections[type].selected = !this.refreshSelections[type].selected; this.refreshSelections[type].selected = !this.refreshSelections[type].selected;
this.render(); this.render();
} }
static async refreshActors() { static async #refreshActors() {
const refreshKeys = Object.keys(this.refreshSelections).filter(key => this.refreshSelections[key].selected); const refreshKeys = Object.keys(this.refreshSelections).filter(key => this.refreshSelections[key].selected);
await this.getRefreshables(refreshKeys); await this.getRefreshables(refreshKeys);
const types = refreshKeys.map(x => this.refreshSelections[x].label).join(', '); const types = refreshKeys.map(x => this.refreshSelections[x].label).join(', ');
@ -157,4 +158,8 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract
this.render(); this.render();
} }
static async #editCountdowns() {
new game.system.api.applications.ui.CountdownEdit().render(true);
}
} }

View file

@ -1,6 +1,7 @@
export { default as CountdownEdit } from './countdownEdit.mjs';
export { default as DhCountdowns } from './countdowns.mjs';
export { default as DhChatLog } from './chatLog.mjs'; export { default as DhChatLog } from './chatLog.mjs';
export { default as DhCombatTracker } from './combatTracker.mjs'; export { default as DhCombatTracker } from './combatTracker.mjs';
export * as DhCountdowns from './countdowns.mjs';
export { default as DhFearTracker } from './fearTracker.mjs'; export { default as DhFearTracker } from './fearTracker.mjs';
export { default as DhHotbar } from './hotbar.mjs'; export { default as DhHotbar } from './hotbar.mjs';
export { ItemBrowser } from './itemBrowser.mjs'; export { ItemBrowser } from './itemBrowser.mjs';

View file

@ -1,5 +1,3 @@
import { EncounterCountdowns } from '../ui/countdowns.mjs';
export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
actions: { actions: {
@ -36,10 +34,21 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
const adversaries = context.turns?.filter(x => x.isNPC) ?? []; const adversaries = context.turns?.filter(x => x.isNPC) ?? [];
const characters = context.turns?.filter(x => !x.isNPC) ?? []; const characters = context.turns?.filter(x => !x.isNPC) ?? [];
const spotlightRequests = characters
?.filter(x => !x.isNPC)
.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, { Object.assign(context, {
actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens, actionTokens: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).actionTokens,
adversaries, adversaries,
characters characters: characters?.filter(x => !x.isNPC).filter(x => x.system.spotlight.requestOrderIndex == 0),
spotlightRequests
}); });
} }
@ -114,7 +123,8 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
async setCombatantSpotlight(combatantId) { async setCombatantSpotlight(combatantId) {
const update = { const update = {
system: { system: {
'spotlight.requesting': false 'spotlight.requesting': false,
'spotlight.requestOrderIndex': 0
} }
}; };
const combatant = this.viewed.combatants.get(combatantId); const combatant = this.viewed.combatants.get(combatantId);
@ -142,11 +152,15 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
} }
static async requestSpotlight(_, target) { static async requestSpotlight(_, target) {
const characters = this.viewed.turns?.filter(x => !x.isNPC) ?? [];
const orderValues = characters.map(character => character.system.spotlight.requestOrderIndex);
const maxRequestIndex = Math.max(...orderValues);
const { combatantId } = target.closest('[data-combatant-id]')?.dataset ?? {}; const { combatantId } = target.closest('[data-combatant-id]')?.dataset ?? {};
const combatant = this.viewed.combatants.get(combatantId); const combatant = this.viewed.combatants.get(combatantId);
await combatant.update({ await combatant.update({
'system.spotlight': { 'system.spotlight': {
requesting: !combatant.system.spotlight.requesting requesting: !combatant.system.spotlight.requesting,
requestOrderIndex: !combatant.system.spotlight.requesting ? maxRequestIndex + 1 : 0
} }
}); });
@ -168,8 +182,4 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
await combatant.update({ 'system.actionTokens': newIndex }); await combatant.update({ 'system.actionTokens': newIndex });
this.render(); this.render();
} }
static openCountdowns() {
new EncounterCountdowns().open();
}
} }

View file

@ -0,0 +1,199 @@
import { DhCountdown } from '../../data/countdowns.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class CountdownEdit extends HandlebarsApplicationMixin(ApplicationV2) {
constructor() {
super();
this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
this.editingCountdowns = new Set();
this.currentEditCountdown = null;
this.hideNewCountdowns = false;
}
static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'dialog', 'dh-style', 'countdown-edit'],
tag: 'form',
position: { width: 600 },
window: {
title: 'DAGGERHEART.APPLICATIONS.CountdownEdit.title',
icon: 'fa-solid fa-clock-rotate-left'
},
actions: {
addCountdown: CountdownEdit.#addCountdown,
toggleCountdownEdit: CountdownEdit.#toggleCountdownEdit,
editCountdownImage: CountdownEdit.#editCountdownImage,
editCountdownOwnership: CountdownEdit.#editCountdownOwnership,
removeCountdown: CountdownEdit.#removeCountdown
},
form: { handler: this.updateData, submitOnChange: true }
};
static PARTS = {
countdowns: {
template: 'systems/daggerheart/templates/ui/countdown-edit.hbs',
scrollable: ['.expanded-view', '.edit-content']
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.isGM = game.user.isGM;
context.ownershipDefaultOptions = CONFIG.DH.GENERAL.basicOwnershiplevels;
context.defaultOwnership = this.data.defaultOwnership;
context.countdownBaseTypes = CONFIG.DH.GENERAL.countdownBaseTypes;
context.countdownTypes = CONFIG.DH.GENERAL.countdownTypes;
context.hideNewCountdowns = this.hideNewCountdowns;
context.countdowns = Object.keys(this.data.countdowns).reduce((acc, key) => {
const countdown = this.data.countdowns[key];
acc[key] = {
...countdown,
typeName: game.i18n.localize(CONFIG.DH.GENERAL.countdownBaseTypes[countdown.type].name),
progress: {
...countdown.progress,
typeName: game.i18n.localize(CONFIG.DH.GENERAL.countdownTypes[countdown.progress.type].label)
},
editing: this.editingCountdowns.has(key)
};
return acc;
}, {});
return context;
}
/** @override */
async _postRender(_context, _options) {
if (this.currentEditCountdown) {
setTimeout(() => {
const input = this.element.querySelector(
`.countdown-edit-container[data-id="${this.currentEditCountdown}"] input`
);
if (input) {
input.select();
this.currentEditCountdown = null;
}
}, 100);
}
}
canPerformEdit() {
if (game.user.isGM) return true;
if (!game.users.activeGM) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired'));
return false;
}
return true;
}
async updateSetting(update) {
const noGM = !game.users.find(x => x.isGM && x.active);
if (noGM) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired'));
return;
}
await this.data.updateSource(update);
await emitAsGM(GMUpdateEvent.UpdateCountdowns, this.gmSetSetting.bind(this.data), this.data, null, {
refreshType: RefreshType.Countdown
});
this.render();
}
static async updateData(_event, _, formData) {
const { hideNewCountdowns, ...settingsData } = foundry.utils.expandObject(formData.object);
// Sync current and max if max is changing and they were equal before
for (const [id, countdown] of Object.entries(settingsData.countdowns ?? {})) {
const existing = this.data.countdowns[id];
const wasEqual = existing && existing.progress.current === existing.progress.max;
if (wasEqual && countdown.progress.max !== existing.progress.max) {
countdown.progress.current = countdown.progress.max;
} else {
countdown.progress.current = Math.min(countdown.progress.current, countdown.progress.max);
}
}
this.hideNewCountdowns = hideNewCountdowns;
this.updateSetting(settingsData);
}
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 }
});
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
}
static #addCountdown() {
const id = foundry.utils.randomID();
this.editingCountdowns.add(id);
this.currentEditCountdown = id;
this.updateSetting({
[`countdowns.${id}`]: DhCountdown.defaultCountdown(null, this.hideNewCountdowns)
});
}
static #editCountdownImage(_, target) {
const countdown = this.data.countdowns[target.id];
const fp = new foundry.applications.apps.FilePicker.implementation({
current: countdown.img,
type: 'image',
callback: async path => this.updateSetting({ [`countdowns.${target.id}.img`]: path }),
top: this.position.top + 40,
left: this.position.left + 10
});
return fp.browse();
}
static #toggleCountdownEdit(_, button) {
const { countdownId } = button.dataset;
const isEditing = this.editingCountdowns.has(countdownId);
if (isEditing) this.editingCountdowns.delete(countdownId);
else {
this.editingCountdowns.add(countdownId);
this.currentEditCountdown = countdownId;
}
this.render();
}
static async #editCountdownOwnership(_, button) {
const countdown = this.data.countdowns[button.dataset.countdownId];
const data = await game.system.api.applications.dialogs.OwnershipSelection.configure(
countdown.name,
countdown.ownership,
this.data.defaultOwnership
);
if (!data) return;
this.updateSetting({ [`countdowns.${button.dataset.countdownId}`]: data });
}
static async #removeCountdown(event, button) {
const { countdownId } = button.dataset;
if (!event.shiftKey) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.CountdownEdit.removeCountdownTitle')
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.CountdownEdit.removeCountdownText', {
name: this.data.countdowns[countdownId].name
})
});
if (!confirmed) return;
}
if (this.editingCountdowns.has(countdownId)) this.editingCountdowns.delete(countdownId);
this.updateSetting({ [`countdowns.-=${countdownId}`]: null });
}
}

View file

@ -1,355 +1,218 @@
import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import constructHTMLButton from '../../helpers/utils.mjs';
import OwnershipSelection from '../dialogs/ownershipSelection.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) { /**
constructor(basePath) { * A UI element which displays the countdowns in this world.
super({}); *
* @extends ApplicationV2
* @mixes HandlebarsApplication
*/
this.basePath = basePath; export default class DhCountdowns extends HandlebarsApplicationMixin(ApplicationV2) {
} constructor(options = {}) {
super(options);
get title() {
return game.i18n.format('DAGGERHEART.APPLICATIONS.Countdown.title', { this.sidebarCollapsed = true;
type: game.i18n.localize(`DAGGERHEART.APPLICATIONS.Countdown.types.${this.basePath}`) this.setupHooks();
});
} }
/** @inheritDoc */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['daggerheart', 'dh-style', 'countdown'], id: 'countdowns',
tag: 'form', tag: 'div',
position: { width: 740, height: 700 }, classes: ['daggerheart', 'dh-style', 'countdowns', 'faded-ui'],
window: { window: {
icon: 'fa-solid fa-clock-rotate-left',
frame: true, frame: true,
title: 'Countdowns', title: 'DAGGERHEART.UI.Countdowns.title',
resizable: true, positioned: false,
resizable: false,
minimizable: false minimizable: false
}, },
actions: { actions: {
addCountdown: this.addCountdown, toggleViewMode: DhCountdowns.#toggleViewMode,
removeCountdown: this.removeCountdown, decreaseCountdown: (_, target) => this.editCountdown(false, target),
editImage: this.onEditImage, increaseCountdown: (_, target) => this.editCountdown(true, target)
openOwnership: this.openOwnership,
openCountdownOwnership: this.openCountdownOwnership,
toggleSimpleView: this.toggleSimpleView
}, },
form: { handler: this.updateData, submitOnChange: true } position: {
}; width: 400,
height: 222,
static PARTS = { top: 50
countdowns: {
template: 'systems/daggerheart/templates/ui/countdowns.hbs',
scrollable: ['.expanded-view']
} }
}; };
_attachPartListeners(partId, htmlElement, options) { /** @override */
super._attachPartListeners(partId, htmlElement, options); static PARTS = {
resources: {
root: true,
template: 'systems/daggerheart/templates/ui/countdowns.hbs'
}
};
htmlElement.querySelectorAll('.mini-countdown-container').forEach(element => { get element() {
element.addEventListener('click', event => this.updateCountdownValue.bind(this)(event, false)); return document.body.querySelector('.daggerheart.dh-style.countdowns');
element.addEventListener('contextmenu', event => this.updateCountdownValue.bind(this)(event, true));
});
}
async _preFirstRender(context, options) {
options.position =
game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].position) ??
Countdowns.DEFAULT_OPTIONS.position;
const viewSetting =
game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].simple) ?? !game.user.isGM;
this.simpleView =
game.user.isGM || !this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER) ? viewSetting : true;
context.simple = this.simpleView;
}
_onPosition(position) {
game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].position, position);
} }
/**@inheritdoc */
async _renderFrame(options) { async _renderFrame(options) {
const frame = await super._renderFrame(options); const frame = await super._renderFrame(options);
if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER)) { const iconOnly =
const button = constructHTMLButton({ game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) ===
label: '', CONFIG.DH.GENERAL.countdownAppMode.iconOnly;
classes: ['header-control', 'icon', 'fa-solid', 'fa-wrench'], if (iconOnly) frame.classList.add('icon-only');
dataset: { action: 'toggleSimpleView', tooltip: 'DAGGERHEART.APPLICATIONS.Countdown.toggleSimple' } else frame.classList.remove('icon-only');
});
this.window.controls.after(button); const header = frame.querySelector('.window-header');
} header.querySelector('button[data-action="close"]').remove();
const minimizeTooltip = game.i18n.localize('DAGGERHEART.UI.Countdowns.toggleIconMode');
const minimizeButton = `<a class="header-control" data-tooltip="${minimizeTooltip}" aria-label="${minimizeTooltip}" data-action="toggleViewMode"><i class="fa-solid fa-down-left-and-up-right-to-center"></i></a>`;
header.insertAdjacentHTML('beforeEnd', minimizeButton);
return frame; return frame;
} }
testUserPermission(level, exact, altSettings) { /** @override */
if (game.user.isGM) return true; async _prepareContext(options) {
const context = await super._prepareContext(options);
const settings =
altSettings ?? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath];
const defaultAllowed = exact ? settings.ownership.default === level : settings.ownership.default >= level;
const userAllowed = exact
? settings.playerOwnership[game.user.id]?.value === level
: settings.playerOwnership[game.user.id]?.value >= level;
return defaultAllowed || userAllowed;
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
const countdownData = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[
this.basePath
];
context.isGM = game.user.isGM; context.isGM = game.user.isGM;
context.base = this.basePath; context.sidebarCollapsed = this.sidebarCollapsed;
context.iconOnly =
game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) ===
CONFIG.DH.GENERAL.countdownAppMode.iconOnly;
context.canCreate = this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true); const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
context.source = { context.countdowns = Object.keys(setting.countdowns).reduce((acc, key) => {
...countdownData, const countdown = setting.countdowns[key];
countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => { const ownership = DhCountdowns.#getPlayerOwnership(game.user, setting, countdown);
const countdown = countdownData.countdowns[key]; if (ownership === CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) return acc;
if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.LIMITED, false, countdown)) { const playersWithAccess = game.users.reduce((acc, user) => {
acc[key] = { const ownership = DhCountdowns.#getPlayerOwnership(user, setting, countdown);
...countdown, if (!user.isGM && ownership && ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) {
canEdit: this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true, countdown) acc.push(user);
};
} }
return acc; return acc;
}, {}) }, []);
}; const nonGmPlayers = game.users.filter(x => !x.isGM);
context.systemFields = countdownData.schema.fields; acc[key] = {
context.countdownFields = context.systemFields.countdowns.element.fields; ...countdown,
context.simple = this.simpleView; editable: game.user.isGM || ownership === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER,
playerAccess: playersWithAccess.length !== nonGmPlayers.length ? playersWithAccess : [],
noPlayerAccess: nonGmPlayers.length && playersWithAccess.length === 0
};
return acc;
}, {});
return context; return context;
} }
static async updateData(event, _, formData) { static #getPlayerOwnership(user, setting, countdown) {
const data = foundry.utils.expandObject(formData.object); const playerOwnership = countdown.ownership[user.id];
const newSetting = foundry.utils.mergeObject( return playerOwnership === undefined || playerOwnership === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns).toObject(), ? setting.defaultOwnership
data : playerOwnership;
); }
if (game.user.isGM) { toggleCollapsedPosition = async (_, collapsed) => {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, newSetting); this.sidebarCollapsed = collapsed;
this.render(); if (!collapsed) this.element.classList.add('expanded');
} else { else this.element.classList.remove('expanded');
await game.socket.emit(`system.${CONFIG.DH.id}`, { };
action: socketEvent.GMUpdate,
data: { cooldownRefresh = ({ refreshType }) => {
action: GMUpdateEvent.UpdateSetting, if (refreshType === RefreshType.Countdown) this.render();
uuid: CONFIG.DH.SETTINGS.gameSettings.Countdowns, };
update: newSetting
} static canPerformEdit() {
}); if (game.user.isGM) return true;
const noGM = !game.users.find(x => x.isGM && x.active);
if (noGM) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired'));
return false;
} }
return true;
} }
async updateSetting(update) { static async #toggleViewMode() {
if (game.user.isGM) { const currentMode = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode);
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, update); const appMode = CONFIG.DH.GENERAL.countdownAppMode;
await game.socket.emit(`system.${CONFIG.DH.id}`, { const newMode = currentMode === appMode.textIcon ? appMode.iconOnly : appMode.textIcon;
action: socketEvent.Refresh, await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode, newMode);
data: {
refreshType: RefreshType.Countdown,
application: `${this.basePath}-countdowns`
}
});
this.render(); if (newMode === appMode.iconOnly) this.element.classList.add('icon-only');
} else { else this.element.classList.remove('icon-only');
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: CONFIG.DH.SETTINGS.gameSettings.Countdowns,
update: update,
refresh: { refreshType: RefreshType.Countdown, application: `${this.basePath}-countdowns` }
}
});
}
}
static onEditImage(_, target) {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath];
const current = setting.countdowns[target.dataset.countdown].img;
const fp = new foundry.applications.apps.FilePicker.implementation({
current,
type: 'image',
callback: async path => this.updateImage.bind(this)(path, target.dataset.countdown),
top: this.position.top + 40,
left: this.position.left + 10
});
return fp.browse();
}
async updateImage(path, countdown) {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
await setting.updateSource({
[`${this.basePath}.countdowns.${countdown}.img`]: path
});
await this.updateSetting(setting);
}
static openOwnership(_, target) {
new Promise((resolve, reject) => {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath];
const ownership = { default: setting.ownership.default, players: setting.playerOwnership };
new OwnershipSelection(resolve, reject, this.title, ownership).render(true);
}).then(async ownership => {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
await setting.updateSource({
[`${this.basePath}.ownership`]: ownership
});
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, setting.toObject());
this.render();
});
}
static openCountdownOwnership(_, target) {
const countdownId = target.dataset.countdown;
new Promise((resolve, reject) => {
const countdown = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath]
.countdowns[countdownId];
const ownership = { default: countdown.ownership.default, players: countdown.playerOwnership };
new OwnershipSelection(resolve, reject, countdown.name, ownership).render(true);
}).then(async ownership => {
const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
await setting.updateSource({
[`${this.basePath}.countdowns.${countdownId}.ownership`]: ownership
});
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, setting);
this.render();
});
}
static async toggleSimpleView() {
this.simpleView = !this.simpleView;
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.basePath}Countdown`].simple, this.simpleView);
this.render(); this.render();
} }
async updateCountdownValue(event, increase) { static async editCountdown(increase, target) {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); if (!DhCountdowns.canPerformEdit()) return;
const countdown = countdownSetting[this.basePath].countdowns[event.currentTarget.dataset.countdown];
if (!this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)) { const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
return; const countdown = settings.countdowns[target.id];
} const newCurrent = increase
? Math.min(countdown.progress.current + 1, countdown.progress.max)
const currentValue = countdown.progress.current; : Math.max(countdown.progress.current - 1, 0);
await settings.updateSource({ [`countdowns.${target.id}.progress.current`]: newCurrent });
if (increase && currentValue === countdown.progress.max) return; await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
if (!increase && currentValue === 0) return; refreshType: RefreshType.Countdown
await countdownSetting.updateSource({
[`${this.basePath}.countdowns.${event.currentTarget.dataset.countdown}.progress.current`]: increase
? currentValue + 1
: currentValue - 1
}); });
await this.updateSetting(countdownSetting.toObject());
} }
static async addCountdown() { 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 }
});
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
}
setupHooks() {
Hooks.on('collapseSidebar', this.toggleCollapsedPosition.bind());
Hooks.on(socketEvent.Refresh, this.cooldownRefresh.bind());
}
close(options) {
Hooks.off('collapseSidebar', this.toggleCollapsedPosition);
Hooks.off(socketEvent.Refresh, this.cooldownRefresh);
super.close(options);
}
static async updateCountdowns(progressType) {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
await countdownSetting.updateSource({ const updatedCountdowns = Object.keys(countdownSetting.countdowns).reduce((acc, key) => {
[`${this.basePath}.countdowns.${foundry.utils.randomID()}`]: { const countdown = countdownSetting.countdowns[key];
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.newCountdown'), if (countdown.progress.type === progressType && countdown.progress.current > 0) {
ownership: game.user.isGM acc.push(key);
? {}
: {
players: {
[game.user.id]: { type: CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER }
}
}
} }
});
await this.updateSetting(countdownSetting.toObject()); return acc;
} }, []);
static async removeCountdown(_, target) { const countdownData = countdownSetting.toObject();
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, {
const countdownName = countdownSetting[this.basePath].countdowns[target.dataset.countdown].name; ...countdownData,
countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => {
const confirmed = await foundry.applications.api.DialogV2.confirm({ const countdown = foundry.utils.deepClone(countdownData.countdowns[key]);
window: { if (updatedCountdowns.includes(key)) {
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.removeCountdownTitle') countdown.progress.current -= 1;
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.Countdown.removeCountdownText', { name: countdownName })
});
if (!confirmed) return;
await countdownSetting.updateSource({ [`${this.basePath}.countdowns.-=${target.dataset.countdown}`]: null });
await this.updateSetting(countdownSetting.toObject());
}
async open() {
await this.render(true);
if (
Object.keys(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns)[this.basePath].countdowns
).length > 0
) {
this.minimize();
}
}
}
export class NarrativeCountdowns extends Countdowns {
constructor() {
super('narrative');
}
static DEFAULT_OPTIONS = {
id: 'narrative-countdowns'
};
}
export class EncounterCountdowns extends Countdowns {
constructor() {
super('encounter');
}
static DEFAULT_OPTIONS = {
id: 'encounter-countdowns'
};
}
export async function updateCountdowns(progressType) {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const update = Object.keys(countdownSetting).reduce((update, typeKey) => {
return foundry.utils.mergeObject(
update,
Object.keys(countdownSetting[typeKey].countdowns).reduce((acc, countdownKey) => {
const countdown = countdownSetting[typeKey].countdowns[countdownKey];
if (countdown.progress.current > 0 && countdown.progress.type.value === progressType) {
acc[`${typeKey}.countdowns.${countdownKey}.progress.current`] = countdown.progress.current - 1;
} }
acc[key] = countdown;
return acc; return acc;
}, {}) }, {})
); });
}, {});
await countdownSetting.updateSource(update); const data = { refreshType: RefreshType.Countdown };
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSetting); await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
const data = { refreshType: RefreshType.Countdown }; data
await game.socket.emit(`system.${CONFIG.DH.id}`, { });
action: socketEvent.Refresh, Hooks.callAll(socketEvent.Refresh, data);
data }
});
Hooks.callAll(socketEvent.Refresh, data);
} }

View file

@ -16,6 +16,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
this.selectedMenu = { path: [], data: null }; this.selectedMenu = { path: [], data: null };
this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig; this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig;
this.presets = {}; this.presets = {};
this.compendiumBrowserTypeKey = 'compendiumBrowserDefault';
} }
/** @inheritDoc */ /** @inheritDoc */
@ -84,10 +85,26 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
/** @inheritDoc */ /** @inheritDoc */
async _preRender(context, options) { async _preRender(context, options) {
this.presets = options.presets ?? {}; this.presets = options.presets ?? {};
const noFolder = this.presets?.render?.noFolder;
if (noFolder === true) {
this.compendiumBrowserTypeKey = 'compendiumBrowserNoFolder';
}
const lite = this.presets?.render?.lite;
if (lite === true) {
this.compendiumBrowserTypeKey = 'compendiumBrowserLite';
}
const userPresetPosition = game.user.getFlag(
CONFIG.DH.id,
CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position
);
const width = this.presets?.render?.noFolder === true || this.presets?.render?.lite === true ? 600 : 850; options.position = userPresetPosition ?? ItemBrowser.DEFAULT_OPTIONS.position;
if (this.rendered) this.setPosition({ width });
else options.position.width = width; if (!userPresetPosition) {
const width = noFolder === true || lite === true ? 600 : 850;
if (this.rendered) this.setPosition({ width });
else options.position.width = width;
}
await super._preRender(context, options); await super._preRender(context, options);
} }
@ -113,6 +130,10 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
}); });
} }
_onPosition(position) {
game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position, position);
}
_attachPartListeners(partId, htmlElement, options) { _attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options); super._attachPartListeners(partId, htmlElement, options);

View file

@ -8,8 +8,21 @@ export const encounterCountdown = {
position: 'countdown-encounter-position' position: 'countdown-encounter-position'
}; };
export const compendiumBrowserDefault = {
position: 'compendium-browser-default-position'
};
export const compendiumBrowserNoFolder = {
position: 'compendium-browser-no-folder-position'
};
export const compendiumBrowserLite = {
position: 'compendium-browser-lite-position'
};
export const itemAttachmentSource = 'attachmentSource'; export const itemAttachmentSource = 'attachmentSource';
export const userFlags = { export const userFlags = {
welcomeMessage: 'welcome-message' welcomeMessage: 'welcome-message',
countdownMode: 'countdown-mode'
}; };

View file

@ -650,3 +650,30 @@ export const fearDisplay = {
bar: { value: 'bar', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.bar' }, bar: { value: 'bar', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.bar' },
hide: { value: 'hide', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.hide' } hide: { value: 'hide', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.hide' }
}; };
export const basicOwnershiplevels = {
0: { value: 0, label: 'OWNERSHIP.NONE' },
2: { value: 2, label: 'OWNERSHIP.OBSERVER' },
3: { value: 3, label: 'OWNERSHIP.OWNER' }
};
export const simpleOwnershiplevels = {
[-1]: { value: -1, label: 'OWNERSHIP.INHERIT' },
...basicOwnershiplevels
};
export const countdownBaseTypes = {
narrative: {
id: 'narrative',
name: 'DAGGERHEART.APPLICATIONS.Countdown.types.narrative'
},
encounter: {
id: 'encounter',
name: 'DAGGERHEART.APPLICATIONS.Countdown.types.encounter'
}
};
export const countdownAppMode = {
textIcon: 'text-icon',
iconOnly: 'icon-only'
};

View file

@ -370,19 +370,6 @@ export const typeConfig = {
label: 'DAGGERHEART.ITEMS.Subclass.spellcastingTrait' label: 'DAGGERHEART.ITEMS.Subclass.spellcastingTrait'
} }
], ],
filters: []
},
beastforms: {
columns: [
{
key: 'system.tier',
label: 'DAGGERHEART.GENERAL.Tiers.singular'
},
{
key: 'system.mainTrait',
label: 'DAGGERHEART.GENERAL.Trait.single'
}
],
filters: [ filters: [
{ {
key: 'system.linkedClass.uuid', key: 'system.linkedClass.uuid',

View file

@ -1,9 +1,11 @@
import DHAbilityUse from './abilityUse.mjs'; import DHAbilityUse from './abilityUse.mjs';
import DHActorRoll from './actorRoll.mjs'; import DHActorRoll from './actorRoll.mjs';
import DHSystemMessage from './systemMessage.mjs';
export const config = { export const config = {
abilityUse: DHAbilityUse, abilityUse: DHAbilityUse,
adversaryRoll: DHActorRoll, adversaryRoll: DHActorRoll,
damageRoll: DHActorRoll, damageRoll: DHActorRoll,
dualityRoll: DHActorRoll dualityRoll: DHActorRoll,
systemMessage: DHSystemMessage
}; };

View file

@ -0,0 +1,9 @@
export default class DHSystemMessage extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
useTitle: new fields.BooleanField({ initial: true })
};
}
}

View file

@ -3,7 +3,8 @@ export default class DhCombatant extends foundry.abstract.TypeDataModel {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
spotlight: new fields.SchemaField({ spotlight: new fields.SchemaField({
requesting: new fields.BooleanField({ required: true, initial: false }) requesting: new fields.BooleanField({ required: true, initial: false }),
requestOrderIndex: new fields.NumberField({ required: true, integer: true, initial: 0 })
}), }),
actionTokens: new fields.NumberField({ required: true, integer: true, initial: 3 }) actionTokens: new fields.NumberField({ required: true, integer: true, initial: 3 })
}; };

View file

@ -1,25 +1,28 @@
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
export default class DhCountdowns extends foundry.abstract.DataModel { export default class DhCountdowns extends foundry.abstract.DataModel {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
/* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */
narrative: new fields.EmbeddedDataField(DhCountdownData), narrative: new fields.EmbeddedDataField(DhCountdownData),
encounter: new fields.EmbeddedDataField(DhCountdownData) encounter: new fields.EmbeddedDataField(DhCountdownData),
/**/
countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhCountdown)),
defaultOwnership: new fields.NumberField({
required: true,
choices: CONFIG.DH.GENERAL.basicOwnershiplevels,
initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER
})
}; };
} }
static CountdownCategories = { narrative: 'narrative', combat: 'combat' };
} }
/* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */
class DhCountdownData extends foundry.abstract.DataModel { class DhCountdownData extends foundry.abstract.DataModel {
static LOCALIZATION_PREFIXES = ['DAGGERHEART.APPLICATIONS.Countdown']; // Nots ure why this won't work. Setting labels manually for now
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhCountdown)), countdowns: new fields.TypedObjectField(new fields.EmbeddedDataField(DhOldCountdown)),
ownership: new fields.SchemaField({ ownership: new fields.SchemaField({
default: new fields.NumberField({ default: new fields.NumberField({
required: true, required: true,
@ -56,7 +59,8 @@ class DhCountdownData extends foundry.abstract.DataModel {
} }
} }
class DhCountdown extends foundry.abstract.DataModel { /* Outdated and unused. Needed for migration. Remove in next minor version. (1.3) */
class DhOldCountdown extends foundry.abstract.DataModel {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
@ -129,17 +133,88 @@ class DhCountdown extends foundry.abstract.DataModel {
} }
} }
export const registerCountdownHooks = () => { export class DhCountdown extends foundry.abstract.DataModel {
Hooks.on(socketEvent.Refresh, ({ refreshType, application }) => { static defineSchema() {
if (refreshType === RefreshType.Countdown) { const fields = foundry.data.fields;
if (application) { return {
foundry.applications.instances.get(application)?.render(); type: new fields.StringField({
} else { required: true,
foundry.applications.instances.get('narrative-countdowns')?.render(); choices: CONFIG.DH.GENERAL.countdownBaseTypes,
foundry.applications.instances.get('encounter-countdowns')?.render(); label: 'DAGGERHEART.GENERAL.type'
} }),
name: new fields.StringField({
required: true,
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.name.label'
}),
img: new fields.FilePathField({
categories: ['IMAGE'],
base64: false,
initial: 'icons/magic/time/hourglass-yellow-green.webp'
}),
ownership: new fields.TypedObjectField(
new fields.NumberField({
required: true,
choices: CONFIG.DH.GENERAL.simpleOwnershiplevels,
initial: CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT
})
),
progress: new fields.SchemaField({
current: new fields.NumberField({
required: true,
integer: true,
initial: 1,
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.current.label'
}),
max: new fields.NumberField({
required: true,
integer: true,
initial: 1,
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.progress.max.label'
}),
type: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.countdownTypes,
initial: CONFIG.DH.GENERAL.countdownTypes.custom.id,
label: 'DAGGERHEART.APPLICATIONS.Countdown.FIELDS.countdowns.element.type.label'
})
})
};
}
return false; 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;
}, {})
: undefined;
return {
type: type ?? CONFIG.DH.GENERAL.countdownBaseTypes.narrative.id,
name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.newCountdown'),
img: 'icons/magic/time/hourglass-yellow-green.webp',
ownership: ownership,
progress: {
current: 1,
max: 1
}
};
}
get playerOwnership() {
return Array.from(game.users).reduce((acc, user) => {
acc[user.id] = {
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,
isGM: user.isGM
};
return acc;
}, {});
}
}

View file

@ -103,7 +103,7 @@ export default class CostField extends fields.ArrayField {
static calcCosts(costs) { static calcCosts(costs) {
const resources = CostField.getResources.call(this, costs); const resources = CostField.getResources.call(this, costs);
let filteredCosts = costs; let filteredCosts = costs;
if (this.parent.metadata.isQuantifiable && this.parent.consumeOnUse === false) { if (this.parent?.metadata.isQuantifiable && this.parent.consumeOnUse === false) {
filteredCosts = filteredCosts.filter(c => c.key !== 'quantity'); filteredCosts = filteredCosts.filter(c => c.key !== 'quantity');
} }

View file

@ -81,6 +81,9 @@ export default class DamageField extends fields.SchemaField {
static async applyDamage(config, targets = null, force = false) { static async applyDamage(config, targets = null, force = false) {
targets ??= config.targets.filter(target => target.hit); targets ??= config.targets.filter(target => target.hit);
if (!config.damage || !targets?.length || (!DamageField.getApplyAutomation() && !force)) return; if (!config.damage || !targets?.length || (!DamageField.getApplyAutomation() && !force)) return;
const targetDamage = [];
const damagePromises = [];
for (let target of targets) { for (let target of targets) {
const actor = fromUuidSync(target.actorId); const actor = fromUuidSync(target.actorId);
if (!actor) continue; if (!actor) continue;
@ -95,9 +98,45 @@ export default class DamageField extends fields.SchemaField {
}); });
} }
if (config.hasHealing) actor.takeHealing(config.damage); if (config.hasHealing)
else actor.takeDamage(config.damage, config.isDirect); damagePromises.push(
actor
.takeHealing(config.damage)
.then(updates => targetDamage.push({ token: actor.token ?? actor.prototypeToken, updates }))
);
else
damagePromises.push(
actor
.takeDamage(config.damage, config.isDirect)
.then(updates => targetDamage.push({ token: actor.token ?? actor.prototypeToken, updates }))
);
} }
Promise.all(damagePromises).then(async _ => {
const summaryMessageSettings = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Automation
).summaryMessages;
if (!summaryMessageSettings.damage) return;
const cls = getDocumentClass('ChatMessage');
const msg = {
type: 'systemMessage',
user: game.user.id,
speaker: cls.getSpeaker(),
title: game.i18n.localize(
`DAGGERHEART.UI.Chat.damageSummary.${config.hasHealing ? 'healingTitle' : 'title'}`
),
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/damageSummary.hbs',
{
targets: targetDamage
}
)
};
cls.create(msg);
});
} }
/** /**

View file

@ -46,17 +46,48 @@ export default class EffectsField extends fields.ArrayField {
*/ */
static async applyEffects(targets) { static async applyEffects(targets) {
if (!this.effects?.length || !targets?.length) return; if (!this.effects?.length || !targets?.length) return;
let effects = this.effects; let effects = this.effects;
targets.forEach(async token => { const messageTargets = [];
targets.forEach(async baseToken => {
if (this.hasSave && token.saved.success === true) effects = this.effects.filter(e => e.onSave === true); if (this.hasSave && token.saved.success === true) effects = this.effects.filter(e => e.onSave === true);
if (!effects.length) return; if (!effects.length) return;
const token = canvas.tokens.get(baseToken.id);
if (!token) return;
messageTargets.push(token.document);
effects.forEach(async e => { effects.forEach(async e => {
const actor = canvas.tokens.get(token.id)?.actor, const effect = this.item.effects.get(e._id);
effect = this.item.effects.get(e._id); if (!token.actor || !effect) return;
if (!actor || !effect) return; await EffectsField.applyEffect(effect, token.actor);
await EffectsField.applyEffect(effect, actor);
}); });
}); });
if (messageTargets.length === 0) return;
const summaryMessageSettings = game.settings.get(
CONFIG.DH.id,
CONFIG.DH.SETTINGS.gameSettings.Automation
).summaryMessages;
if (!summaryMessageSettings.effects) return;
const cls = getDocumentClass('ChatMessage');
const msg = {
type: 'systemMessage',
user: game.user.id,
speaker: cls.getSpeaker(),
title: game.i18n.localize('DAGGERHEART.UI.Chat.effectSummary.title'),
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/effectSummary.hbs',
{
effects: this.effects.map(e => this.item.effects.get(e._id)),
targets: messageTargets
}
)
};
cls.create(msg);
} }
/** /**

View file

@ -2,6 +2,10 @@ export default class DhAutomation extends foundry.abstract.DataModel {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
summaryMessages: new fields.SchemaField({
damage: new fields.BooleanField({ initial: true, label: 'DAGGERHEART.GENERAL.damage' }),
effects: new fields.BooleanField({ initial: true, label: 'DAGGERHEART.GENERAL.Effect.plural' })
}),
hopeFear: new fields.SchemaField({ hopeFear: new fields.SchemaField({
gm: new fields.BooleanField({ gm: new fields.BooleanField({
required: true, required: true,

View file

@ -501,6 +501,7 @@ export default class DhpActor extends Actor {
/**@inheritdoc */ /**@inheritdoc */
getRollData() { getRollData() {
const rollData = super.getRollData(); const rollData = super.getRollData();
rollData.name = this.name;
rollData.system = this.system.getRollData(); rollData.system = this.system.getRollData();
rollData.prof = this.system.proficiency ?? 1; rollData.prof = this.system.proficiency ?? 1;
rollData.cast = this.system.spellcastModifier ?? 1; rollData.cast = this.system.spellcastModifier ?? 1;
@ -598,6 +599,8 @@ export default class DhpActor extends Actor {
await this.modifyResource(updates); await this.modifyResource(updates);
if (Hooks.call(`${CONFIG.DH.id}.postTakeDamage`, this, updates) === false) return null; if (Hooks.call(`${CONFIG.DH.id}.postTakeDamage`, this, updates) === false) return null;
return updates;
} }
calculateDamage(baseDamage, type) { calculateDamage(baseDamage, type) {
@ -646,6 +649,8 @@ export default class DhpActor extends Actor {
await this.modifyResource(updates); await this.modifyResource(updates);
if (Hooks.call(`${CONFIG.DH.id}.postTakeHealing`, this, updates) === false) return null; if (Hooks.call(`${CONFIG.DH.id}.postTakeHealing`, this, updates) === false) return null;
return updates;
} }
async modifyResource(resources) { async modifyResource(resources) {

View file

@ -143,6 +143,12 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
html.querySelectorAll('.button-target-selection').forEach(element => { html.querySelectorAll('.button-target-selection').forEach(element => {
element.addEventListener('click', this.onTargetSelection.bind(this)); element.addEventListener('click', this.onTargetSelection.bind(this));
}); });
html.querySelectorAll('.token-target-container').forEach(element => {
element.addEventListener('pointerover', this.hoverTarget);
element.addEventListener('pointerout', this.unhoverTarget);
element.addEventListener('click', this.clickTarget);
});
} }
async onRollDamage(event) { async onRollDamage(event) {

View file

@ -1,29 +1,8 @@
import { parseInlineParams } from './parser.mjs';
export default function DhDamageEnricher(match, _options) { export default function DhDamageEnricher(match, _options) {
const parts = match[1].split('|').map(x => x.trim()); const { value, type, inline } = parseInlineParams(match[1]);
if (!value || !type) return match[0];
let value = null,
type = null,
inline = false;
parts.forEach(part => {
const split = part.split(':').map(x => x.toLowerCase().trim());
if (split.length === 2) {
switch (split[0]) {
case 'value':
value = split[1];
break;
case 'type':
type = split[1];
break;
case 'inline':
inline = true;
break;
}
}
});
if (!value || !value) return match[0];
return getDamageMessage(value, type, inline, match[0]); return getDamageMessage(value, type, inline, match[0]);
} }

View file

@ -0,0 +1,8 @@
import { parseInlineParams } from './parser.mjs';
export default function DhLookupEnricher(match, { rollData }) {
const results = parseInlineParams(match[1], { first: 'formula' });
const element = document.createElement('span');
element.textContent = Roll.replaceFormulaData(String(results.formula), rollData);
return element;
}

View file

@ -1,55 +1,25 @@
import { parseInlineParams } from './parser.mjs';
export default function DhTemplateEnricher(match, _options) { export default function DhTemplateEnricher(match, _options) {
const parts = match[1].split('|').map(x => x.trim()); const params = parseInlineParams(match[1]);
const { type, angle = CONFIG.MeasuredTemplate.defaults.angle, inline = false } = params;
let type = null, const direction = Number(params.direction) || 0;
range = null, const range =
angle = CONFIG.MeasuredTemplate.defaults.angle, params.range && Number.isNaN(params.range)
direction = 0, ? Object.values(CONFIG.DH.GENERAL.templateRanges).find(
inline = false; x => x.id.toLowerCase() === split[1] || x.short === split[1]
)?.id
parts.forEach(part => { : params.range;
const split = part.split(':').map(x => x.toLowerCase().trim()); if (!(type in CONFIG.MeasuredTemplate.types) || !range) return match[0];
if (split.length === 2) {
switch (split[0]) {
case 'type':
const matchedType = Object.values(CONFIG.DH.GENERAL.templateTypes).find(
x => x.toLowerCase() === split[1]
);
type = matchedType;
break;
case 'range':
if (Number.isNaN(Number(split[1]))) {
const matchedRange = Object.values(CONFIG.DH.GENERAL.templateRanges).find(
x => x.id.toLowerCase() === split[1] || x.short === split[1]
);
range = matchedRange?.id;
} else {
range = split[1];
}
break;
case 'inline':
inline = true;
break;
case 'angle':
angle = split[1];
break;
case 'direction':
direction = split[1];
break;
}
}
});
if (!type || !range) return match[0];
const label = game.i18n.localize(`DAGGERHEART.CONFIG.TemplateTypes.${type}`); const label = game.i18n.localize(`DAGGERHEART.CONFIG.TemplateTypes.${type}`);
const rangeDisplay = Number.isNaN(Number(range))
const rangeDisplay = Number.isNaN(Number(range)) ? game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.name`) : range; ? game.i18n.localize(`DAGGERHEART.CONFIG.Range.${range}.name`)
: range;
let angleDisplay = ''; let angleDisplay = '';
if (angle != CONFIG.MeasuredTemplate.defaults.angle) { if (angle != CONFIG.MeasuredTemplate.defaults.angle) {
angleDisplay = 'angle:' + angle; angleDisplay = 'angle:' + angle;
} }
let directionDisplay = ''; let directionDisplay = '';
if (direction != 0) { if (direction != 0) {
@ -95,8 +65,9 @@ export const renderMeasuredTemplate = async event => {
let baseDistance = range; let baseDistance = range;
if (Number.isNaN(Number(range))) { if (Number.isNaN(Number(range))) {
baseDistance = baseDistance = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement[
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement[range]; range
];
} }
const distance = type === CONFIG.DH.GENERAL.templateTypes.EMANATION ? baseDistance + 2.5 : baseDistance; const distance = type === CONFIG.DH.GENERAL.templateTypes.EMANATION ? baseDistance + 2.5 : baseDistance;

View file

@ -2,6 +2,7 @@ import { default as DhDamageEnricher, renderDamageButton } from './DamageEnriche
import { default as DhDualityRollEnricher, renderDualityButton } from './DualityRollEnricher.mjs'; import { default as DhDualityRollEnricher, renderDualityButton } from './DualityRollEnricher.mjs';
import { default as DhEffectEnricher } from './EffectEnricher.mjs'; import { default as DhEffectEnricher } from './EffectEnricher.mjs';
import { default as DhTemplateEnricher, renderMeasuredTemplate } from './TemplateEnricher.mjs'; import { default as DhTemplateEnricher, renderMeasuredTemplate } from './TemplateEnricher.mjs';
import { default as DhLookupEnricher } from './LookupEnricher.mjs';
export { DhDamageEnricher, DhDualityRollEnricher, DhEffectEnricher, DhTemplateEnricher }; export { DhDamageEnricher, DhDualityRollEnricher, DhEffectEnricher, DhTemplateEnricher };
@ -21,6 +22,10 @@ export const enricherConfig = [
{ {
pattern: /@Template\[(.*)\]({.*})?/g, pattern: /@Template\[(.*)\]({.*})?/g,
enricher: DhTemplateEnricher enricher: DhTemplateEnricher
},
{
pattern: /@Lookup\[(.*)\]({.*})?/g,
enricher: DhLookupEnricher
} }
]; ];

View file

@ -0,0 +1,20 @@
/**
* @param {string} paramString The parameter inside the brackets of something like @Template[] to parse
* @param {Object} options
* @param {string} options.first If set, the first parameter is treated as a value with this as its key
* @returns {Record<string, string | undefined> | null}
*/
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) {
params[first] = param;
} else {
const parts = param.split(':');
params[parts[0]] = parts.length > 1 ? parts[1] : true;
}
}
return params;
}

View file

@ -15,7 +15,7 @@ export default class RegisterHandlebarsHelpers {
setVar: this.setVar, setVar: this.setVar,
empty: this.empty, empty: this.empty,
pluralize: this.pluralize, pluralize: this.pluralize,
log: this.log positive: this.positive
}); });
} }
static add(a, b) { static add(a, b) {
@ -91,7 +91,7 @@ export default class RegisterHandlebarsHelpers {
return game.i18n.localize(key); return game.i18n.localize(key);
} }
static log(test) { static positive(a) {
return test; return Math.abs(Number(a));
} }
} }

View file

@ -29,7 +29,6 @@ export const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs', 'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs',
'systems/daggerheart/templates/dialogs/downtime/activities.hbs', 'systems/daggerheart/templates/dialogs/downtime/activities.hbs',
'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs', 'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs',
'systems/daggerheart/templates/ui/chat/parts/roll-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/roll-part.hbs',
'systems/daggerheart/templates/ui/chat/parts/damage-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/damage-part.hbs',
'systems/daggerheart/templates/ui/chat/parts/target-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/target-part.hbs',

View file

@ -97,6 +97,7 @@ export async function runMigrations() {
} }
if (foundry.utils.isNewerVersion('1.2.0', lastMigrationVersion)) { if (foundry.utils.isNewerVersion('1.2.0', lastMigrationVersion)) {
/* Migrate old action costs */
const lockedPacks = []; const lockedPacks = [];
const compendiumItems = []; const compendiumItems = [];
for (let pack of game.packs) { for (let pack of game.packs) {
@ -148,6 +149,36 @@ export async function runMigrations() {
await pack.configure({ locked: true }); await pack.configure({ locked: true });
} }
/* Migrate old countdown structure */
const countdownSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);
const getCountdowns = (data, type) => {
return Object.keys(data.countdowns).reduce((acc, key) => {
const countdown = data.countdowns[key];
acc[key] = {
...countdown,
type: type,
ownership: Object.keys(countdown.ownership.players).reduce((acc, key) => {
acc[key] = countdown.ownership.players[key].type;
return acc;
}, {}),
progress: {
...countdown.progress,
type: countdown.progress.type.value
}
};
return acc;
}, {});
};
await countdownSettings.updateSource({
countdowns: {
...getCountdowns(countdownSettings.narrative, CONFIG.DH.GENERAL.countdownBaseTypes.narrative.id),
...getCountdowns(countdownSettings.encounter, CONFIG.DH.GENERAL.countdownBaseTypes.encounter.id)
}
});
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSettings);
lastMigrationVersion = '1.2.0'; lastMigrationVersion = '1.2.0';
} }

View file

@ -30,6 +30,7 @@ export const GMUpdateEvent = {
UpdateEffect: 'DhGMUpdateEffect', UpdateEffect: 'DhGMUpdateEffect',
UpdateSetting: 'DhGMUpdateSetting', UpdateSetting: 'DhGMUpdateSetting',
UpdateFear: 'DhGMUpdateFear', UpdateFear: 'DhGMUpdateFear',
UpdateCountdowns: 'DhGMUpdateCountdowns',
UpdateSaveMessage: 'DhGMUpdateSaveMessage' UpdateSaveMessage: 'DhGMUpdateSaveMessage'
}; };
@ -66,6 +67,10 @@ export const registerSocketHooks = () => {
) )
); );
break; break;
case GMUpdateEvent.UpdateCountdowns:
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data.update);
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
break;
case GMUpdateEvent.UpdateSaveMessage: case GMUpdateEvent.UpdateSaveMessage:
const action = await fromUuid(data.update.action), const action = await fromUuid(data.update.action),
message = game.messages.get(data.update.message); message = game.messages.get(data.update.message);
@ -90,14 +95,15 @@ export const registerUserQueries = () => {
CONFIG.queries.reactionRoll = game.system.api.fields.ActionFields.SaveField.rollSaveQuery; CONFIG.queries.reactionRoll = game.system.api.fields.ActionFields.SaveField.rollSaveQuery;
}; };
export const emitAsGM = async (eventName, callback, update, uuid = null) => { export const emitAsGM = async (eventName, callback, update, uuid = null, refresh = null) => {
if (!game.user.isGM) { if (!game.user.isGM) {
return await game.socket.emit(`system.${CONFIG.DH.id}`, { return await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate, action: socketEvent.GMUpdate,
data: { data: {
action: eventName, action: eventName,
uuid, uuid,
update update,
refresh
} }
}); });
} else return callback(update); } else return callback(update);

View file

@ -61,6 +61,7 @@
width: 40px; width: 40px;
height: 40px; height: 40px;
object-fit: cover; object-fit: cover;
object-position: top center;
} }
.message-sub-header-container { .message-sub-header-container {

View file

@ -26,7 +26,7 @@
&:not(.single-img) { &:not(.single-img) {
.inventory-item-header:hover { .inventory-item-header:hover {
.img-portait { .img-portait:has(.roll-img) {
.roll-img { .roll-img {
opacity: 1; opacity: 1;
} }
@ -96,7 +96,9 @@
} }
.roll-img { .roll-img {
object-fit: contain;
opacity: 0; opacity: 0;
padding: 2px;
} }
} }
@ -307,12 +309,14 @@
background-color: @dark-blue; background-color: @dark-blue;
bottom: 0; bottom: 0;
mask-image: linear-gradient(180deg, transparent 0%, black 20%); mask-image: linear-gradient(180deg, transparent 0%, black 20%);
border-radius: 0 0 6px 6px;
.card-name { .card-name {
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-size: var(--font-size-12); font-size: var(--font-size-12);
line-height: 15px; line-height: 15px;
text-align: center;
color: @beige; color: @beige;
} }

View file

@ -1,13 +1,16 @@
@import '../utils/colors.less'; @import '../utils/colors.less';
@import '../utils/fonts.less'; @import '../utils/fonts.less';
.daggerheart.dh-style { .daggerheart.dh-style {
.tab.active.description { .tab.active.description {
overflow-y: hidden !important; display: flex;
height: -webkit-fill-available !important; flex-direction: column;
height: -webkit-fill-available !important;
fieldset { overflow-y: hidden !important;
height: -webkit-fill-available; padding-top: 10px;
}
} prose-mirror.active + .artist-attribution {
} display: none;
}
}
}

View file

@ -0,0 +1,87 @@
@import '../../utils/colors.less';
#interface.theme-light {
.daggerheart.chat.damage-summary .token-target-container {
&:hover {
background: @dark-blue-10;
}
header {
.actor-name {
color: @dark;
}
&::after {
background: @dark-blue;
}
}
}
}
.daggerheart.chat.damage-summary {
display: flex;
flex-direction: column;
gap: 5px;
padding: 0;
.token-target-container {
display: flex;
flex-direction: column;
gap: 2px;
cursor: pointer;
transition: all 0.3s ease;
border-radius: 6px;
&:hover {
background: @golden-10;
}
header {
display: flex;
align-items: center;
gap: 5px;
pointer-events: none;
position: relative;
margin-bottom: 10px;
img {
width: 40px;
height: 40px;
padding: 0;
border-radius: 50%;
}
.actor-name {
margin: 0;
color: @beige;
font-size: var(--font-size-20);
padding: 8px;
}
&::after {
content: '';
position: absolute;
bottom: -10px;
background: @golden;
mask-image: linear-gradient(270deg, transparent 0%, black 50%, transparent 100%);
height: 2px;
width: 100%;
}
}
.damage-container {
display: flex;
flex-direction: column;
justify-content: center;
gap: 5px;
pointer-events: none;
margin-top: 5px;
list-style: disc;
.damage-row {
padding: 0 2px;
gap: 4px;
}
}
}
}

View file

@ -0,0 +1,166 @@
@import '../../utils/colors.less';
#interface.theme-light {
.daggerheart.chat.effect-summary {
.effect-header,
.actor-header {
&::before,
&::after {
height: 2px;
background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, @dark-blue 100%);
}
&::after {
background: linear-gradient(90deg, @dark-blue 0%, rgba(0, 0, 0, 0) 100%);
}
span {
color: @dark;
}
}
.token-target-container,
.effect-target-container {
.effect-label .title,
.title {
color: @dark-blue;
}
.effect-label {
border-color: @dark-blue;
}
&:hover {
background: @dark-blue-10;
}
}
}
}
.daggerheart.chat.effect-summary {
display: flex;
flex-direction: column;
.effect-header,
.actor-header {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 5px;
&::before,
&::after {
content: '';
flex: 1;
height: 2px;
background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, @golden 100%);
}
&::after {
background: linear-gradient(90deg, @golden 0%, rgba(0, 0, 0, 0) 100%);
}
span {
color: @beige;
padding: 0 10px;
white-space: nowrap;
}
}
.effects-container {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-bottom: 8px;
}
.targets-container {
display: flex;
flex-direction: column;
gap: 5px;
}
.token-target-container {
display: flex;
align-items: center;
gap: 13px;
border-radius: 6px;
padding: 0 2px;
border-radius: 6px;
background: transparent;
transition: all 0.3s ease;
padding: 5px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: @golden-10;
}
img {
width: 40px;
height: 40px;
border-radius: 50%;
pointer-events: none;
}
.title {
font-size: var(--font-size-20);
color: @golden;
font-weight: 700;
margin: 0;
pointer-events: none;
}
}
details[open] {
.fa-chevron-down {
transform: rotate(180deg);
transition: all 0.3s ease;
}
}
.effect-target-container {
width: 100%;
transition: all 0.3s ease;
cursor: pointer;
&:hover {
background: @golden-10;
}
.fa-chevron-down {
transition: all 0.3s ease;
margin-left: auto;
}
.effect-label {
display: flex;
flex-direction: row;
align-items: center;
margin: 8px 8px 0;
padding-bottom: 5px;
width: -webkit-fill-available;
gap: 13px;
border-bottom: 1px solid @golden;
.effect-img {
width: 40px;
height: 40px;
border-radius: 3px;
object-fit: cover;
}
.title {
font-size: var(--font-size-20);
color: @golden;
font-weight: 700;
margin: 0;
}
}
.description {
padding: 8px;
}
}
}

View file

@ -0,0 +1,142 @@
@import '../../utils/colors.less';
@import '../../utils/fonts.less';
.theme-light .daggerheart.application.dh-style.countdown-edit {
background-image: url('../assets/parchments/dh-parchment-light.png');
}
.daggerheart.application.dh-style.countdown-edit {
color: light-dark(@dark, @beige);
background-image: url('../assets/parchments/dh-parchment-dark.png');
.edit-container {
display: flex;
flex-direction: column;
gap: 8px;
h2 {
text-align: center;
color: light-dark(@dark, @golden);
}
.header-tools {
display: grid;
grid-template-columns: 2fr 1fr 144px;
gap: 8px;
.hide-tools {
white-space: nowrap;
flex-wrap: nowrap;
display: flex;
align-items: center;
input {
position: relative;
top: 2px;
}
}
.header-main-button {
height: 32px;
flex: 1;
}
.default-ownership-tools {
display: flex;
align-items: center;
gap: 8px;
select {
flex: 1;
background: light-dark(@beige, @dark-blue);
}
}
}
.edit-content {
display: flex;
flex-direction: column;
gap: 8px;
overflow-y: auto;
overflow-x: hidden;
max-height: 500px;
scrollbar-width: thin;
scrollbar-color: light-dark(@dark-blue, @golden) transparent;
.countdown-edit-container {
display: grid;
grid-template-columns: 48px 1fr 64px;
align-items: center;
gap: 8px;
img {
width: 52px;
height: 52px;
border-radius: 6px;
}
.countdown-edit-text {
display: flex;
flex-direction: column;
justify-content: center;
gap: 8px;
.countdown-edit-subtext {
display: flex;
gap: 10px;
.countdown-edit-sub-tag {
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;
}
}
}
.countdown-edit-tools {
display: flex;
gap: 8px;
&.same-row {
margin-top: 17.5px;
}
a {
font-size: 16px;
}
}
}
.countdown-edit-subrow {
display: flex;
gap: 16px;
margin: 0 72px 0 56px;
}
.countdown-edit-input {
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
&.tiny {
flex: 0;
input {
min-width: 2.5rem;
}
}
input,
select {
background: light-dark(@beige, @dark-blue);
color: light-dark(@dark, @beige);
}
}
}
}
}

View file

@ -1,60 +1,130 @@
@import '../../utils/colors.less'; @import '../../utils/colors.less';
@import '../../utils/fonts.less'; @import '../../utils/fonts.less';
.daggerheart.dh-style.countdown { .theme-dark {
fieldset { .daggerheart.dh-style.countdowns {
align-items: center; background-image: url(../assets/parchments/dh-parchment-dark.png);
margin-top: 5px;
border-radius: 6px;
border-color: light-dark(@dark-blue, @golden);
legend { .window-header {
font-weight: bold; background-image: url(../assets/parchments/dh-parchment-dark.png);
color: light-dark(@dark-blue, @golden); }
}
a { }
text-shadow: none;
} .daggerheart.dh-style.countdowns {
z-index: var(--z-index-ui) !important;
border: 0;
border-radius: 4px;
box-shadow: none;
width: 300px;
top: 16px;
right: 64px;
transition:
right ease 250ms,
opacity var(--ui-fade-duration) ease,
opacity var(--ui-fade-duration);
.window-title {
font-family: @font-body;
}
&.expanded {
right: 364px;
}
&.icon-only {
width: 180px;
min-width: 180px;
}
.window-header {
cursor: default;
border-bottom: 0;
}
.window-content {
padding-top: 4px;
padding-bottom: 16px;
overflow: auto;
max-height: 312px;
.countdowns-container {
display: flex;
flex-direction: column;
gap: 8px;
.countdown-container {
display: flex;
justify-content: space-between;
&.icon-only {
gap: 8px;
.countdown-main-container {
.countdown-content {
justify-content: center;
.countdown-tools {
gap: 8px;
}
}
}
}
.countdown-main-container {
display: flex;
align-items: center;
gap: 16px;
img {
width: 44px;
height: 44px;
border-radius: 6px;
}
.countdown-content {
display: flex;
flex-direction: column;
justify-content: space-between;
.countdown-tools {
display: flex;
align-items: center;
gap: 16px;
.progress-tag {
border: 1px solid;
border-radius: 4px;
padding: 2px 4px;
background-color: light-dark(@beige, @dark-blue);
}
}
}
}
.countdown-access-container {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-auto-rows: min-content;
width: 38px;
gap: 4px;
.countdown-access {
height: 10px;
width: 10px;
border-radius: 50%;
border: 1px solid light-dark(@dark-blue, @beige-80);
content: '';
}
}
.countdown-no-access-container {
width: 38px;
display: flex;
align-items: center;
justify-content: center;
}
}
} }
} }
.minimized-view {
display: flex;
gap: 8px;
flex-wrap: wrap;
.mini-countdown-container {
width: fit-content;
display: flex;
align-items: center;
gap: 8px;
border: 2px solid light-dark(@dark-blue, @golden);
border-radius: 6px;
padding: 0 4px 0 0;
background-image: url('../assets/parchments/dh-parchment-light.png');
color: light-dark(@beige, @dark);
cursor: pointer;
&.disabled {
cursor: initial;
}
img {
width: 30px;
height: 30px;
border-radius: 6px 0 0 6px;
}
.mini-countdown-name {
white-space: nowrap;
}
.mini-countdown-value {
}
}
}
.hidden {
display: none;
}
} }

View file

@ -1,7 +1,9 @@
@import './chat/ability-use.less'; @import './chat/ability-use.less';
@import './chat/action.less'; @import './chat/action.less';
@import './chat/chat.less'; @import './chat/chat.less';
@import './chat/damage-summary.less';
@import './chat/downtime.less'; @import './chat/downtime.less';
@import './chat/effect-summary.less';
@import './chat/refresh-message.less'; @import './chat/refresh-message.less';
@import './chat/sheet.less'; @import './chat/sheet.less';
@ -13,6 +15,7 @@
@import './item-browser/item-browser.less'; @import './item-browser/item-browser.less';
@import './countdown/countdown.less'; @import './countdown/countdown.less';
@import './countdown/countdown-edit.less';
@import './countdown/sheet.less'; @import './countdown/sheet.less';
@import './ownership-selection/ownership-selection.less'; @import './ownership-selection/ownership-selection.less';

View file

@ -6,9 +6,15 @@
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
.ownership-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px;
}
.ownership-container { .ownership-container {
display: flex; display: flex;
border: 2px solid light-dark(@dark-blue, @golden);
border-radius: 6px; border-radius: 6px;
padding: 0 4px 0 0; padding: 0 4px 0 0;
align-items: center; align-items: center;
@ -17,12 +23,24 @@
img { img {
height: 40px; height: 40px;
width: 40px; width: 40px;
border-radius: 6px 0 0 6px; border-radius: 50%;
}
span {
flex: 3;
} }
select { select {
flex: 1;
margin: 4px 0; margin: 4px 0;
} }
} }
footer {
margin-top: 10px;
button {
height: 32px;
}
}
} }
} }

View file

@ -1,5 +1,17 @@
.tab.sidebar-tab.daggerheartMenu-sidebar { .tab.sidebar-tab.daggerheartMenu-sidebar {
padding: 0 4px; padding: 4px;
div[data-application-part] {
display: flex;
flex-direction: column;
gap: 8px;
}
h2 {
margin-top: 8px;
text-align: center;
font-weight: bold;
}
.menu-refresh-container { .menu-refresh-container {
display: flex; display: flex;

View file

@ -270,7 +270,8 @@
"adversaryRoll": {}, "adversaryRoll": {},
"damageRoll": {}, "damageRoll": {},
"abilityUse": {}, "abilityUse": {},
"tagTeam": {} "tagTeam": {},
"systemMessage": {}
} }
}, },
"background": "systems/daggerheart/assets/logos/FoundrybornBackgroundLogo.png", "background": "systems/daggerheart/assets/logos/FoundrybornBackgroundLogo.png",

View file

@ -2,20 +2,22 @@
<div class="form-group"> <div class="form-group">
<div class="form-fields"> <div class="form-fields">
<label>{{localize "DAGGERHEART.APPLICATIONS.OwnershipSelection.default"}}</label> <label>{{localize "DAGGERHEART.APPLICATIONS.OwnershipSelection.default"}}</label>
<select name="ownership.default" data-dtype="Number"> <select name="default" data-dtype="Number" disabled>
{{selectOptions @root.ownershipOptions selected=ownership.default labelAttr="label" valueAttr="value" }} {{selectOptions ownershipDefaultOptions selected=defaultOwnership labelAttr="label" valueAttr="value" localize=true }}
</select> </select>
</div> </div>
</div> </div>
{{#each ownership.players as |player id|}} <ul class="ownership-list">
<div class="ownership-container"> {{#each ownership as |player id|}}
<img src="{{player.img}}" /> <li class="ownership-container">
<div>{{player.name}}</div> <img src="{{player.img}}" />
<select name="{{concat "ownership.players." id ".type"}}" data-dtype="Number"> <span>{{player.name}}</span>
{{selectOptions @root.ownershipOptions selected=player.ownership labelAttr="label" valueAttr="value" }} <select name="{{concat "ownership." id}}" data-dtype="Number">
</select> {{selectOptions @root.ownershipOptions selected=player.ownership labelAttr="label" valueAttr="value" localize=true }}
</div> </select>
{{/each}} </li>
{{/each}}
</ul>
<footer class="flexrow"> <footer class="flexrow">
<button type="submit">{{localize "Save"}}</button> <button type="submit">{{localize "Save"}}</button>
</footer> </footer>

View file

@ -2,7 +2,7 @@
<div class="tiers-container"> <div class="tiers-container">
{{#each this.tiers as |tier key|}} {{#each this.tiers as |tier key|}}
<fieldset class="tier-container"> <fieldset class="tier-container">
<legend>{{tier.name}}</legend> <legend>{{localize tier.name}}</legend>
{{#each tier.groups}} {{#each tier.groups}}
<div class="checkbox-group-container"> <div class="checkbox-group-container">

View file

@ -7,7 +7,11 @@
<label>{{localize "DAGGERHEART.SETTINGS.Automation.FIELDS.hopeFear.label"}}</label> <label>{{localize "DAGGERHEART.SETTINGS.Automation.FIELDS.hopeFear.label"}}</label>
{{formGroup settingFields.schema.fields.hopeFear.fields.gm value=settingFields._source.hopeFear.gm localize=true}} {{formGroup settingFields.schema.fields.hopeFear.fields.gm value=settingFields._source.hopeFear.gm localize=true}}
{{formGroup settingFields.schema.fields.hopeFear.fields.players value=settingFields._source.hopeFear.players localize=true}} {{formGroup settingFields.schema.fields.hopeFear.fields.players value=settingFields._source.hopeFear.players localize=true}}
</div>
<div class="form-group setting-two-values">
<label>{{localize "DAGGERHEART.SETTINGS.Automation.FIELDS.summaryMessages.label"}}</label>
{{formGroup settingFields.schema.fields.summaryMessages.fields.damage value=settingFields._source.summaryMessages.damage localize=true}}
{{formGroup settingFields.schema.fields.summaryMessages.fields.effects value=settingFields._source.summaryMessages.effects localize=true}}
</div> </div>
{{formGroup settingFields.schema.fields.actionPoints value=settingFields._source.actionPoints localize=true}} {{formGroup settingFields.schema.fields.actionPoints value=settingFields._source.actionPoints localize=true}}
{{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}} {{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}}

View file

@ -19,6 +19,6 @@
</a> </a>
</div> </div>
</div> </div>
<span class="card-name">{{item.name}}</span> <span class="card-name" data-action="viewItem">{{item.name}}</span>
</div> </div>
</li> </li>

View file

@ -23,7 +23,13 @@ Parameters:
(hasProperty item "toChat" ) "toChat" "editDoc" ) }}' {{#unless hideTooltip}} {{#if (eq type 'attack' )}} (hasProperty item "toChat" ) "toChat" "editDoc" ) }}' {{#unless hideTooltip}} {{#if (eq type 'attack' )}}
data-tooltip="#attack#{{item.actor.uuid}}" {{else}} data-tooltip="#item#{{item.uuid}}" {{/if}} {{/unless}}> data-tooltip="#attack#{{item.actor.uuid}}" {{else}} data-tooltip="#item#{{item.uuid}}" {{/if}} {{/unless}}>
<img src="{{item.img}}" class="item-img {{#if isActor}}actor-img{{/if}}" /> <img src="{{item.img}}" class="item-img {{#if isActor}}actor-img{{/if}}" />
<img class="roll-img" src="systems/daggerheart/assets/icons/dice/default/d20.svg" alt="d20"> {{#if (or item.system.actionsList.size item.system.actionsList.length item.actionType)}}
{{#if @root.isNPC}}
<img class="roll-img d20" src="systems/daggerheart/assets/icons/dice/default/d20.svg" alt="d20">
{{else}}
<img class="roll-img duality" src="systems/daggerheart/assets/icons/dice/duality/DualityBW.svg" alt="2d12">
{{/if}}
{{/if}}
</div> </div>
{{!-- Name & Tags --}} {{!-- Name & Tags --}}

View file

@ -3,10 +3,7 @@
data-tab='{{tabs.description.id}}' data-tab='{{tabs.description.id}}'
data-group='{{tabs.description.group}}' data-group='{{tabs.description.group}}'
> >
<fieldset> {{formInput systemFields.description value=document.system.description enriched=enrichedDescription toggled=true}}
<legend>{{localize "DAGGERHEART.GENERAL.description"}}</legend>
{{formInput systemFields.description value=document.system.description enriched=enrichedDescription toggled=true}}
</fieldset>
{{#if (and showAttribution document.system.attribution.artist)}} {{#if (and showAttribution document.system.attribution.artist)}}
<label class="artist-attribution">{{localize "DAGGERHEART.GENERAL.artistAttribution" artist=document.system.attribution.artist}}</label> <label class="artist-attribution">{{localize "DAGGERHEART.GENERAL.artistAttribution" artist=document.system.attribution.artist}}</label>

View file

@ -1,4 +1,6 @@
<div> <div>
<h2>{{localize "DAGGERHEART.APPLICATIONS.DaggerheartMenu.title"}}</h2>
<fieldset> <fieldset>
<legend>{{localize "Refresh Features"}}</legend> <legend>{{localize "Refresh Features"}}</legend>
@ -16,7 +18,9 @@
{{/each}} {{/each}}
</div> </div>
<button data-action="refreshActors" {{disabled disableRefresh}}>{{localize "Refresh"}}</button> <button data-action="refreshActors" {{disabled disableRefresh}}>{{localize "DAGGERHEART.GENERAL.refresh"}}</button>
</div> </div>
</fieldset> </fieldset>
</div>
<button data-action="editCountdowns">{{localize "DAGGERHEART.APPLICATIONS.DaggerheartMenu.countdowns"}}</button>
</div>

View file

@ -0,0 +1,33 @@
<ul class="daggerheart chat damage-summary">
{{#each targets}}
<li class="token-target-container" data-token="{{this.token.id}}">
<header>
<img src="{{this.token.texture.src}}" />
<h2 class="actor-name">{{this.token.name}}</h2>
</header>
<ul class="damage-container">
{{#each this.updates}}
<li class="damage-row">
{{#if (gte this.value 0)}}
<span>
{{
localize "DAGGERHEART.UI.Chat.markResource"
quantity=this.value
resource=(localize (concat "DAGGERHEART.CONFIG.HealingType." this.key ".name"))
}}
</span>
{{else}}
<span>
{{
localize "DAGGERHEART.UI.Chat.clearResource"
quantity=(positive this.value)
resource=(localize (concat "DAGGERHEART.CONFIG.HealingType." this.key ".name"))
}}
</span>
{{/if}}
</li>
{{/each}}
</ul>
</li>
{{/each}}
</ul>

View file

@ -0,0 +1,33 @@
<div class="daggerheart chat effect-summary">
<div class="effect-header">
<span>{{localize "DAGGERHEART.GENERAL.Effect.plural"}}</span>
</div>
<div class="effects-container">
{{#each effects}}
<details class="effect-target-container">
<summary class="effect-label">
<img class="effect-img" src="{{this.img}}" />
<h2 class="title">{{this.name}}</h2>
<i class="fa-solid fa-chevron-down"></i>
</summary>
<div class="description">
{{{this.description}}}
</div>
</details>
{{/each}}
</div>
{{#if targets}}
<div class="actor-header">
<span>{{localize "DAGGERHEART.UI.Chat.appliedTo"}}</span>
</div>
<div class="targets-container">
{{#each targets}}
<div class="token-target-container" data-token="{{this.id}}">
<img src="{{this.texture.src}}" />
<h2 class="title">{{this.name}}</h2>
</div>
{{/each}}
</div>
{{/if}}
</div>

View file

@ -1,4 +1,7 @@
<div class="combat-tracker"> <div class="combat-tracker">
{{#if (gt this.spotlightRequests.length 0)}}
{{> 'systems/daggerheart/templates/ui/combatTracker/combatTrackerSection.hbs' this title=(localize "DAGGERHEART.GENERAL.SpotlightRequests.plural") turns=this.spotlightRequests}}
{{/if}}
{{#if (gt this.characters.length 0)}} {{#if (gt this.characters.length 0)}}
{{> 'systems/daggerheart/templates/ui/combatTracker/combatTrackerSection.hbs' this title=(localize "DAGGERHEART.GENERAL.Character.plural") turns=this.characters}} {{> 'systems/daggerheart/templates/ui/combatTracker/combatTrackerSection.hbs' this title=(localize "DAGGERHEART.GENERAL.Character.plural") turns=this.characters}}
{{/if}} {{/if}}

View file

@ -67,7 +67,6 @@
{{!-- Combat Controls --}} {{!-- Combat Controls --}}
<div class="inner-controls"> <div class="inner-controls">
{{#if hasCombat}} {{#if hasCombat}}
<button data-action="openCountdowns">{{localize "DAGGERHEART.APPLICATIONS.CombatTracker.openCountdowns"}}</button>
<div class="control-buttons right flexrow"> <div class="control-buttons right flexrow">
<div class="spacer"></div> <div class="spacer"></div>
<button type="button" class="encounter-context-menu inline-control combat-control icon fa-solid fa-ellipsis-vertical" <button type="button" class="encounter-context-menu inline-control combat-control icon fa-solid fa-ellipsis-vertical"

View file

@ -0,0 +1,76 @@
<div>
<div class="edit-container">
<header class="dialog-header">
<h1>{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.editTitle"}}</h1>
</header>
<div class="header-tools">
<button class="header-main-button" data-action="addCountdown"><i class="fa-solid fa-plus"></i> {{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.newCountdown"}}</button>
<div class="hide-tools">
<label>{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.hideNewCountdowns"}}</label>
<input type="checkbox" name="hideNewCountdowns" {{checked hideNewCountdowns}} />
</div>
{{#if isGM}}
<div class="default-ownership-tools">
<i class="fa-solid fa-eye" data-tooltip="{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.defaultOwnershipTooltip"}}"></i>
<select name="defaultOwnership">
{{selectOptions ownershipDefaultOptions selected=defaultOwnership labelAttr="label" valueAttr="value" localize=true}}
</select>
</div>
{{/if}}
</div>
<div class="edit-content">
{{#each countdowns as | countdown id | }}
<div class="countdown-edit-container {{#unless countdown.editing}}viewing{{/unless}}" data-id="{{id}}">
<a data-action="editCountdownImage" id="{{id}}"><img src="{{countdown.img}}" /></a>
{{#unless countdown.editing}}
<div class="countdown-edit-text">
<h4>{{countdown.name}}</h4>
<div class="countdown-edit-subtext">
<div class="countdown-edit-sub-tag">{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.currentCountdownValue" value=countdown.progress.current}}</div>
<div class="countdown-edit-sub-tag">{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.currentCountdownMax" value=countdown.progress.max}}</div>
<div class="countdown-edit-sub-tag">{{countdown.typeName}}</div>
<div class="countdown-edit-sub-tag">{{countdown.progress.typeName}}</div>
</div>
</div>
{{else}}
<div class="countdown-edit-input">
<label>{{localize "Name"}}</label>
<input type="text" name="{{concat "countdowns." id ".name"}}" value="{{countdown.name}}" />
</div>
{{/unless}}
<div class="countdown-edit-tools {{#if countdown.editing}}same-row{{/if}}">
<a data-action="toggleCountdownEdit" data-countdown-id="{{id}}"><i class="fa-solid {{#unless countdown.editing}}fa-pen-to-square{{else}}fa-check{{/unless}}"></i></a>
<a data-action="editCountdownOwnership" data-countdown-id="{{id}}"><i class="fa-solid fa-users"></i></a>
<a data-action="removeCountdown" data-countdown-id="{{id}}"><i class="fa-solid fa-trash"></i></a>
</div>
</div>
{{#if countdown.editing}}
<div class="countdown-edit-subrow">
<div class="countdown-edit-input tiny">
<label>{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.current"}}</label>
<input type="number" name="{{concat "countdowns." id ".progress.current"}}" value="{{countdown.progress.current}}" />
</div>
<div class="countdown-edit-input tiny">
<label>{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.max"}}</label>
<input type="number" name="{{concat "countdowns." id ".progress.max"}}" value="{{countdown.progress.max}}" />
</div>
<div class="countdown-edit-input">
<label>{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.category"}}</label>
<select name="{{concat "countdowns." id ".type"}}">
{{selectOptions ../countdownBaseTypes selected=countdown.type valueAttr="id" labelAttr="name" localize=true}}
</select>
</div>
<div class="countdown-edit-input">
<label>{{localize "DAGGERHEART.APPLICATIONS.CountdownEdit.type"}}</label>
<select name="{{concat "countdowns." id ".progress.type"}}">
{{selectOptions ../countdownTypes selected=countdown.progress.type valueAttr="id" labelAttr="label" localize=true}}
</select>
</div>
</div>
{{/if}}
{{/each}}
</div>
</div>
</div>

View file

@ -1,45 +1,33 @@
<div> <div>
{{#if simple}} <div class="countdowns-container">
<div class="minimized-view"> {{#each countdowns as | countdown id |}}
{{#each source.countdowns}} <div class="countdown-container {{#if ../iconOnly}}icon-only{{/if}}">
<a class="mini-countdown-container {{#if (not this.canEdit)}}disabled{{/if}}" data-countdown="{{@key}}"> <div class="countdown-main-container">
<img src="{{this.img}}" /> <img src="{{countdown.img}}" {{#if ../iconOnly}}data-tooltip="{{countdown.name}}"{{/if}}/>
<div class="mini-countdown-name">{{this.name}}</div> <div class="countdown-content">
<div class="mini-countdown-value">{{this.progress.current}}/{{this.progress.max}}</div> {{#unless ../iconOnly}}<label>{{countdown.name}}</label>{{/unless}}
</a> <div class="countdown-tools">
{{/each}} {{#if countdown.editable}}<a data-action="decreaseCountdown" id="{{id}}"><i class="fa-solid fa-minus"></i></a>{{/if}}
</div> <div class="progress-tag">
{{else}} {{countdown.progress.current}}/{{countdown.progress.max}}
<div class="expanded-view">
<div class="countdowns-menu">
{{#if canCreate}}<button class="flex" data-action="addCountdown">{{localize "DAGGERHEART.APPLICATIONS.Countdown.addCountdown"}}</button>{{/if}}
{{#if isGM}}<button data-action="openOwnership" data-tooltip="{{localize "DAGGERHEART.APPLICATIONS.Countdown.openOwnership"}}"><i class="fa-solid fa-users"></i></button>{{/if}}
</div>
<div class="countdowns-container">
{{#each source.countdowns}}
<fieldset class="countdown-fieldset">
<legend>
{{this.name}}
{{#if this.canEdit}}<a><i class="fa-solid fa-trash icon-button" data-action="removeCountdown" data-countdown="{{@key}}"></i></a>{{/if}}
{{#if @root.isGM}}<a><i class="fa-solid fa-users icon-button" data-action="openCountdownOwnership" data-countdown="{{@key}}" data-tooltip="{{localize "DAGGERHEART.APPLICATIONS.Countdown.openOwnership"}}"></i></a>{{/if}}
</legend>
<div class="countdown-container">
<img src="{{this.img}}" {{#if this.canEdit}}data-action='editImage'{{else}}class="disabled"{{/if}} data-countdown="{{@key}}" />
<div class="countdown-inner-container">
{{formGroup @root.countdownFields.name name=(concat @root.base ".countdowns." @key ".name") value=this.name localize=true disabled=(not this.canEdit)}}
<div class="countdown-value-container">
{{formGroup @root.countdownFields.progress.fields.current name=(concat @root.base ".countdowns." @key ".progress.current") value=this.progress.current localize=true disabled=(not this.canEdit)}}
{{formGroup @root.countdownFields.progress.fields.max name=(concat @root.base ".countdowns." @key ".progress.max") value=this.progress.max localize=true disabled=(not this.canEdit)}}
</div>
{{formGroup @root.countdownFields.progress.fields.type.fields.value name=(concat @root.base ".countdowns." @key ".progress.type.value") value=this.progress.type.value localize=true localize=true disabled=(not this.canEdit)}}
</div> </div>
{{#if countdown.editable}}<a data-action="increaseCountdown" id="{{id}}"><i class="fa-solid fa-plus"></i></a>{{/if}}
</div> </div>
</fieldset> </div>
{{/each}} </div>
{{#if (and @root.isGM (not ../iconOnly))}}
{{#if (gt countdown.playerAccess.length 0)}}
<div class="countdown-access-container">
{{#each countdown.playerAccess as |player|}}
<div class="countdown-access" style="{{concat "background: " player.color.css ";"}}" data-tooltip="{{player.name}}"></div>
{{/each}}
</div>
{{/if}}
{{#if countdown.noPlayerAccess}}
<div class="countdown-no-access-container"><i class="fa-solid fa-eye-slash" data-tooltip="{{localize "DAGGERHEART.UI.Countdowns.noPlayerAccess"}}"></i></div>
{{/if}}
{{/if}}
</div> </div>
</div> {{/each}}
{{/if}} </div>
</div> </div>