Merge branch 'v13-Dev' into effect-click-simpler

This commit is contained in:
WBHarry 2026-03-25 10:56:24 +01:00
commit ab502d03fb
84 changed files with 622 additions and 617 deletions

View file

@ -70,8 +70,8 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.rollConfig = this.config;
context.hasRoll = !!this.config.roll;
context.canRoll = true;
context.selectedRollMode = this.config.selectedRollMode ?? game.settings.get('core', 'rollMode');
context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({
context.selectedMessageMode = this.config.selectedMessageMode ?? game.settings.get('core', 'messageMode');
context.rollModes = Object.entries(CONFIG.ChatMessage.modes).map(([action, { label, icon }]) => ({
action,
label,
icon
@ -142,10 +142,10 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
}));
}
static updateRollConfiguration(event, _, formData) {
static updateRollConfiguration(_event, _, formData) {
const { ...rest } = foundry.utils.expandObject(formData.object);
this.config.selectedRollMode = rest.selectedRollMode;
this.config.selectedMessageMode = rest.selectedMessageMode;
if (this.config.costs) {
this.config.costs = foundry.utils.mergeObject(this.config.costs, rest.costs);

View file

@ -52,8 +52,8 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
context.formula = this.roll.constructFormula(this.config);
context.hasHealing = this.config.hasHealing;
context.directDamage = this.config.directDamage;
context.selectedRollMode = this.config.selectedRollMode;
context.rollModes = Object.entries(CONFIG.Dice.rollModes).map(([action, { label, icon }]) => ({
context.selectedMessageMode = this.config.selectedMessageMode;
context.rollModes = Object.entries(CONFIG.ChatMessage.modes).map(([action, { label, icon }]) => ({
action,
label,
icon
@ -69,7 +69,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
const { ...rest } = foundry.utils.expandObject(formData.object);
foundry.utils.mergeObject(this.config.roll, rest.roll);
foundry.utils.mergeObject(this.config.modifiers, rest.modifiers);
this.config.selectedRollMode = rest.selectedRollMode;
this.config.selectedMessageMode = rest.selectedMessageMode;
this.render();
}

View file

@ -58,6 +58,10 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
id: 'initialization',
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/initialization.hbs'
},
rollSelection: {
id: 'rollSelection',
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/rollSelection.hbs'
},
tagTeamRoll: {
id: 'tagTeamRoll',
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs'
@ -78,15 +82,52 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
element.addEventListener('change', this.updateRollType.bind(this));
}
_configureRenderParts(options) {
const { initialization, rollSelection, tagTeamRoll } = super._configureRenderParts(options);
const augmentedParts = { initialization };
for (const memberKey of Object.keys(this.party.system.tagTeam.members)) {
augmentedParts[memberKey] = {
id: memberKey,
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/tagTeamMember.hbs'
};
}
augmentedParts.rollSelection = rollSelection;
augmentedParts.tagTeamRoll = tagTeamRoll;
return augmentedParts;
}
/**@inheritdoc */
async _onRender(context, options) {
await super._onRender(context, options);
if (this.element.querySelector('.team-container')) return;
const initializationPart = this.element.querySelector('.initialization-container');
initializationPart.insertAdjacentHTML('afterend', '<div class="team-container"></div>');
const teamContainer = this.element.querySelector('.team-container');
for (const memberContainer of this.element.querySelectorAll('.team-member-container'))
teamContainer.appendChild(memberContainer);
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.isEditable = this.getIsEditable();
context.fields = this.party.system.schema.fields.tagTeam.fields;
context.data = this.party.system.tagTeam;
context.rollTypes = CONFIG.DH.GENERAL.tagTeamRollTypes;
context.traitOptions = CONFIG.DH.ACTOR.abilities;
context.members = {};
context.allHaveRolled = Object.keys(this.party.system.tagTeam.members).every(key => {
const data = this.party.system.tagTeam.members[key];
return Boolean(data.rollData);
});
return context;
}
async _preparePartContext(partId, context, options) {
const partContext = await super._preparePartContext(partId, context, options);
partContext.partId = partId;
switch (partId) {
case 'initialization':
partContext.tagTeamFields = this.party.system.schema.fields.tagTeam.fields;
@ -100,66 +141,20 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
partContext.initiatorDisabled = !selectedMembers.length;
partContext.openForAllPlayers = this.openForAllPlayers;
break;
case 'rollSelection':
partContext.members = Object.keys(this.party.system.tagTeam.members).reduce((acc, key) => {
const member = this.party.system.tagTeam.members[key];
acc[key] = { selected: member.selected };
return acc;
}, {});
break;
case 'tagTeamRoll':
partContext.fields = this.party.system.schema.fields.tagTeam.fields;
partContext.data = this.party.system.tagTeam;
partContext.rollTypes = CONFIG.DH.GENERAL.tagTeamRollTypes;
partContext.traitOptions = CONFIG.DH.ACTOR.abilities;
const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected);
const critSelected = !selectedRoll
? undefined
: (selectedRoll?.rollData?.options?.roll?.isCritical ?? false);
partContext.members = {};
for (const actorId in this.party.system.tagTeam.members) {
const data = this.party.system.tagTeam.members[actorId];
const actor = game.actors.get(actorId);
const rollOptions = [];
const damageRollOptions = [];
for (const item of actor.items) {
if (item.system.metadata.hasActions) {
const actions = [
...item.system.actions,
...(item.system.attack ? [item.system.attack] : [])
];
for (const action of actions) {
if (action.hasRoll) {
const actionItem = {
value: action.uuid,
label: action.name,
group: item.name,
baseAction: action.baseAction
};
if (action.hasDamage) damageRollOptions.push(actionItem);
else rollOptions.push(actionItem);
}
}
}
}
const damage = data.rollData?.options?.damage;
partContext.hasDamage |= Boolean(damage);
const critHitPointsDamage = await this.getCriticalDamage(damage);
partContext.members[actorId] = {
...data,
isEditable: actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER),
key: actorId,
readyToRoll: Boolean(data.rollChoice),
hasRolled: Boolean(data.rollData),
rollOptions,
damageRollOptions,
damage: damage,
critDamage: critHitPointsDamage,
useCritDamage:
critSelected || (critSelected === undefined && data.rollData?.options?.roll?.isCritical)
};
}
partContext.hintText = await this.getInfoTexts(this.party.system.tagTeam.members);
partContext.joinedRoll = await this.getJoinedRoll({
overrideIsCritical: critSelected,
@ -169,24 +164,85 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
break;
}
if (Object.keys(this.party.system.tagTeam.members).includes(partId)) {
const data = this.party.system.tagTeam.members[partId];
const actor = game.actors.get(partId);
const rollOptions = [];
const damageRollOptions = [];
for (const item of actor.items) {
if (item.system.metadata.hasActions) {
const actions = [...item.system.actions, ...(item.system.attack ? [item.system.attack] : [])];
for (const action of actions) {
if (action.hasRoll) {
const actionItem = {
value: action.uuid,
label: action.name,
group: item.name,
baseAction: action.baseAction
};
if (action.hasDamage) damageRollOptions.push(actionItem);
else rollOptions.push(actionItem);
}
}
}
}
const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected);
const critSelected = !selectedRoll
? undefined
: (selectedRoll?.rollData?.options?.roll?.isCritical ?? false);
const damage = data.rollData?.options?.damage;
partContext.hasDamage |= Boolean(damage);
const critHitPointsDamage = await this.getCriticalDamage(damage);
partContext.members[partId] = {
...data,
isEditable: actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER),
key: partId,
readyToRoll: Boolean(data.rollChoice),
hasRolled: Boolean(data.rollData),
rollOptions,
damageRollOptions,
damage: damage,
critDamage: critHitPointsDamage,
useCritDamage: critSelected || (critSelected === undefined && data.rollData?.options?.roll?.isCritical)
};
}
return partContext;
}
static async updateData(_event, _, formData) {
getUpdatingParts(target) {
const { initialization, rollSelection, tagTeamRoll } = this.constructor.PARTS;
const isInitialization = this.tabGroups.application === initialization.id;
const updatingMember = target.closest('.team-member-container')?.dataset?.memberKey;
return [
...(isInitialization ? [initialization.id] : []),
...(updatingMember ? [updatingMember] : []),
...(!isInitialization ? [rollSelection.id] : []),
...(!isInitialization ? [tagTeamRoll.id] : [])
];
}
static async updateData(event, _, formData) {
const { initiator, openForAllPlayers, ...partyData } = foundry.utils.expandObject(formData.object);
this.initiator = initiator;
this.openForAllPlayers = openForAllPlayers !== undefined ? openForAllPlayers : this.openForAllPlayers;
this.updatePartyData(partyData);
this.updatePartyData(partyData, this.getUpdatingParts(event.target));
}
async updatePartyData(update, options = { render: true }) {
async updatePartyData(update, updatingParts, options = { render: true }) {
const gmUpdate = async update => {
await this.party.update(update);
this.render();
this.render({ parts: updatingParts });
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: { refreshType: RefreshType.TagTeamRoll, action: 'refresh' }
data: { refreshType: RefreshType.TagTeamRoll, action: 'refresh', parts: updatingParts }
});
};
@ -195,7 +251,9 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
gmUpdate,
update,
this.party.uuid,
options.render ? { refreshType: RefreshType.TagTeamRoll, action: 'refresh' } : undefined
options.render
? { refreshType: RefreshType.TagTeamRoll, action: 'refresh', parts: updatingParts }
: undefined
);
}
@ -206,7 +264,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
});
}
tagTeamRefresh = ({ refreshType, action }) => {
tagTeamRefresh = ({ refreshType, action, parts }) => {
if (refreshType !== RefreshType.TagTeamRoll) return;
switch (action) {
@ -214,7 +272,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
this.tabGroups.application = 'tagTeamRoll';
break;
case 'refresh':
this.render();
this.render({ parts });
break;
case 'close':
this.close();
@ -304,22 +362,28 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
}
async updateRollType(event) {
this.updatePartyData({
[`system.tagTeam.members.${event.target.dataset.member}`]: {
rollType: event.target.value,
rollChoice: null
}
});
this.updatePartyData(
{
[`system.tagTeam.members.${event.target.dataset.member}`]: {
rollType: event.target.value,
rollChoice: null
}
},
this.getUpdatingParts(event.target)
);
}
static async #removeRoll(_, button) {
this.updatePartyData({
[`system.tagTeam.members.${button.dataset.member}`]: {
rollData: null,
rollChoice: null,
selected: false
}
});
this.updatePartyData(
{
[`system.tagTeam.members.${button.dataset.member}`]: {
rollData: null,
rollChoice: null,
selected: false
}
},
this.getUpdatingParts(button)
);
}
static async #makeRoll(event, button) {
@ -342,9 +406,12 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
const rollData = result.messageRoll.toJSON();
delete rollData.options.messageRoll;
this.updatePartyData({
[`system.tagTeam.members.${member}.rollData`]: rollData
});
this.updatePartyData(
{
[`system.tagTeam.members.${member}.rollData`]: rollData
},
this.getUpdatingParts(button)
);
}
async makeTraitRoll(memberKey) {
@ -389,15 +456,18 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
diceType
);
const rollData = parsedRoll.toJSON();
this.updatePartyData({
[`system.tagTeam.members.${member}.rollData`]: {
...rollData,
options: {
...rollData.options,
roll: newRoll
this.updatePartyData(
{
[`system.tagTeam.members.${member}.rollData`]: {
...rollData,
options: {
...rollData.options,
roll: newRoll
}
}
}
});
},
this.getUpdatingParts(button)
);
}
static async #makeDamageRoll(event, button) {
@ -423,29 +493,35 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
if (!config.damage) return;
const current = this.party.system.tagTeam.members[memberKey].rollData;
await this.updatePartyData({
[`system.tagTeam.members.${memberKey}.rollData`]: {
...current,
options: {
...current.options,
damage: config.damage
await this.updatePartyData(
{
[`system.tagTeam.members.${memberKey}.rollData`]: {
...current,
options: {
...current.options,
damage: config.damage
}
}
}
});
},
this.getUpdatingParts(button)
);
}
static async #removeDamageRoll(_, button) {
const { memberKey } = button.dataset;
const current = this.party.system.tagTeam.members[memberKey].rollData;
this.updatePartyData({
[`system.tagTeam.members.${memberKey}.rollData`]: {
...current,
options: {
...current.options,
damage: null
this.updatePartyData(
{
[`system.tagTeam.members.${memberKey}.rollData`]: {
...current,
options: {
...current.options,
damage: null
}
}
}
});
},
this.getUpdatingParts(button)
);
}
static async #rerollDamageDice(_, button) {
@ -476,9 +552,12 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
return acc;
}, 0);
this.updatePartyData({
[`system.tagTeam.members.${memberKey}.rollData`]: rollData
});
this.updatePartyData(
{
[`system.tagTeam.members.${memberKey}.rollData`]: rollData
},
this.getUpdatingParts(button)
);
}
async getCriticalDamage(damage) {
@ -529,15 +608,18 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
static async #selectRoll(_, button) {
const { memberKey } = button.dataset;
this.updatePartyData({
[`system.tagTeam.members`]: Object.entries(this.party.system.tagTeam.members).reduce(
(acc, [key, member]) => {
acc[key] = { selected: key === memberKey ? !member.selected : false };
return acc;
},
{}
)
});
this.updatePartyData(
{
[`system.tagTeam.members`]: Object.entries(this.party.system.tagTeam.members).reduce(
(acc, [key, member]) => {
acc[key] = { selected: key === memberKey ? !member.selected : false };
return acc;
},
{}
)
},
this.getUpdatingParts(button)
);
}
async getJoinedRoll({ overrideIsCritical, displayVersion } = {}) {
@ -602,6 +684,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
members: _replace({})
}
},
[],
{ render: false }
);

