mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-01-12 03:31:07 +01:00
229 - Narrative Countdown Window Update (#237)
* Improved * Fixed the mode not sticking * Removed console log
This commit is contained in:
parent
ac7fb93635
commit
a79b7189b6
16 changed files with 258 additions and 187 deletions
|
|
@ -1138,6 +1138,7 @@
|
||||||
"RemoveCountdownText": "Are you sure you want to remove the countdown: {name}?",
|
"RemoveCountdownText": "Are you sure you want to remove the countdown: {name}?",
|
||||||
"OpenOwnership": "Edit Player Ownership",
|
"OpenOwnership": "Edit Player Ownership",
|
||||||
"Title": "{type} Countdowns",
|
"Title": "{type} Countdowns",
|
||||||
|
"ToggleSimple": "Toggle Simple View",
|
||||||
"Types": {
|
"Types": {
|
||||||
"narrative": "Narrative",
|
"narrative": "Narrative",
|
||||||
"encounter": "Encounter"
|
"encounter": "Encounter"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { countdownTypes } from '../config/generalConfig.mjs';
|
import { countdownTypes } from '../config/generalConfig.mjs';
|
||||||
import { GMUpdateEvent, RefreshType, socketEvent } from '../helpers/socket.mjs';
|
import { GMUpdateEvent, RefreshType, socketEvent } from '../helpers/socket.mjs';
|
||||||
|
import constructHTMLButton from '../helpers/utils.mjs';
|
||||||
import OwnershipSelection from './ownershipSelection.mjs';
|
import OwnershipSelection from './ownershipSelection.mjs';
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
@ -25,14 +26,15 @@ class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
frame: true,
|
frame: true,
|
||||||
title: 'Countdowns',
|
title: 'Countdowns',
|
||||||
resizable: true,
|
resizable: true,
|
||||||
minimizable: true
|
minimizable: false
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
addCountdown: this.addCountdown,
|
addCountdown: this.addCountdown,
|
||||||
removeCountdown: this.removeCountdown,
|
removeCountdown: this.removeCountdown,
|
||||||
editImage: this.onEditImage,
|
editImage: this.onEditImage,
|
||||||
openOwnership: this.openOwnership,
|
openOwnership: this.openOwnership,
|
||||||
openCountdownOwnership: this.openCountdownOwnership
|
openCountdownOwnership: this.openCountdownOwnership,
|
||||||
|
toggleSimpleView: this.toggleSimpleView
|
||||||
},
|
},
|
||||||
form: { handler: this.updateData, submitOnChange: true }
|
form: { handler: this.updateData, submitOnChange: true }
|
||||||
};
|
};
|
||||||
|
|
@ -53,11 +55,47 @@ class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onFirstRender(context, options) {
|
async _preFirstRender(context, options) {
|
||||||
super._onFirstRender(context, options);
|
options.position =
|
||||||
|
game.user.getFlag(SYSTEM.id, SYSTEM.FLAGS[`${this.basePath}Countdown`].position) ??
|
||||||
|
Countdowns.DEFAULT_OPTIONS.position;
|
||||||
|
|
||||||
this.element.querySelector('.expanded-view').classList.toggle('hidden');
|
const viewSetting =
|
||||||
this.element.querySelector('.minimized-view').classList.toggle('hidden');
|
game.user.getFlag(SYSTEM.id, SYSTEM.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(SYSTEM.id, SYSTEM.FLAGS[`${this.basePath}Countdown`].position, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _renderFrame(options) {
|
||||||
|
const frame = await super._renderFrame(options);
|
||||||
|
|
||||||
|
if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER)) {
|
||||||
|
const button = constructHTMLButton({
|
||||||
|
label: '',
|
||||||
|
classes: ['header-control', 'icon', 'fa-solid', 'fa-wrench'],
|
||||||
|
dataset: { action: 'toggleSimpleView', tooltip: 'DAGGERHEART.Countdown.ToggleSimple' }
|
||||||
|
});
|
||||||
|
this.window.controls.after(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
testUserPermission(level, exact, altSettings) {
|
||||||
|
if (game.user.isGM) return true;
|
||||||
|
|
||||||
|
const settings =
|
||||||
|
altSettings ?? game.settings.get(SYSTEM.id, SYSTEM.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) {
|
async _prepareContext(_options) {
|
||||||
|
|
@ -67,15 +105,17 @@ class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
context.isGM = game.user.isGM;
|
context.isGM = game.user.isGM;
|
||||||
context.base = this.basePath;
|
context.base = this.basePath;
|
||||||
|
|
||||||
context.canCreate = countdownData.playerOwnership[game.user.id].value === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER;
|
context.canCreate = this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true);
|
||||||
context.source = {
|
context.source = {
|
||||||
...countdownData,
|
...countdownData,
|
||||||
countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => {
|
countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => {
|
||||||
const countdown = countdownData.countdowns[key];
|
const countdown = countdownData.countdowns[key];
|
||||||
|
|
||||||
const ownershipValue = countdown.playerOwnership[game.user.id].value;
|
if (this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.LIMITED, false, countdown)) {
|
||||||
if (ownershipValue > CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) {
|
acc[key] = {
|
||||||
acc[key] = { ...countdown, canEdit: ownershipValue === CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER };
|
...countdown,
|
||||||
|
canEdit: this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER, true, countdown)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
|
|
@ -83,7 +123,7 @@ class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
};
|
};
|
||||||
context.systemFields = countdownData.schema.fields;
|
context.systemFields = countdownData.schema.fields;
|
||||||
context.countdownFields = context.systemFields.countdowns.element.fields;
|
context.countdownFields = context.systemFields.countdowns.element.fields;
|
||||||
context.minimized = this.minimized || _options.isFirstRender;
|
context.simple = this.simpleView;
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
@ -110,28 +150,6 @@ class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async minimize() {
|
|
||||||
await super.minimize();
|
|
||||||
|
|
||||||
this.element.querySelector('.expanded-view').classList.toggle('hidden');
|
|
||||||
this.element.querySelector('.minimized-view').classList.toggle('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
async maximize() {
|
|
||||||
if (this.minimized) {
|
|
||||||
const settings = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath];
|
|
||||||
if (settings.playerOwnership[game.user.id].value <= CONST.DOCUMENT_OWNERSHIP_LEVELS.LIMITED) {
|
|
||||||
ui.notifications.info(game.i18n.localize('DAGGERHEART.Countdown.Notifications.LimitedOwnership'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.element.querySelector('.expanded-view').classList.toggle('hidden');
|
|
||||||
this.element.querySelector('.minimized-view').classList.toggle('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
await super.maximize();
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateSetting(update) {
|
async updateSetting(update) {
|
||||||
if (game.user.isGM) {
|
if (game.user.isGM) {
|
||||||
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, update);
|
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns, update);
|
||||||
|
|
@ -213,11 +231,17 @@ class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async toggleSimpleView() {
|
||||||
|
this.simpleView = !this.simpleView;
|
||||||
|
await game.user.setFlag(SYSTEM.id, SYSTEM.FLAGS[`${this.basePath}Countdown`].simple, this.simpleView);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
async updateCountdownValue(event, increase) {
|
async updateCountdownValue(event, increase) {
|
||||||
const countdownSetting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns);
|
const countdownSetting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns);
|
||||||
const countdown = countdownSetting[this.basePath].countdowns[event.currentTarget.dataset.countdown];
|
const countdown = countdownSetting[this.basePath].countdowns[event.currentTarget.dataset.countdown];
|
||||||
|
|
||||||
if (countdown.playerOwnership[game.user.id] < CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER) {
|
if (!this.testUserPermission(CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,6 @@ export class DHRoll extends Roll {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async buildPost(roll, config, message) {
|
static async buildPost(roll, config, message) {
|
||||||
console.log(config)
|
|
||||||
for (const hook of config.hooks) {
|
for (const hook of config.hooks) {
|
||||||
if (Hooks.call(`${SYSTEM.id}.postRoll${hook.capitalize()}`, config, message) === false) return null;
|
if (Hooks.call(`${SYSTEM.id}.postRoll${hook.capitalize()}`, config, message) === false) return null;
|
||||||
}
|
}
|
||||||
|
|
@ -441,9 +440,9 @@ export class DamageRoll extends DHRoll {
|
||||||
static async postEvaluate(roll, config = {}) {
|
static async postEvaluate(roll, config = {}) {
|
||||||
super.postEvaluate(roll, config);
|
super.postEvaluate(roll, config);
|
||||||
config.roll.type = config.type;
|
config.roll.type = config.type;
|
||||||
if(config.source?.message) {
|
if (config.source?.message) {
|
||||||
const chatMessage = ui.chat.collection.get(config.source.message);
|
const chatMessage = ui.chat.collection.get(config.source.message);
|
||||||
chatMessage.update({'system.damage': config});
|
chatMessage.update({ 'system.damage': config });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1,9 @@
|
||||||
export const displayDomainCardsAsList = 'displayDomainCardsAsList';
|
export const displayDomainCardsAsList = 'displayDomainCardsAsList';
|
||||||
|
export const narrativeCountdown = {
|
||||||
|
simple: 'countdown-narrative-simple',
|
||||||
|
position: 'countdown-narrative-position'
|
||||||
|
};
|
||||||
|
export const encounterCountdown = {
|
||||||
|
simple: 'countdown-encounter-simple',
|
||||||
|
position: 'countdown-encounter-position'
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -403,11 +403,18 @@ export class DHBaseAction extends foundry.abstract.DataModel {
|
||||||
hasCost(costs) {
|
hasCost(costs) {
|
||||||
const realCosts = this.getRealCosts(costs),
|
const realCosts = this.getRealCosts(costs),
|
||||||
hasFearCost = realCosts.findIndex(c => c.type === 'fear');
|
hasFearCost = realCosts.findIndex(c => c.type === 'fear');
|
||||||
if(hasFearCost > -1) {
|
if (hasFearCost > -1) {
|
||||||
const fearCost = realCosts.splice(hasFearCost, 1);
|
const fearCost = realCosts.splice(hasFearCost, 1);
|
||||||
if(!game.user.isGM || fearCost[0].total > game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear)) return false;
|
if (
|
||||||
|
!game.user.isGM ||
|
||||||
|
fearCost[0].total > game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear)
|
||||||
|
)
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return realCosts.reduce((a, c) => a && this.actor.system.resources[c.type]?.value >= (c.total ?? c.value), true);
|
return realCosts.reduce(
|
||||||
|
(a, c) => a && this.actor.system.resources[c.type]?.value >= (c.total ?? c.value),
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
/* COST */
|
/* COST */
|
||||||
|
|
||||||
|
|
@ -499,19 +506,25 @@ export class DHBaseAction extends foundry.abstract.DataModel {
|
||||||
|
|
||||||
/* SAVE */
|
/* SAVE */
|
||||||
async rollSave(target, event, message) {
|
async rollSave(target, event, message) {
|
||||||
if(!target?.actor) return;
|
if (!target?.actor) return;
|
||||||
return target.actor.diceRoll({
|
return target.actor
|
||||||
event,
|
.diceRoll({
|
||||||
title: 'Roll Save',
|
event,
|
||||||
roll: {
|
title: 'Roll Save',
|
||||||
trait: this.save.trait,
|
roll: {
|
||||||
difficulty: this.save.difficulty,
|
trait: this.save.trait,
|
||||||
type: "reaction"
|
difficulty: this.save.difficulty,
|
||||||
},
|
type: 'reaction'
|
||||||
data: target.actor.getRollData()
|
},
|
||||||
}).then(async (result) => {
|
data: target.actor.getRollData()
|
||||||
if(result) this.updateChatMessage(message, target.id, {result: result.roll.total, success: result.roll.success});
|
})
|
||||||
})
|
.then(async result => {
|
||||||
|
if (result)
|
||||||
|
this.updateChatMessage(message, target.id, {
|
||||||
|
result: result.roll.total,
|
||||||
|
success: result.roll.success
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateChatMessage(message, targetId, changes, chain = true) {
|
async updateChatMessage(message, targetId, changes, chain = true) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { DHBaseAction } from "../action/action.mjs";
|
import { DHBaseAction } from '../action/action.mjs';
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
|
@ -42,6 +42,9 @@ export default class DHAdversaryRoll extends foundry.abstract.TypeDataModel {
|
||||||
|
|
||||||
prepareDerivedData() {
|
prepareDerivedData() {
|
||||||
this.hasHitTarget = this.targets.filter(t => t.hit === true).length > 0;
|
this.hasHitTarget = this.targets.filter(t => t.hit === true).length > 0;
|
||||||
this.currentTargets = this.targetSelection !== true ? Array.from(game.user.targets).map(t => DHBaseAction.formatTarget(t)) : this.targets;
|
this.currentTargets =
|
||||||
|
this.targetSelection !== true
|
||||||
|
? Array.from(game.user.targets).map(t => DHBaseAction.formatTarget(t))
|
||||||
|
: this.targets;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ export default class DHDamageRoll extends foundry.abstract.TypeDataModel {
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
messageType: new fields.StringField({initial: 'damage'}),
|
messageType: new fields.StringField({ initial: 'damage' }),
|
||||||
title: new fields.StringField(),
|
title: new fields.StringField(),
|
||||||
roll: new fields.DataField({}),
|
roll: new fields.DataField({}),
|
||||||
targets: new fields.ArrayField(
|
targets: new fields.ArrayField(
|
||||||
|
|
@ -28,7 +28,7 @@ export default class DHDamageRoll extends foundry.abstract.TypeDataModel {
|
||||||
action: new fields.StringField(),
|
action: new fields.StringField(),
|
||||||
message: new fields.StringField()
|
message: new fields.StringField()
|
||||||
}),
|
}),
|
||||||
directDamage: new fields.BooleanField({initial: true})
|
directDamage: new fields.BooleanField({ initial: true })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,6 +38,9 @@ export default class DHDamageRoll extends foundry.abstract.TypeDataModel {
|
||||||
|
|
||||||
prepareDerivedData() {
|
prepareDerivedData() {
|
||||||
this.hasHitTarget = this.targets.filter(t => t.hit === true).length > 0;
|
this.hasHitTarget = this.targets.filter(t => t.hit === true).length > 0;
|
||||||
this.currentTargets = this.targetSelection !== true ? Array.from(game.user.targets).map(t => DHBaseAction.formatTarget(t)) : this.targets;
|
this.currentTargets =
|
||||||
|
this.targetSelection !== true
|
||||||
|
? Array.from(game.user.targets).map(t => DHBaseAction.formatTarget(t))
|
||||||
|
: this.targets;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import DHAdversaryRoll from "./adversaryRoll.mjs";
|
import DHAdversaryRoll from './adversaryRoll.mjs';
|
||||||
|
|
||||||
export default class DHDualityRoll extends DHAdversaryRoll {
|
export default class DHDualityRoll extends DHAdversaryRoll {
|
||||||
get messageTemplate() {
|
get messageTemplate() {
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ class DhCountdownData extends foundry.abstract.DataModel {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
}),
|
||||||
|
window: new fields.SchemaField({})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -485,7 +485,9 @@ export default class DhpActor extends Actor {
|
||||||
resources.forEach(r => {
|
resources.forEach(r => {
|
||||||
switch (r.type) {
|
switch (r.type) {
|
||||||
case 'fear':
|
case 'fear':
|
||||||
ui.resources.updateFear(game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear) + r.value);
|
ui.resources.updateFear(
|
||||||
|
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear) + r.value
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'armorStack':
|
case 'armorStack':
|
||||||
updates.armor.resources['system.marks.value'] = Math.max(
|
updates.armor.resources['system.marks.value'] = Math.max(
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,28 @@ export const damageKeyToNumber = key => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default function constructHTMLButton({
|
||||||
|
label,
|
||||||
|
dataset = {},
|
||||||
|
classes = [],
|
||||||
|
icon = '',
|
||||||
|
type = 'button',
|
||||||
|
disabled = false
|
||||||
|
}) {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.type = type;
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(dataset)) {
|
||||||
|
button.dataset[key] = value;
|
||||||
|
}
|
||||||
|
button.classList.add(...classes);
|
||||||
|
if (icon) icon = `<i class="${icon}"></i> `;
|
||||||
|
if (disabled) button.disabled = true;
|
||||||
|
button.innerHTML = `${icon}${label}`;
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
export const adjustDice = (dice, decrease) => {
|
export const adjustDice = (dice, decrease) => {
|
||||||
const diceKeys = Object.keys(diceTypes);
|
const diceKeys = Object.keys(diceTypes);
|
||||||
const index = diceKeys.indexOf(dice);
|
const index = diceKeys.indexOf(dice);
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
||||||
element.addEventListener('click', this.clickTarget);
|
element.addEventListener('click', this.clickTarget);
|
||||||
});
|
});
|
||||||
html.querySelectorAll('.button-target-selection').forEach(element => {
|
html.querySelectorAll('.button-target-selection').forEach(element => {
|
||||||
element.addEventListener('click', event => this.onTargetSelection(event, data.message))
|
element.addEventListener('click', event => this.onTargetSelection(event, data.message));
|
||||||
});
|
});
|
||||||
html.querySelectorAll('.damage-button').forEach(element =>
|
html.querySelectorAll('.damage-button').forEach(element =>
|
||||||
element.addEventListener('click', event => this.onDamage(event, data.message))
|
element.addEventListener('click', event => this.onDamage(event, data.message))
|
||||||
|
|
@ -122,11 +122,13 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
||||||
|
|
||||||
onRollAllSave = async (event, message) => {
|
onRollAllSave = async (event, message) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const targets = event.target.parentElement.querySelectorAll('.target-section > [data-token] .target-save-container');
|
const targets = event.target.parentElement.querySelectorAll(
|
||||||
targets.forEach((el) => {
|
'.target-section > [data-token] .target-save-container'
|
||||||
el.dispatchEvent(new PointerEvent("click", { shiftKey: true}))
|
);
|
||||||
})
|
targets.forEach(el => {
|
||||||
}
|
el.dispatchEvent(new PointerEvent('click', { shiftKey: true }));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
onApplyEffect = async (event, message) => {
|
onApplyEffect = async (event, message) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
@ -146,18 +148,26 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const targetSelection = Boolean(event.target.dataset.targetHit),
|
const targetSelection = Boolean(event.target.dataset.targetHit),
|
||||||
msg = ui.chat.collection.get(message._id);
|
msg = ui.chat.collection.get(message._id);
|
||||||
if(msg.system.targetSelection === targetSelection) return;
|
if (msg.system.targetSelection === targetSelection) return;
|
||||||
if(targetSelection !== true && !Array.from(game.user.targets).length) return ui.notifications.info(game.i18n.localize('DAGGERHEART.Notification.Info.NoTargetsSelected'));
|
if (targetSelection !== true && !Array.from(game.user.targets).length)
|
||||||
|
return ui.notifications.info(game.i18n.localize('DAGGERHEART.Notification.Info.NoTargetsSelected'));
|
||||||
msg.system.targetSelection = targetSelection;
|
msg.system.targetSelection = targetSelection;
|
||||||
msg.system.prepareDerivedData();
|
msg.system.prepareDerivedData();
|
||||||
ui.chat.updateMessage(msg);
|
ui.chat.updateMessage(msg);
|
||||||
}
|
};
|
||||||
|
|
||||||
getTargetList = (event, message) => {
|
getTargetList = (event, message) => {
|
||||||
const targetSelection = event.target.closest('.message-content').querySelector('.button-target-selection.target-selected'),
|
const targetSelection = event.target
|
||||||
|
.closest('.message-content')
|
||||||
|
.querySelector('.button-target-selection.target-selected'),
|
||||||
isHit = Boolean(targetSelection.dataset.targetHit);
|
isHit = Boolean(targetSelection.dataset.targetHit);
|
||||||
return {isHit, targets: isHit ? message.system.targets.filter(t => t.hit === true).map(target => game.canvas.tokens.get(target.id)) : Array.from(game.user.targets)};
|
return {
|
||||||
}
|
isHit,
|
||||||
|
targets: isHit
|
||||||
|
? message.system.targets.filter(t => t.hit === true).map(target => game.canvas.tokens.get(target.id))
|
||||||
|
: Array.from(game.user.targets)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
hoverTarget = event => {
|
hoverTarget = event => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
@ -185,9 +195,11 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const { isHit, targets } = this.getTargetList(event, message);
|
const { isHit, targets } = this.getTargetList(event, message);
|
||||||
|
|
||||||
if(message.system.onSave && isHit) {
|
if (message.system.onSave && isHit) {
|
||||||
const pendingingSaves = message.system.targets.filter(target => target.hit && target.saved.success === null);
|
const pendingingSaves = message.system.targets.filter(
|
||||||
if(pendingingSaves.length) {
|
target => target.hit && target.saved.success === null
|
||||||
|
);
|
||||||
|
if (pendingingSaves.length) {
|
||||||
const confirm = await foundry.applications.api.DialogV2.confirm({
|
const confirm = await foundry.applications.api.DialogV2.confirm({
|
||||||
window: { title: 'Pending Reaction Rolls found' },
|
window: { title: 'Pending Reaction Rolls found' },
|
||||||
content: `<p>Some Tokens still need to roll their Reaction Roll.</p><p>Are you sure you want to continue ?</p><p><i>Undone reaction rolls will be considered as failed</i></p>`
|
content: `<p>Some Tokens still need to roll their Reaction Roll.</p><p>Are you sure you want to continue ?</p><p><i>Undone reaction rolls will be considered as failed</i></p>`
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,15 @@ fieldset.daggerheart.chat {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
&:before, &:after {
|
&:before,
|
||||||
|
&:after {
|
||||||
content: '\f0d8';
|
content: '\f0d8';
|
||||||
font-family: "Font Awesome 6 Pro";
|
font-family: 'Font Awesome 6 Pro';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.expanded {
|
&.expanded {
|
||||||
legend:before, legend:after {
|
legend:before,
|
||||||
|
legend:after {
|
||||||
content: '\f0d7';
|
content: '\f0d7';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -229,20 +231,20 @@ fieldset.daggerheart.chat {
|
||||||
.target-selection {
|
.target-selection {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
input[type="radio"] {
|
input[type='radio'] {
|
||||||
display: none;
|
display: none;
|
||||||
&:checked + label {
|
&:checked + label {
|
||||||
text-shadow: 0px 0px 4px #CE5937;
|
text-shadow: 0px 0px 4px #ce5937;
|
||||||
}
|
}
|
||||||
&:not(:checked) + label {
|
&:not(:checked) + label {
|
||||||
opacity: .75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: .75;
|
opacity: 0.75;
|
||||||
&.target-selected {
|
&.target-selected {
|
||||||
text-shadow: 0px 0px 4px #CE5937;
|
text-shadow: 0px 0px 4px #ce5937;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -273,7 +275,8 @@ fieldset.daggerheart.chat {
|
||||||
background: @miss;
|
background: @miss;
|
||||||
}
|
}
|
||||||
|
|
||||||
img, .target-save-container {
|
img,
|
||||||
|
.target-save-container {
|
||||||
width: 22px;
|
width: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|
@ -401,7 +404,7 @@ fieldset.daggerheart.chat {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
&::after {
|
&::after {
|
||||||
content: "??";
|
content: '??';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -414,7 +417,8 @@ fieldset.daggerheart.chat {
|
||||||
border-top-width: 0;
|
border-top-width: 0;
|
||||||
display: contents;
|
display: contents;
|
||||||
legend {
|
legend {
|
||||||
&:before, &:after {
|
&:before,
|
||||||
|
&:after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
.theme-light {
|
.theme-light {
|
||||||
.daggerheart.dh-style.countdown {
|
.daggerheart.dh-style.countdown {
|
||||||
&.minimized .minimized-view .mini-countdown-container {
|
.minimized-view .mini-countdown-container {
|
||||||
background-image: url('../assets/parchments/dh-parchment-dark.png');
|
background-image: url('../assets/parchments/dh-parchment-dark.png');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -26,51 +26,38 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.minimized {
|
.minimized-view {
|
||||||
height: auto !important;
|
display: flex;
|
||||||
max-height: unset !important;
|
gap: 8px;
|
||||||
max-width: 740px !important;
|
flex-wrap: wrap;
|
||||||
width: auto !important;
|
|
||||||
|
|
||||||
.window-content {
|
.mini-countdown-container {
|
||||||
display: flex;
|
width: fit-content;
|
||||||
padding: 4px 8px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minimized-view {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex-wrap: wrap;
|
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;
|
||||||
|
|
||||||
.mini-countdown-container {
|
&.disabled {
|
||||||
width: fit-content;
|
cursor: initial;
|
||||||
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 {
|
img {
|
||||||
cursor: initial;
|
width: 30px;
|
||||||
}
|
height: 30px;
|
||||||
|
border-radius: 6px 0 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
.mini-countdown-name {
|
||||||
width: 30px;
|
white-space: nowrap;
|
||||||
height: 30px;
|
}
|
||||||
border-radius: 6px 0 0 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mini-countdown-name {
|
.mini-countdown-value {
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mini-countdown-value {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1414,7 +1414,7 @@ fieldset.daggerheart.chat legend {
|
||||||
fieldset.daggerheart.chat legend:before,
|
fieldset.daggerheart.chat legend:before,
|
||||||
fieldset.daggerheart.chat legend:after {
|
fieldset.daggerheart.chat legend:after {
|
||||||
content: '\f0d8';
|
content: '\f0d8';
|
||||||
font-family: "Font Awesome 6 Pro";
|
font-family: 'Font Awesome 6 Pro';
|
||||||
}
|
}
|
||||||
fieldset.daggerheart.chat.expanded legend:before,
|
fieldset.daggerheart.chat.expanded legend:before,
|
||||||
fieldset.daggerheart.chat.expanded legend:after {
|
fieldset.daggerheart.chat.expanded legend:after {
|
||||||
|
|
@ -1559,13 +1559,13 @@ fieldset.daggerheart.chat .daggerheart.chat {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
}
|
}
|
||||||
.daggerheart.chat.roll .target-selection input[type="radio"] {
|
.daggerheart.chat.roll .target-selection input[type='radio'] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.daggerheart.chat.roll .target-selection input[type="radio"]:checked + label {
|
.daggerheart.chat.roll .target-selection input[type='radio']:checked + label {
|
||||||
text-shadow: 0px 0px 4px #CE5937;
|
text-shadow: 0px 0px 4px #ce5937;
|
||||||
}
|
}
|
||||||
.daggerheart.chat.roll .target-selection input[type="radio"]:not(:checked) + label {
|
.daggerheart.chat.roll .target-selection input[type='radio']:not(:checked) + label {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
.daggerheart.chat.roll .target-selection label {
|
.daggerheart.chat.roll .target-selection label {
|
||||||
|
|
@ -1573,7 +1573,7 @@ fieldset.daggerheart.chat .daggerheart.chat {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
.daggerheart.chat.roll .target-selection label.target-selected {
|
.daggerheart.chat.roll .target-selection label.target-selected {
|
||||||
text-shadow: 0px 0px 4px #CE5937;
|
text-shadow: 0px 0px 4px #ce5937;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.daggerheart.chat.roll .target-section {
|
.daggerheart.chat.roll .target-section {
|
||||||
|
|
@ -1700,7 +1700,7 @@ fieldset.daggerheart.chat .daggerheart.chat {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.daggerheart.chat [data-view-perm='false']::after {
|
.daggerheart.chat [data-view-perm='false']::after {
|
||||||
content: "??";
|
content: '??';
|
||||||
}
|
}
|
||||||
.theme-colorful .chat-message.duality {
|
.theme-colorful .chat-message.duality {
|
||||||
border-color: black;
|
border-color: black;
|
||||||
|
|
@ -3474,7 +3474,7 @@ div.daggerheart.views.multiclass {
|
||||||
#resources:has(.fear-bar) {
|
#resources:has(.fear-bar) {
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
.theme-light .daggerheart.dh-style.countdown.minimized .minimized-view .mini-countdown-container {
|
.theme-light .daggerheart.dh-style.countdown .minimized-view .mini-countdown-container {
|
||||||
background-image: url('../assets/parchments/dh-parchment-dark.png');
|
background-image: url('../assets/parchments/dh-parchment-dark.png');
|
||||||
}
|
}
|
||||||
.daggerheart.dh-style.countdown {
|
.daggerheart.dh-style.countdown {
|
||||||
|
|
@ -3494,23 +3494,12 @@ div.daggerheart.views.multiclass {
|
||||||
.daggerheart.dh-style.countdown fieldset legend a {
|
.daggerheart.dh-style.countdown fieldset legend a {
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
.daggerheart.dh-style.countdown.minimized {
|
.daggerheart.dh-style.countdown .minimized-view {
|
||||||
height: auto !important;
|
|
||||||
max-height: unset !important;
|
|
||||||
max-width: 740px !important;
|
|
||||||
width: auto !important;
|
|
||||||
}
|
|
||||||
.daggerheart.dh-style.countdown.minimized .window-content {
|
|
||||||
display: flex;
|
|
||||||
padding: 4px 8px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.daggerheart.dh-style.countdown.minimized .minimized-view {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.daggerheart.dh-style.countdown.minimized .minimized-view .mini-countdown-container {
|
.daggerheart.dh-style.countdown .minimized-view .mini-countdown-container {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -3522,15 +3511,15 @@ div.daggerheart.views.multiclass {
|
||||||
color: light-dark(#efe6d8, #222);
|
color: light-dark(#efe6d8, #222);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.daggerheart.dh-style.countdown.minimized .minimized-view .mini-countdown-container.disabled {
|
.daggerheart.dh-style.countdown .minimized-view .mini-countdown-container.disabled {
|
||||||
cursor: initial;
|
cursor: initial;
|
||||||
}
|
}
|
||||||
.daggerheart.dh-style.countdown.minimized .minimized-view .mini-countdown-container img {
|
.daggerheart.dh-style.countdown .minimized-view .mini-countdown-container img {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
border-radius: 6px 0 0 6px;
|
border-radius: 6px 0 0 6px;
|
||||||
}
|
}
|
||||||
.daggerheart.dh-style.countdown.minimized .minimized-view .mini-countdown-container .mini-countdown-name {
|
.daggerheart.dh-style.countdown .minimized-view .mini-countdown-container .mini-countdown-name {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.daggerheart.dh-style.countdown .hidden {
|
.daggerheart.dh-style.countdown .hidden {
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,45 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="expanded-view {{#if minimized}}hidden{{/if}}">
|
{{#if simple}}
|
||||||
<div class="countdowns-menu">
|
<div class="minimized-view">
|
||||||
{{#if canCreate}}<button class="flex" data-action="addCountdown">{{localize "DAGGERHEART.Countdown.AddCountdown"}}</button>{{/if}}
|
|
||||||
{{#if isGM}}<button data-action="openOwnership" data-tooltip="{{localize "DAGGERHEART.Countdown.OpenOwnership"}}"><i class="fa-solid fa-users"></i></button>{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="countdowns-container">
|
|
||||||
{{#each source.countdowns}}
|
{{#each source.countdowns}}
|
||||||
<fieldset class="countdown-fieldset">
|
<a class="mini-countdown-container {{#if (not this.canEdit)}}disabled{{/if}}" data-countdown="{{@key}}">
|
||||||
<legend>
|
<img src="{{this.img}}" />
|
||||||
{{this.name}}
|
<div class="mini-countdown-name">{{this.name}}</div>
|
||||||
{{#if this.canEdit}}<a><i class="fa-solid fa-trash icon-button" data-action="removeCountdown" data-countdown="{{@key}}"></i></a>{{/if}}
|
<div class="mini-countdown-value">{{this.progress.current}}/{{this.progress.max}}</div>
|
||||||
{{#if @root.isGM}}<a><i class="fa-solid fa-users icon-button" data-action="openCountdownOwnership" data-countdown="{{@key}}" data-tooltip="{{localize "DAGGERHEART.Countdown.OpenOwnership"}}"></i></a>{{/if}}
|
</a>
|
||||||
</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>
|
|
||||||
</fieldset>
|
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{{else}}
|
||||||
<div class="minimized-view {{#if (not minimized)}}hidden{{/if}}">
|
<div class="expanded-view">
|
||||||
{{#each source.countdowns}}
|
<div class="countdowns-menu">
|
||||||
<a class="mini-countdown-container {{#if (not this.canEdit)}}disabled{{/if}}" data-countdown="{{@key}}">
|
{{#if canCreate}}<button class="flex" data-action="addCountdown">{{localize "DAGGERHEART.Countdown.AddCountdown"}}</button>{{/if}}
|
||||||
<img src="{{this.img}}" />
|
{{#if isGM}}<button data-action="openOwnership" data-tooltip="{{localize "DAGGERHEART.Countdown.OpenOwnership"}}"><i class="fa-solid fa-users"></i></button>{{/if}}
|
||||||
<div class="mini-countdown-name">{{this.name}}</div>
|
</div>
|
||||||
<div class="mini-countdown-value">{{this.progress.current}}/{{this.progress.max}}</div>
|
|
||||||
</a>
|
<div class="countdowns-container">
|
||||||
{{/each}}
|
{{#each source.countdowns}}
|
||||||
</div>
|
<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.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>
|
||||||
|
</fieldset>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue