|
|
- import { TVA_CONFIG, updateSettings, _arrayAwareDiffObject } from './settings.js';
- import { showArtSelect } from '../token-variants.mjs';
- import EffectMappingForm from '../applications/effectMappingForm.js';
- import CompendiumMapConfig from '../applications/compendiumMap.js';
- import { toggleTemplateDialog } from '../applications/dialogs.js';
-
- const simplifyRegex = new RegExp(/[^A-Za-z0-9/\\]/g);
-
- export const SUPPORTED_COMP_ATTRIBUTES = ['rotation', 'elevation'];
- export const EXPRESSION_OPERATORS = ['\\(', '\\)', '&&', '||', '\\!'];
-
- // Types of searches
- export const SEARCH_TYPE = {
- PORTRAIT: 'Portrait',
- TOKEN: 'Token',
- PORTRAIT_AND_TOKEN: 'PortraitAndToken',
- TILE: 'Tile',
- ITEM: 'Item',
- JOURNAL: 'JournalEntry',
- MACRO: 'Macro',
- };
-
- export const BASE_IMAGE_CATEGORIES = [
- 'Portrait',
- 'Token',
- 'PortraitAndToken',
- 'Tile',
- 'Item',
- 'JournalEntry',
- 'Macro',
- 'RollTable',
- ];
-
- export const PRESSED_KEYS = {
- popupOverride: false,
- config: false,
- };
-
- const BATCH_UPDATES = {
- TOKEN: [],
- TOKEN_CALLBACKS: [],
- TOKEN_CONTEXT: { animate: true },
- ACTOR: [],
- ACTOR_CONTEXT: null,
- };
-
- export function startBatchUpdater() {
- canvas.app.ticker.add(() => {
- if (BATCH_UPDATES.TOKEN.length) {
- canvas.scene.updateEmbeddedDocuments('Token', BATCH_UPDATES.TOKEN, BATCH_UPDATES.TOKEN_CONTEXT).then(() => {
- for (const cb of BATCH_UPDATES.TOKEN_CALLBACKS) {
- cb();
- }
- BATCH_UPDATES.TOKEN_CALLBACKS = [];
- });
- BATCH_UPDATES.TOKEN = [];
- }
- if (BATCH_UPDATES.ACTOR.length !== 0) {
- if (BATCH_UPDATES.ACTOR_CONTEXT) Actor.updateDocuments(BATCH_UPDATES.ACTOR, BATCH_UPDATES.ACTOR_CONTEXT);
- else Actor.updateDocuments(BATCH_UPDATES.ACTOR);
- BATCH_UPDATES.ACTOR = [];
- BATCH_UPDATES.ACTOR_CONTEXT = null;
- }
- });
- }
-
- export function queueTokenUpdate(id, update, callback = null, animate = true) {
- update._id = id;
- BATCH_UPDATES.TOKEN.push(update);
- BATCH_UPDATES.TOKEN_CONTEXT = { animate };
- if (callback) BATCH_UPDATES.TOKEN_CALLBACKS.push(callback);
- }
-
- export function queueActorUpdate(id, update, context = null) {
- update._id = id;
- BATCH_UPDATES.ACTOR.push(update);
- BATCH_UPDATES.ACTOR_CONTEXT = context;
- }
-
- /**
- * Updates Token and/or Proto Token with the new image and custom configuration if one exists.
- * @param {string} imgSrc Image source path/url
- * @param {object} [options={}] Update options
- * @param {Token[]} [options.token] Token to be updated with the new image
- * @param {Actor} [options.actor] Actor with Proto Token to be updated with the new image
- * @param {string} [options.imgName] Image name if it differs from the file name. Relevant for rolltable sourced images.
- * @param {object} [options.tokenUpdate] Token update to be merged and performed at the same time as image update
- * @param {object} [options.actorUpdate] Actor update to be merged and performed at the same time as image update
- * @param {string} [options.pack] Compendium pack of the Actor being updated
- * @param {func} [options.callback] Callback to be executed when a batch update has been performed
- * @param {object} [options.config] Token Configuration settings to be applied to the token
- */
- export async function updateTokenImage(
- imgSrc,
- {
- token = null,
- actor = null,
- imgName = null,
- tokenUpdate = {},
- actorUpdate = {},
- pack = '',
- callback = null,
- config = undefined,
- animate = true,
- update = null,
- applyDefaultConfig = true,
- } = {}
- ) {
- if (!(token || actor)) {
- console.warn(game.i18n.localize('token-variants.notifications.warn.update-image-no-token-actor'));
- return;
- }
-
- token = token?.document ?? token;
-
- // Check if it's a wildcard image
- if ((imgSrc && imgSrc.includes('*')) || (imgSrc.includes('{') && imgSrc.includes('}'))) {
- const images = await wildcardImageSearch(imgSrc);
- if (images.length) {
- imgSrc = images[Math.floor(Math.random() * images.length)];
- }
- }
-
- if (!actor && token.actor) {
- actor = game.actors.get(token.actor.id);
- }
-
- const getDefaultConfig = (token, actor) => {
- let configEntries = [];
- if (token) configEntries = token.getFlag('token-variants', 'defaultConfig') || [];
- else if (actor) {
- const tokenData = actor.prototypeToken;
- if ('token-variants' in tokenData.flags && 'defaultConfig' in tokenData['token-variants'])
- configEntries = tokenData['token-variants']['defaultConfig'];
- }
- return expandObject(Object.fromEntries(configEntries));
- };
-
- const constructDefaultConfig = (origData, customConfig) => {
- const flatOrigData = flattenObject(origData);
- TokenDataAdapter.dataToForm(flatOrigData);
- const flatCustomConfig = flattenObject(customConfig);
- let filtered = filterObject(flatOrigData, flatCustomConfig);
-
- // Flags need special treatment as once set they are not removed via absence of them in the update
- for (let [k, v] of Object.entries(flatCustomConfig)) {
- if (k.startsWith('flags.')) {
- if (!(k in flatOrigData)) {
- let splitK = k.split('.');
- splitK[splitK.length - 1] = '-=' + splitK[splitK.length - 1];
- filtered[splitK.join('.')] = null;
- }
- }
- }
-
- return Object.entries(filtered);
- };
-
- let tokenUpdateObj = tokenUpdate;
- if (imgSrc) {
- setProperty(tokenUpdateObj, 'texture.src', imgSrc);
- if (imgName && getFileName(imgSrc) === imgName) setProperty(tokenUpdateObj, 'flags.token-variants.-=name', null);
- else setProperty(tokenUpdateObj, 'flags.token-variants.name', imgName);
- }
-
- const tokenCustomConfig = mergeObject(
- getTokenConfigForUpdate(imgSrc || token?.texture.src, imgName, token),
- config ?? {}
- );
- const usingCustomConfig = token?.getFlag('token-variants', 'usingCustomConfig');
- const defaultConfig = getDefaultConfig(token);
- if (!isEmpty(tokenCustomConfig) || usingCustomConfig) {
- tokenUpdateObj = modMergeObject(tokenUpdateObj, defaultConfig);
- }
-
- if (!isEmpty(tokenCustomConfig)) {
- if (token) {
- setProperty(tokenUpdateObj, 'flags.token-variants.usingCustomConfig', true);
- let doc = token.document ?? token;
- const tokenData = doc.toObject ? doc.toObject() : deepClone(doc);
-
- const defConf = constructDefaultConfig(mergeObject(tokenData, defaultConfig), tokenCustomConfig);
- setProperty(tokenUpdateObj, 'flags.token-variants.defaultConfig', defConf);
- } else if (actor && !token) {
- setProperty(tokenUpdateObj, 'flags.token-variants.usingCustomConfig', true);
- const tokenData = actor.prototypeToken instanceof Object ? actor.prototypeToken : actor.prototypeToken.toObject();
- const defConf = constructDefaultConfig(tokenData, tokenCustomConfig);
- setProperty(tokenUpdateObj, 'flags.token-variants.defaultConfig', defConf);
- }
-
- // Fix, an empty flag may be passed which would overwrite any current flags in the updateObj
- // Remove it before doing the merge
- if (!tokenCustomConfig.flags) {
- delete tokenCustomConfig.flags;
- }
-
- tokenUpdateObj = modMergeObject(tokenUpdateObj, tokenCustomConfig);
- } else if (usingCustomConfig) {
- setProperty(tokenUpdateObj, 'flags.token-variants.-=usingCustomConfig', null);
- delete tokenUpdateObj?.flags?.['token-variants']?.defaultConfig;
- setProperty(tokenUpdateObj, 'flags.token-variants.-=defaultConfig', null);
- }
-
- if (!applyDefaultConfig) {
- setProperty(tokenUpdateObj, 'flags.token-variants.-=usingCustomConfig', null);
- delete tokenUpdateObj?.flags?.['token-variants']?.defaultConfig;
- setProperty(tokenUpdateObj, 'flags.token-variants.-=defaultConfig', null);
- }
-
- if (!isEmpty(tokenUpdateObj)) {
- if (actor && !token) {
- TokenDataAdapter.formToData(actor.prototypeToken, tokenUpdateObj);
- actorUpdate.token = tokenUpdateObj;
- if (pack) {
- queueActorUpdate(actor.id, actorUpdate, { pack: pack });
- } else {
- await (actor.document ?? actor).update(actorUpdate);
- }
- }
-
- if (token) {
- TokenDataAdapter.formToData(token, tokenUpdateObj);
- if (TVA_CONFIG.updateTokenProto && token.actor) {
- if (update) {
- mergeObject(update, { token: tokenUpdateObj });
- } else {
- // Timeout to prevent race conditions with other modules namely MidiQOL
- // this is a low priority update so it should be Ok to do
- if (token.actorLink) {
- setTimeout(() => queueActorUpdate(token.actor.id, { token: tokenUpdateObj }), 500);
- } else {
- setTimeout(() => token.actor.update({ token: tokenUpdateObj }), 500);
- }
- }
- }
-
- if (update) {
- mergeObject(update, tokenUpdateObj);
- } else {
- if (token.object) queueTokenUpdate(token.id, tokenUpdateObj, callback, animate);
- else {
- await token.update(tokenUpdateObj, { animate });
- callback();
- }
- }
- }
- }
- }
-
- /**
- * Assign new artwork to the actor
- */
- export async function updateActorImage(actor, imgSrc, directUpdate = true, pack = '') {
- if (!actor) return;
- if (directUpdate) {
- await (actor.document ?? actor).update({
- img: imgSrc,
- });
- } else {
- queueActorUpdate(
- actor.id,
- {
- img: imgSrc,
- },
- pack ? { pack: pack } : null
- );
- }
- }
-
- async function showTileArtSelect() {
- for (const tile of canvas.tiles.controlled) {
- const tileName = tile.document.getFlag('token-variants', 'tileName') || tile.id;
- showArtSelect(tileName, {
- callback: async function (imgSrc, name) {
- tile.document.update({ img: imgSrc });
- },
- searchType: SEARCH_TYPE.TILE,
- });
- }
- }
-
- /**
- * Checks if a key is pressed taking into account current game version.
- * @param {string} key v/Ctrl/Shift/Alt
- * @returns
- */
- export function keyPressed(key) {
- if (key === 'v') return game.keyboard.downKeys.has('KeyV');
- return PRESSED_KEYS[key];
- }
-
- export function registerKeybinds() {
- game.keybindings.register('token-variants', 'popupOverride', {
- name: 'Popup Override',
- hint: 'When held will trigger popups even when they are disabled.',
- editable: [
- {
- key: 'ShiftLeft',
- },
- ],
- onDown: () => {
- PRESSED_KEYS.popupOverride = true;
- },
- onUp: () => {
- PRESSED_KEYS.popupOverride = false;
- },
- restricted: false,
- precedence: CONST.KEYBINDING_PRECEDENCE.NORMAL,
- });
- game.keybindings.register('token-variants', 'config', {
- name: 'Config',
- hint: 'When held during a mouse Left-Click of an Image or an Active Affect will display a configuration window.',
- editable: [
- {
- key: 'ShiftLeft',
- },
- ],
- onDown: () => {
- PRESSED_KEYS.config = true;
- },
- onUp: () => {
- PRESSED_KEYS.config = false;
- },
- restricted: false,
- precedence: CONST.KEYBINDING_PRECEDENCE.NORMAL,
- });
- game.keybindings.register('token-variants', 'showArtSelectPortrait', {
- name: 'Show Art Select: Portrait',
- hint: 'Brings up an Art Select pop-up to change the portrait images of the selected tokens.',
- editable: [
- {
- key: 'Digit1',
- modifiers: ['Shift'],
- },
- ],
- onDown: () => {
- for (const token of canvas.tokens.controlled) {
- const actor = token.actor;
- if (!actor) continue;
- showArtSelect(actor.name, {
- callback: async function (imgSrc, name) {
- await updateActorImage(actor, imgSrc);
- },
- searchType: SEARCH_TYPE.PORTRAIT,
- object: actor,
- });
- }
- if (TVA_CONFIG.tilesEnabled && canvas.tokens.controlled.length === 0) showTileArtSelect();
- },
- restricted: true,
- precedence: CONST.KEYBINDING_PRECEDENCE.NORMAL,
- });
- game.keybindings.register('token-variants', 'showArtSelectToken', {
- name: 'Show Art Select: Token',
- hint: 'Brings up an Art Select pop-up to change the token images of the selected tokens.',
- editable: [
- {
- key: 'Digit2',
- modifiers: ['Shift'],
- },
- ],
- onDown: () => {
- for (const token of canvas.tokens.controlled) {
- showArtSelect(token.name, {
- callback: async function (imgSrc, imgName) {
- updateTokenImage(imgSrc, {
- actor: token.actor,
- imgName: imgName,
- token: token,
- });
- },
- searchType: SEARCH_TYPE.TOKEN,
- object: token,
- });
- }
- if (TVA_CONFIG.tilesEnabled && canvas.tokens.controlled.length === 0) showTileArtSelect();
- },
- restricted: true,
- precedence: CONST.KEYBINDING_PRECEDENCE.NORMAL,
- });
- game.keybindings.register('token-variants', 'showArtSelectGeneral', {
- name: 'Show Art Select: Portrait+Token',
- hint: 'Brings up an Art Select pop-up to change both Portrait and Token images of the selected tokens.',
- editable: [
- {
- key: 'Digit3',
- modifiers: ['Shift'],
- },
- ],
- onDown: () => {
- for (const token of canvas.tokens.controlled) {
- const actor = token.actor;
- showArtSelect(token.name, {
- callback: async function (imgSrc, imgName) {
- if (actor) await updateActorImage(actor, imgSrc);
- updateTokenImage(imgSrc, {
- actor: token.actor,
- imgName: imgName,
- token: token,
- });
- },
- searchType: SEARCH_TYPE.PORTRAIT_AND_TOKEN,
- object: token,
- });
- }
- if (TVA_CONFIG.tilesEnabled && canvas.tokens.controlled.length === 0) showTileArtSelect();
- },
- restricted: true,
- precedence: CONST.KEYBINDING_PRECEDENCE.NORMAL,
- });
- game.keybindings.register('token-variants', 'openGlobalMappings', {
- name: 'Open Global Effect Configurations',
- hint: 'Brings up the settings window for Global Effect Configurations',
- editable: [
- {
- key: 'KeyG',
- modifiers: ['Shift'],
- },
- ],
- onDown: () => {
- const setting = game.settings.get('core', DefaultTokenConfig.SETTING);
- const data = new foundry.data.PrototypeToken(setting);
- const token = new TokenDocument(data, { actor: null });
- new EffectMappingForm(token, { globalMappings: true }).render(true);
- },
- restricted: true,
- precedence: CONST.KEYBINDING_PRECEDENCE.NORMAL,
- });
-
- game.keybindings.register('token-variants', 'compendiumMapper', {
- name: 'Compendium Mapper',
- hint: 'Opens Compendium Mapper',
- editable: [
- {
- key: 'KeyM',
- modifiers: ['Shift'],
- },
- ],
- onDown: () => {
- new CompendiumMapConfig().render(true);
- },
- restricted: true,
- precedence: CONST.KEYBINDING_PRECEDENCE.NORMAL,
- });
-
- game.keybindings.register('token-variants', 'toggleTemplate', {
- name: 'Toggle Template Dialog',
- hint: 'Brings up a dialog from which you can toggle templates on currently selected tokens.',
- editable: [],
- onDown: toggleTemplateDialog,
- restricted: true,
- precedence: CONST.KEYBINDING_PRECEDENCE.NORMAL,
- });
- }
-
- /**
- * Retrieves a custom token configuration if one exists for the given image
- */
- export function getTokenConfig(imgSrc, imgName) {
- if (!imgName) imgName = getFileName(imgSrc);
- const tokenConfigs = (TVA_CONFIG.tokenConfigs || []).flat();
- return tokenConfigs.find((config) => config.tvImgSrc == imgSrc && config.tvImgName == imgName) ?? {};
- }
-
- /**
- * Retrieves a custom token configuration if one exists for the given image and removes control keys
- * returning a clean config that can be used in token update.
- */
- export function getTokenConfigForUpdate(imgSrc, imgName, token) {
- if (!imgSrc) return {};
-
- let tokenConfig = {};
- for (const path of TVA_CONFIG.searchPaths) {
- if (path.config && imgSrc.startsWith(path.text)) {
- mergeObject(tokenConfig, path.config);
- }
- }
-
- let imgConfig = getTokenConfig(imgSrc, imgName ?? getFileName(imgSrc));
- if (!isEmpty(imgConfig)) {
- imgConfig = deepClone(imgConfig);
- delete imgConfig.tvImgSrc;
- delete imgConfig.tvImgName;
- if (token) TokenDataAdapter.formToData(token, imgConfig);
-
- for (var key in imgConfig) {
- if (!key.startsWith('tvTab_')) {
- tokenConfig[key] = imgConfig[key];
- }
- }
- }
-
- if (TVA_CONFIG.imgNameContainsDimensions || TVA_CONFIG.imgNameContainsFADimensions) {
- extractDimensionsFromImgName(imgSrc, tokenConfig);
- }
-
- return tokenConfig;
- }
-
- /**
- * Adds or removes a custom token configuration
- */
- export function setTokenConfig(imgSrc, imgName, tokenConfig) {
- const tokenConfigs = (TVA_CONFIG.tokenConfigs || []).flat();
- const tcIndex = tokenConfigs.findIndex((config) => config.tvImgSrc == imgSrc && config.tvImgName == imgName);
-
- let deleteConfig = !tokenConfig || Object.keys(tokenConfig).length === 0;
- if (!deleteConfig) {
- tokenConfig['tvImgSrc'] = imgSrc;
- tokenConfig['tvImgName'] = imgName;
- }
-
- if (tcIndex != -1 && !deleteConfig) {
- tokenConfigs[tcIndex] = tokenConfig;
- } else if (tcIndex != -1 && deleteConfig) {
- tokenConfigs.splice(tcIndex, 1);
- } else if (!deleteConfig) {
- tokenConfigs.push(tokenConfig);
- }
- updateSettings({ tokenConfigs: tokenConfigs });
- return !deleteConfig;
- }
-
- /**
- * Extracts the file name from the given path.
- */
- export function getFileName(path) {
- if (!path) return '';
- return decodeURISafely(path).split('\\').pop().split('/').pop().split('.').slice(0, -1).join('.');
- }
-
- /**
- * Extracts the file name including the extension from the given path.
- */
- export function getFileNameWithExt(path) {
- if (!path) return '';
- return decodeURISafely(path).split('\\').pop().split('/').pop();
- }
-
- /**
- * Extract the directory path excluding the file name.
- */
- export function getFilePath(path) {
- return decodeURISafely(path).match(/(.*)[\/\\]/)[1] || '';
- }
-
- /**
- * Simplify name.
- */
- export function simplifyName(name) {
- return name.replace(simplifyRegex, '').toLowerCase();
- }
-
- export function simplifyPath(path) {
- return decodeURIComponentSafely(path).replace(simplifyRegex, '').toLowerCase();
- }
-
- /**
- * Parses the 'excludedKeyword' setting (a comma separated string) into a Set
- */
- export function parseKeywords(keywords) {
- return keywords
- .split(/\W/)
- .map((word) => simplifyName(word))
- .filter((word) => word != '');
- }
-
- /**
- * Returns true of provided path points to an image
- */
- export function isImage(path) {
- var extension = path.split('.');
- extension = extension[extension.length - 1].toLowerCase();
- return ['jpg', 'jpeg', 'png', 'svg', 'webp', 'gif'].includes(extension);
- }
-
- /**
- * Returns true of provided path points to a video
- */
- export function isVideo(path) {
- var extension = path.split('.');
- extension = extension[extension.length - 1].toLowerCase();
- return ['mp4', 'ogg', 'webm', 'm4v'].includes(extension);
- }
-
- /**
- * Send a recursive HTTP asset browse request to ForgeVTT
- * @param {string} path Asset Library path
- * @param {string} apiKey Key with read access to the Asset Library
- * @returns
- */
- export async function callForgeVTT(path, apiKey) {
- return new Promise(async (resolve, reject) => {
- if (typeof ForgeVTT === 'undefined' || !ForgeVTT.usingTheForge) return resolve({});
-
- const url = `${ForgeVTT.FORGE_URL}/api/assets/browse`;
- const xhr = new XMLHttpRequest();
- xhr.withCredentials = true;
- xhr.open('POST', url);
- xhr.setRequestHeader('Access-Key', apiKey);
- xhr.setRequestHeader('X-XSRF-TOKEN', await ForgeAPI.getXSRFToken());
- xhr.responseType = 'json';
-
- xhr.onreadystatechange = () => {
- if (xhr.readyState !== 4) return;
- resolve(xhr.response);
- };
- xhr.onerror = (err) => {
- resolve({ code: 500, error: err.message });
- };
- let formData = {
- path: path,
- options: {
- recursive: true,
- },
- };
- formData = JSON.stringify(formData);
- xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
- xhr.send(formData);
- });
- }
-
- /**
- * Retrieves filters based on the type of search.
- * @param {SEARCH_TYPE} searchType
- */
- export function getFilters(searchType, filters) {
- // Select filters based on type of search
- filters = filters ? filters : TVA_CONFIG.searchFilters;
-
- if (filters[searchType]) {
- filters = filters[searchType];
- } else {
- filters = {
- include: '',
- exclude: '',
- regex: '',
- };
- }
-
- if (filters.regex) filters.regex = new RegExp(filters.regex);
- return filters;
- }
-
- export function userRequiresImageCache(perm) {
- const permissions = perm ? perm : TVA_CONFIG.permissions;
- const role = game.user.role;
- return (
- permissions.popups[role] ||
- permissions.portrait_right_click[role] ||
- permissions.image_path_button[role] ||
- permissions.hudFullAccess[role]
- );
- }
-
- export async function waitForTokenTexture(token, callback, checks = 40) {
- // v10/v9 compatibility
-
- if (!token.mesh || !token.mesh.texture) {
- checks--;
- if (checks > 1)
- new Promise((resolve) => setTimeout(resolve, 1)).then(() => waitForTokenTexture(token, callback, checks));
- return;
- }
-
- callback(token);
- }
-
- export function flattenSearchResults(results) {
- let flattened = [];
- if (!results) return flattened;
- results.forEach((images) => {
- flattened = flattened.concat(images);
- });
- return flattened;
- }
-
- // Slightly modified version of mergeObject; added an option to ignore -= keys
- export function modMergeObject(
- original,
- other = {},
- {
- insertKeys = true,
- insertValues = true,
- overwrite = true,
- recursive = true,
- inplace = true,
- enforceTypes = false,
- } = {},
- _d = 0
- ) {
- other = other || {};
- if (!(original instanceof Object) || !(other instanceof Object)) {
- throw new Error('One of original or other are not Objects!');
- }
- const options = {
- insertKeys,
- insertValues,
- overwrite,
- recursive,
- inplace,
- enforceTypes,
- };
-
- // Special handling at depth 0
- if (_d === 0) {
- if (!inplace) original = deepClone(original);
- if (Object.keys(original).some((k) => /\./.test(k))) original = expandObject(original);
- if (Object.keys(other).some((k) => /\./.test(k))) other = expandObject(other);
- }
-
- // Iterate over the other object
- for (let k of Object.keys(other)) {
- const v = other[k];
- if (original.hasOwnProperty('-=' + k)) {
- original[k] = original['-=' + k];
- delete original['-=' + k];
- }
- if (original.hasOwnProperty(k)) _modMergeUpdate(original, k, v, options, _d + 1);
- else _modMergeInsert(original, k, v, options, _d + 1);
- }
- return original;
- }
-
- /**
- * A helper function for merging objects when the target key does not exist in the original
- * @private
- */
- function _modMergeInsert(original, k, v, { insertKeys, insertValues } = {}, _d) {
- // Recursively create simple objects
- if (v?.constructor === Object) {
- original[k] = modMergeObject({}, v, {
- insertKeys: true,
- inplace: true,
- });
- return;
- }
-
- // Delete a key
- // if (k.startsWith('-=')) {
- // delete original[k.slice(2)];
- // return;
- // }
-
- // Insert a key
- const canInsert = (_d <= 1 && insertKeys) || (_d > 1 && insertValues);
- if (canInsert) original[k] = v;
- }
-
- /**
- * A helper function for merging objects when the target key exists in the original
- * @private
- */
- function _modMergeUpdate(original, k, v, { insertKeys, insertValues, enforceTypes, overwrite, recursive } = {}, _d) {
- const x = original[k];
- const tv = getType(v);
- const tx = getType(x);
-
- // Recursively merge an inner object
- if (tv === 'Object' && tx === 'Object' && recursive) {
- return modMergeObject(
- x,
- v,
- {
- insertKeys: insertKeys,
- insertValues: insertValues,
- overwrite: overwrite,
- inplace: true,
- enforceTypes: enforceTypes,
- },
- _d
- );
- }
-
- // Overwrite an existing value
- if (overwrite) {
- if (tx !== 'undefined' && tv !== tx && enforceTypes) {
- throw new Error(`Mismatched data types encountered during object merge.`);
- }
- original[k] = v;
- }
- }
-
- export async function tv_executeScript(script, { actor, token, tvaUpdate } = {}) {
- // Add variables to the evaluation scope
- const speaker = ChatMessage.getSpeaker();
- const character = game.user.character;
-
- token = token?.object || token || (canvas.ready ? canvas.tokens.get(speaker.token) : null);
- actor = actor || token?.actor || game.actors.get(speaker.actor);
-
- // Attempt script execution
- const AsyncFunction = async function () {}.constructor;
- try {
- const fn = AsyncFunction('speaker', 'actor', 'token', 'character', 'tvaUpdate', `${script}`);
- await fn.call(null, speaker, actor, token, character, tvaUpdate);
- } catch (err) {
- ui.notifications.error(`There was an error in your script syntax. See the console (F12) for details`);
- console.error(err);
- }
- }
-
- export async function executeMacro(macroName, token) {
- token = token?.object || token;
- game.macros.find((m) => m.name === macroName)?.execute({ token });
- }
-
- export async function applyTMFXPreset(token, presetName, action = 'apply') {
- token = token.object ?? token;
- if (game.modules.get('tokenmagic')?.active && token.document) {
- const preset = TokenMagic.getPreset(presetName);
- if (preset) {
- if (action === 'apply') {
- await TokenMagic.addUpdateFilters(token, preset);
- } else if (action === 'remove') {
- await TokenMagic.deleteFilters(token, presetName);
- }
- }
- }
- }
-
- export async function toggleTMFXPreset(token, presetName) {
- token = token.object ?? token;
- if (game.modules.get('tokenmagic')?.active && token.document) {
- if (TokenMagic.hasFilterId(token, presetName)) {
- applyTMFXPreset(token, presetName, 'remove');
- } else {
- applyTMFXPreset(token, presetName, 'apply');
- }
- }
- }
-
- export async function applyCEEffect(tokenDoc, ceEffect, action = 'apply') {
- if (game.modules.get('dfreds-convenient-effects')?.active) {
- if (!ceEffect.apply && !ceEffect.remove) return;
- else if (!ceEffect.apply || !ceEffect.remove) {
- if (action === 'apply') {
- if (ceEffect.remove) action = 'remove';
- } else return;
- }
-
- let uuid = tokenDoc.actor?.uuid;
- if (uuid) {
- if (action === 'apply') {
- await game.dfreds.effectInterface.addEffect({
- effectName: ceEffect.name,
- uuid,
- origin: 'token-variants',
- overlay: false,
- });
- } else {
- await game.dfreds.effectInterface.removeEffect({ effectName: ceEffect.name, uuid });
- }
- }
- }
- }
-
- export async function toggleCEEffect(token, effectName) {
- if (game.modules.get('dfreds-convenient-effects')?.active) {
- let uuid = (token.document ?? token).actor?.uuid;
- await game.dfreds.effectInterface.toggleEffect(effectName, {
- uuids: [uuid],
- overlay: false,
- });
- }
- }
-
- export class TokenDataAdapter {
- static dataToForm(data) {
- if ('texture.scaleX' in data) {
- data.scale = Math.abs(data['texture.scaleX']);
- data.mirrorX = data['texture.scaleX'] < 0;
- }
- if ('texture.scaleY' in data) {
- data.scale = Math.abs(data['texture.scaleY']);
- data.mirrorY = data['texture.scaleY'] < 0;
- }
- }
-
- static formToData(token, formData) {
- // Scale/mirroring
- if ('scale' in formData || 'mirrorX' in formData || 'mirrorY' in formData) {
- const doc = token.document ? token.document : token;
- if (!('scale' in formData)) formData.scale = Math.abs(doc.texture.scaleX);
- if (!('mirrorX' in formData)) formData.mirrorX = doc.texture.scaleX < 0;
- if (!('mirrorY' in formData)) formData.mirrorY = doc.texture.scaleY < 0;
- setProperty(formData, 'texture.scaleX', formData.scale * (formData.mirrorX ? -1 : 1));
- setProperty(formData, 'texture.scaleY', formData.scale * (formData.mirrorY ? -1 : 1));
- ['scale', 'mirrorX', 'mirrorY'].forEach((k) => delete formData[k]);
- }
- }
- }
-
- export function determineAddedRemovedEffects(addedEffects, removedEffects, newEffects, oldEffects) {
- for (const ef of newEffects) {
- if (!oldEffects.includes(ef)) {
- addedEffects.push(ef);
- }
- }
- for (const ef of oldEffects) {
- if (!newEffects.includes(ef)) {
- removedEffects.push(ef);
- }
- }
- }
-
- export async function wildcardImageSearch(imgSrc) {
- let source = 'data';
- const browseOptions = { wildcard: true };
-
- // Support non-user sources
- if (/\.s3\./.test(imgSrc)) {
- source = 's3';
- const { bucket, keyPrefix } = FilePicker.parseS3URL(imgSrc);
- if (bucket) {
- browseOptions.bucket = bucket;
- imgSrc = keyPrefix;
- }
- } else if (imgSrc.startsWith('icons/')) source = 'public';
-
- // Retrieve wildcard content
- try {
- const content = await FilePicker.browse(source, imgSrc, browseOptions);
- return content.files;
- } catch (err) {}
- return [];
- }
-
- /**
- * Returns a random name generated using Name Forge module
- * @param {*} randomizerSettings
- * @returns
- */
- export async function nameForgeRandomize(randomizerSettings) {
- const nameForgeSettings = randomizerSettings.nameForge;
- if (nameForgeSettings?.randomize && nameForgeSettings?.models) {
- const nameForge = game.modules.get('nameforge');
- if (nameForge?.active) {
- const randomNames = [];
- for (const modelKey of nameForgeSettings.models) {
- const modelProp = getProperty(nameForge.models, modelKey);
- if (modelProp) {
- const model = await nameForge.api.createModel(modelProp);
- if (model) {
- randomNames.push(nameForge.api.generateName(model)[0]);
- }
- }
- }
- return randomNames[Math.floor(Math.random() * randomNames.length)];
- }
- }
-
- return null;
- }
-
- /**
- * Upload Token and associated overlays as a single image
- */
- export async function uploadTokenImage(token, options) {
- let renderTexture = captureToken(token, options);
- if (renderTexture) {
- const b64 = canvas.app.renderer.extract.base64(renderTexture, 'image/webp', 1);
- let res = await fetch(b64);
- let blob = await res.blob();
- const filename = options.name + `.webp`;
- let file = new File([blob], filename, { type: 'image/webp' });
- await FilePicker.upload('data', options.path, file, {});
- }
- }
-
- /**
- * Modified version of 'dev7355608' captureCanvas function. Captures combined Token and Overlay image
- */
- function captureToken(token, { scale = 3, width = null, height = null } = {}) {
- if (!canvas.ready || !token) {
- return;
- }
-
- width = width ?? token.texture.width;
- height = height ?? token.texture.height;
-
- scale = scale * Math.min(width / token.texture.width, height / token.texture.height);
-
- const renderer = canvas.app.renderer;
- const viewPosition = { ...canvas.scene._viewPosition };
-
- renderer.resize(width ?? renderer.screen.width, height ?? renderer.screen.height);
-
- width = canvas.screenDimensions[0] = renderer.screen.width;
- height = canvas.screenDimensions[1] = renderer.screen.height;
-
- canvas.stage.position.set(width / 2, height / 2);
-
- canvas.pan({
- x: token.center.x,
- y: token.center.y,
- scale,
- });
-
- const renderTexture = PIXI.RenderTexture.create({
- width,
- height,
- resolution: token.texture.resolution,
- });
-
- const cacheParent = canvas.stage.enableTempParent();
-
- canvas.stage.updateTransform();
- canvas.stage.disableTempParent(cacheParent);
-
- let spritesToRender = [token.mesh];
- if (token.tvaOverlays) spritesToRender = spritesToRender.concat(token.tvaOverlays);
- spritesToRender.sort((sprite) => sprite.sort);
-
- for (const sprite of spritesToRender) {
- renderer.render(sprite, { renderTexture, skipUpdateTransform: true, clear: false });
- }
-
- canvas._onResize();
- canvas.pan(viewPosition);
-
- return renderTexture;
- }
-
- export function getAllActorTokens(actor, linked = false, document = false) {
- if (actor.isToken) {
- if (document) return [actor.token];
- else if (actor.token.object) return [actor.token.object];
- else return [];
- }
-
- const tokens = [];
- game.scenes.forEach((scene) =>
- scene.tokens.forEach((token) => {
- if (token.actorId === actor.id) {
- if (linked && token.actorLink) tokens.push(token);
- else if (!linked) tokens.push(token);
- }
- })
- );
- if (document) return tokens;
- else return tokens.map((token) => token.object).filter((token) => token);
- }
-
- export function extractDimensionsFromImgName(img, dimensions = {}) {
- const name = getFileName(img);
-
- let scale;
- if (TVA_CONFIG.imgNameContainsDimensions) {
- const height = name.match(/_height(.*)_/)?.[1];
- if (height) dimensions.height = parseFloat(height);
- const width = name.match(/_width(.*)_/)?.[1];
- if (width) dimensions.width = parseFloat(width);
- scale = name.match(/_scale(.*)_/)?.[1];
- if (scale) scale = Math.max(parseFloat(scale), 0.2);
- }
- if (TVA_CONFIG.imgNameContainsFADimensions) {
- scale = name.match(/_Scale(\d+)_/)?.[1];
- if (scale) {
- scale = Math.max(parseInt(scale) / 100, 0.2);
- }
- }
- if (scale) {
- dimensions['texture.scaleX'] = scale;
- dimensions['texture.scaleY'] = scale;
- }
- return dimensions;
- }
-
- export function string2Hex(hexString) {
- return PIXI.utils.string2hex(hexString);
- }
-
- export function decodeURISafely(uri) {
- try {
- return decodeURI(uri);
- } catch (e) {
- console.warn('URI Component not decodable: ' + uri);
- return uri;
- }
- }
-
- export function decodeURIComponentSafely(uri) {
- try {
- return decodeURIComponent(uri);
- } catch (e) {
- console.warn('URI Component not decodable: ' + uri);
- return uri;
- }
- }
-
- export function mergeMappings(from, to) {
- const changedIDs = {};
-
- for (const m of from) {
- const i = to.findIndex((mapping) => mapping.label === m.label && mapping.group === m.group);
- if (i === -1) to.push(m);
- else {
- changedIDs[to.id] = m.id;
- if (to[i].tokens?.length) {
- if (!m.tokens) m.tokens = [];
- to[i].tokens.forEach((id) => {
- if (!m.tokens.includes(id)) m.tokens.push(id);
- });
- }
- to[i] = m;
- }
- }
-
- // If parent's id has been changed we need to update all the children
- to.forEach((m) => {
- let pID = m.overlayConfig?.parentID;
- if (pID && pID in changedIDs) {
- m.overlayConfig.parentID = changedIDs[pID];
- }
- });
-
- return to;
- }
-
- export function isResponsibleGM() {
- const isResponsibleGM = !game.users
- .filter((user) => user.isGM && (user.active || user.isActive))
- .some((other) => other.id < game.user.id);
- return isResponsibleGM;
- }
|