View file

@ -154,8 +154,13 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
context.openSection = this.openSection;
context.tabs = this._getTabs(this.constructor.TABS);
context.config = CONFIG.DH;
if (this.action.damage?.hasOwnProperty('includeBase') && this.action.type === 'attack')
context.hasBaseDamage = !!this.action.parent.attack;
if (this.action.hasDamage) {
context.allDamageTypesUsed = !getUnusedDamageTypes(this.action.damage.parts).length;
if (this.action.damage.hasOwnProperty('includeBase') && this.action.type === 'attack')
context.hasBaseDamage = !!this.action.parent.attack;
}
context.costOptions = this.getCostOptions();
context.getRollTypeOptions = this.getRollTypeOptions();
context.disableOption = this.disableOption.bind(this);
@ -173,7 +178,6 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
revealed: this.openTrigger === index
};
});
context.allDamageTypesUsed = !getUnusedDamageTypes(this.action.damage.parts).length;
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
context.tierOptions = [
@ -312,8 +316,8 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
const callback = (_, button) => {
const data = this.action.toObject();
const type = choices[button.form.elements.type.value].value;
const part = { applyTo: type };
if (this.action.actor?.isNPC) part.value = { multiplier: 'flat' };
const part = this.action.schema.fields.damage.fields.parts.element.getInitialValue();
part.applyTo = type;
data.damage.parts[type] = part;
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
};

View file

@ -264,12 +264,20 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
hasSave: this.hasSave,
onSave: this.save?.damageMod,
isDirect: !!this.damage?.direct,
selectedRollMode: game.settings.get('core', 'rollMode'),
selectedMessageMode: game.settings.get('core', 'messageMode'),
data: this.getRollData(),
evaluate: this.hasRoll,
resourceUpdates: new ResourceUpdateMap(this.actor),
targetUuid: this.targetUuid,
...configOptions
...configOptions,
skips: {
resources: false,
triggers: false,
createMessage: false,
updateCountdowns: false,
reaction: false,
...(configOptions.skips ?? {})
}
};
DHBaseAction.applyKeybindings(config);
@ -329,6 +337,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
* @param {boolean} successCost
*/
async consume(config, successCost = false) {
config.resourceUpdates = new ResourceUpdateMap(config.actionActor);
await this.workflow.get('cost')?.execute(config, successCost);
await this.workflow.get('uses')?.execute(config, successCost);

View file

@ -89,6 +89,7 @@ export class ActionField extends foundry.data.fields.ObjectField {
/** @override */
_cleanType(value, options, _state) {
if (!(typeof value === 'object')) value = {};
value = super._cleanType(value, options, _state);
const cls = this.getModel(value);
if (cls) return cls.cleanData(value, options, _state);
return value;
@ -309,7 +310,7 @@ export function ActionMixin(Base) {
}
};
ChatMessage.applyRollMode(msg, game.settings.get('core', 'rollMode'));
ChatMessage.applyMode(msg, game.settings.get('core', 'messageMode'));
cls.create(msg);
}
}

View file

@ -18,8 +18,7 @@ export default class DHConsumable extends BaseDataItem {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
consumeOnUse: new fields.BooleanField({ initial: true }),
destroyOnEmpty: new fields.BooleanField({ initial: true })
consumeOnUse: new fields.BooleanField({ initial: true })
};
}

View file

@ -33,7 +33,7 @@ export default class DamageRoll extends DHRoll {
static async buildPost(roll, config, message) {
const chatMessage = config.source?.message
? ui.chat.collection.get(config.source.message)
: getDocumentClass('ChatMessage').applyRollMode({}, config.rollMode ?? CONST.DICE_ROLL_MODES.PUBLIC);
: getDocumentClass('ChatMessage').applyMode({}, config.rollMode ?? 'public');
if (game.modules.get('dice-so-nice')?.active) {
const pool = foundry.dice.terms.PoolTerm.fromRolls(
Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll))

View file

@ -117,10 +117,10 @@ export default class DHRoll extends Roll {
rolls: [roll]
};
config.selectedRollMode ??= game.settings.get('core', 'rollMode');
config.selectedMessageMode ??= game.settings.get('core', 'messageMode');
if (roll._evaluated) {
const message = await cls.create(msgData, { rollMode: config.selectedRollMode });
const message = await cls.create(msgData, { messageMode: config.selectedMessageMode });
if (config.tagTeamSelected) {
game.system.api.applications.dialogs.TagTeamDialog.assignRoll(message.speakerActor, message);

View file

@ -771,20 +771,10 @@ export default class DhpActor extends Actor {
resources.forEach(r => {
if (r.itemId) {
const { path, value } = game.system.api.fields.ActionFields.CostField.getItemIdCostUpdate(r);
if (
r.key === 'quantity' &&
r.target.type === 'consumable' &&
value === 0 &&
r.target.system.destroyOnEmpty
) {
r.target.delete();
} else {
updates.items[r.key] = {
target: r.target,
resources: { [path]: value }
};
}
updates.items[r.key] = {
target: r.target,
resources: { [path]: value }
};
} else {
const valueFunc = (base, resource, baseMax) => {
if (resource.clear) return baseMax && base.inverted ? baseMax : 0;

View file

@ -76,7 +76,7 @@ export default class DhRollTable extends foundry.documents.RollTable {
}
async toMessage(results, { roll, messageData = {}, messageOptions = {} } = {}) {
messageOptions.rollMode ??= game.settings.get('core', 'rollMode');
messageOptions.rollMode ??= game.settings.get('core', 'messageMode');
// Construct chat data
messageData = foundry.utils.mergeObject(

View file

@ -169,70 +169,19 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
}
}
this.baseActivate(element, { ...options, html: html });
this.noOffset = options.noOffset;
super.activate(element, { ...options, html: html });
}
/* Need to pass more options to _setAnchor, so have to copy whole foundry method >_< */
async baseActivate(element, options) {
let { text, direction, cssClass, locked = false, html, content } = options;
if (content && !html) {
foundry.utils.logCompatibilityWarning(
'The content option has been deprecated in favor of the html option',
{ since: 13, until: 15, once: true }
);
html = content;
}
if (text && html) throw new Error('Cannot provide both text and html options to TooltipManager#activate.');
// Deactivate currently active element
this.deactivate();
// Check if the element still exists in the DOM.
if (!document.body.contains(element)) return;
// Mark the new element as active
this.#active = true;
this.element = element;
element.setAttribute('aria-describedby', 'tooltip');
html ||= element.dataset.tooltipHtml;
if (html) {
if (typeof html === 'string') this.tooltip.innerHTML = foundry.utils.cleanHTML(html);
else {
this.tooltip.innerHTML = ''; // Clear existing HTML
this.tooltip.appendChild(html);
}
} else {
text ||= element.dataset.tooltipText;
if (text) this.tooltip.textContent = text;
else {
text = element.dataset.tooltip;
// Localized message should be safe
if (game.i18n.has(text)) this.tooltip.innerHTML = game.i18n.localize(text);
else this.tooltip.innerHTML = foundry.utils.cleanHTML(text);
}
}
// Activate display of the tooltip
this.tooltip.removeAttribute('class');
this.tooltip.classList.add('active', 'themed', 'theme-dark');
this.tooltip.showPopover();
cssClass ??= element.closest('[data-tooltip-class]')?.dataset.tooltipClass;
if (cssClass) this.tooltip.classList.add(...cssClass.split(' '));
// Set tooltip position
direction ??= element.closest('[data-tooltip-direction]')?.dataset.tooltipDirection;
if (!direction) direction = this._determineDirection();
this._setAnchor(direction, options);
if (locked || element.dataset.hasOwnProperty('locked')) this.lockTooltip();
}
_setAnchor(direction, options = {}) {
_setAnchor(direction) {
const directions = this.constructor.TOOLTIP_DIRECTIONS;
const pad = this.constructor.TOOLTIP_MARGIN_PX;
const pos = this.element.getBoundingClientRect();
const { innerHeight, innerWidth } = this.tooltip.ownerDocument.defaultView;
const tooltipPadding = 16;
const horizontalOffset = options.noOffset ? tooltipPadding : this.tooltip.offsetWidth / 2 - pos.width / 2;
const verticalOffset = options.noOffset ? tooltipPadding : this.tooltip.offsetHeight / 2 - pos.height / 2;
const horizontalOffset = this.noOffset ? tooltipPadding : this.tooltip.offsetWidth / 2 - pos.width / 2;
const verticalOffset = this.noOffset ? tooltipPadding : this.tooltip.offsetHeight / 2 - pos.height / 2;
const style = {};
switch (direction) {