import { BASE_IMAGE_CATEGORIES, userRequiresImageCache, waitForTokenTexture } from './utils.js';
|
|
import { ForgeSearchPaths } from '../applications/forgeSearchPaths.js';
|
|
import TokenHUDClientSettings from '../applications/tokenHUDClientSettings.js';
|
|
import CompendiumMapConfig from '../applications/compendiumMap.js';
|
|
import ImportExport from '../applications/importExport.js';
|
|
import ConfigureSettings from '../applications/configureSettings.js';
|
|
import { cacheImages, saveCache } from './search.js';
|
|
import { registerAllHooks } from './hooks/hooks.js';
|
|
import { registerAllWrappers } from './wrappers/wrappers.js';
|
|
|
|
export const TVA_CONFIG = {
|
|
debug: false,
|
|
disableNotifs: false,
|
|
searchPaths: [
|
|
{
|
|
text: 'modules/caeora-maps-tokens-assets/assets/tokens',
|
|
cache: true,
|
|
source: typeof ForgeAPI === 'undefined' ? 'data' : 'forge-bazaar',
|
|
types: ['Portrait', 'Token', 'PortraitAndToken'],
|
|
},
|
|
],
|
|
forgeSearchPaths: {},
|
|
worldHud: {
|
|
displayOnlySharedImages: false,
|
|
disableIfTHWEnabled: false,
|
|
includeKeywords: false,
|
|
updateActorImage: false,
|
|
useNameSimilarity: false,
|
|
includeWildcard: true,
|
|
showFullPath: false,
|
|
animate: true,
|
|
},
|
|
hud: {
|
|
enableSideMenu: true,
|
|
displayAsImage: true,
|
|
imageOpacity: 50,
|
|
},
|
|
keywordSearch: true,
|
|
excludedKeywords: 'and,for',
|
|
runSearchOnPath: false,
|
|
searchFilters: {},
|
|
algorithm: {
|
|
exact: false,
|
|
fuzzy: true,
|
|
fuzzyLimit: 100,
|
|
fuzzyThreshold: 0.3,
|
|
fuzzyArtSelectPercentSlider: true,
|
|
},
|
|
tokenConfigs: [],
|
|
randomizer: {
|
|
actorCreate: false,
|
|
tokenCreate: false,
|
|
tokenCopyPaste: false,
|
|
tokenName: true,
|
|
keywords: false,
|
|
shared: false,
|
|
wildcard: false,
|
|
representedActorDisable: false,
|
|
linkedActorDisable: true,
|
|
popupOnDisable: false,
|
|
diffImages: false,
|
|
syncImages: false,
|
|
nonRepeat: false,
|
|
},
|
|
popup: {
|
|
disableAutoPopupOnActorCreate: true,
|
|
disableAutoPopupOnTokenCreate: true,
|
|
disableAutoPopupOnTokenCopyPaste: true,
|
|
twoPopups: false,
|
|
twoPopupsNoDialog: false,
|
|
},
|
|
imgurClientId: '',
|
|
stackStatusConfig: true,
|
|
mergeGroup: false,
|
|
staticCache: false,
|
|
staticCacheFile: 'modules/token-variants/token-variants-cache.json',
|
|
tilesEnabled: true,
|
|
compendiumMapper: {
|
|
missingOnly: false,
|
|
diffImages: false,
|
|
showImages: true,
|
|
cache: false,
|
|
autoDisplayArtSelect: true,
|
|
syncImages: false,
|
|
overrideCategory: false,
|
|
category: 'Token',
|
|
missingImages: [{ document: 'all', image: CONST.DEFAULT_TOKEN }],
|
|
searchOptions: {},
|
|
},
|
|
permissions: {
|
|
popups: {
|
|
1: false,
|
|
2: false,
|
|
3: true,
|
|
4: true,
|
|
},
|
|
portrait_right_click: {
|
|
1: false,
|
|
2: false,
|
|
3: true,
|
|
4: true,
|
|
},
|
|
image_path_button: {
|
|
1: false,
|
|
2: false,
|
|
3: true,
|
|
4: true,
|
|
},
|
|
hud: {
|
|
1: true,
|
|
2: true,
|
|
3: true,
|
|
4: true,
|
|
},
|
|
hudFullAccess: {
|
|
1: false,
|
|
2: false,
|
|
3: true,
|
|
4: true,
|
|
},
|
|
statusConfig: {
|
|
1: false,
|
|
2: false,
|
|
3: true,
|
|
4: true,
|
|
},
|
|
},
|
|
globalMappings: [],
|
|
templateMappings: [],
|
|
customImageCategories: [],
|
|
displayEffectIconsOnHover: false,
|
|
disableEffectIcons: false,
|
|
filterEffectIcons: false,
|
|
filterCustomEffectIcons: true,
|
|
filterIconList: [],
|
|
updateTokenProto: false,
|
|
imgNameContainsDimensions: false,
|
|
imgNameContainsFADimensions: false,
|
|
playVideoOnHover: true,
|
|
pauseVideoOnHoverOut: false,
|
|
disableImageChangeOnPolymorphed: false,
|
|
disableImageUpdateOnNonPrototype: false,
|
|
disableTokenUpdateAnimation: false,
|
|
mappingsCurrentSceneOnly: false,
|
|
evaluateOverlayOnHover: true,
|
|
invisibleImage: '',
|
|
systemHpPath: '',
|
|
internalEffects: {
|
|
hpChange: { enabled: false, duration: null },
|
|
},
|
|
hideElevationTooltip: false,
|
|
hideTokenBorder: false,
|
|
};
|
|
|
|
export const FEATURE_CONTROL = {
|
|
EffectMappings: true,
|
|
EffectIcons: true,
|
|
Overlays: true,
|
|
UserMappings: true,
|
|
Wildcards: true,
|
|
PopUpAndRandomize: true,
|
|
HUD: true,
|
|
HideElement: true,
|
|
};
|
|
|
|
export function registerSettings() {
|
|
game.settings.register('token-variants', 'featureControl', {
|
|
scope: 'world',
|
|
config: false,
|
|
type: Object,
|
|
default: FEATURE_CONTROL,
|
|
onChange: async (val) => {
|
|
mergeObject(FEATURE_CONTROL, val);
|
|
registerAllHooks();
|
|
registerAllWrappers();
|
|
},
|
|
});
|
|
mergeObject(FEATURE_CONTROL, game.settings.get('token-variants', 'featureControl'));
|
|
|
|
game.settings.registerMenu('token-variants', 'settings', {
|
|
name: 'Configure Settings',
|
|
hint: 'Configure Token Variant Art settings',
|
|
label: 'Settings',
|
|
scope: 'world',
|
|
icon: 'fas fa-cog',
|
|
type: ConfigureSettings,
|
|
restricted: true,
|
|
});
|
|
|
|
const systemHpPaths = {
|
|
'cyberpunk-red-core': 'derivedStats.hp',
|
|
lfg: 'health',
|
|
worldbuilding: 'health',
|
|
twodsix: 'hits',
|
|
};
|
|
TVA_CONFIG.systemHpPath = systemHpPaths[game.system.id] ?? 'attributes.hp';
|
|
|
|
game.settings.register('token-variants', 'effectMappingToggleGroups', {
|
|
scope: 'world',
|
|
config: false,
|
|
type: Object,
|
|
default: { Default: true },
|
|
});
|
|
|
|
game.settings.register('token-variants', 'settings', {
|
|
scope: 'world',
|
|
config: false,
|
|
type: Object,
|
|
default: TVA_CONFIG,
|
|
onChange: async (val) => {
|
|
// Generate a diff, it will be required when doing post-processing of the modified settings
|
|
const diff = _arrayAwareDiffObject(TVA_CONFIG, val);
|
|
|
|
// Check image re-cache required due to permission changes
|
|
let requiresImageCache = false;
|
|
if ('permissions' in diff) {
|
|
if (!userRequiresImageCache(TVA_CONFIG.permissions) && userRequiresImageCache(val.permissions))
|
|
requiresImageCache = true;
|
|
}
|
|
|
|
// Update live settings
|
|
mergeObject(TVA_CONFIG, val);
|
|
|
|
if (TVA_CONFIG.filterEffectIcons && ('filterCustomEffectIcons' in diff || 'filterIconList' in diff)) {
|
|
for (const tkn of canvas.tokens.placeables) {
|
|
waitForTokenTexture(tkn, (token) => {
|
|
token.drawEffects();
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check image re-cache required due to search path changes
|
|
if ('searchPaths' in diff || 'forgeSearchPaths' in diff) {
|
|
if (userRequiresImageCache(TVA_CONFIG.permissions)) requiresImageCache = true;
|
|
}
|
|
|
|
// Cache/re-cache images if necessary
|
|
if (requiresImageCache) {
|
|
await cacheImages();
|
|
}
|
|
|
|
if (diff.staticCache) {
|
|
const cacheFile = diff.staticCacheFile ? diff.staticCacheFile : TVA_CONFIG.staticCacheFile;
|
|
saveCache(cacheFile);
|
|
}
|
|
|
|
TVA_CONFIG.hud = game.settings.get('token-variants', 'hudSettings');
|
|
|
|
registerAllHooks();
|
|
registerAllWrappers();
|
|
|
|
if ('displayEffectIconsOnHover' in diff) {
|
|
for (const tkn of canvas.tokens.placeables) {
|
|
if (tkn.effects) tkn.effects.visible = !diff.displayEffectIconsOnHover;
|
|
}
|
|
}
|
|
|
|
if ('hideElevationTooltip' in diff) {
|
|
for (const tkn of canvas.tokens.placeables) {
|
|
if (tkn.tooltip) tkn.tooltip.text = tkn._getTooltipText();
|
|
}
|
|
}
|
|
|
|
if ('hideTokenBorder' in diff) {
|
|
for (const tkn of canvas.tokens.placeables) {
|
|
if (tkn.border) tkn.border.visible = !diff.hideTokenBorder;
|
|
}
|
|
}
|
|
|
|
if ('filterEffectIcons' in diff || 'disableEffectIcons' in diff) {
|
|
for (const tkn of canvas.tokens.placeables) {
|
|
tkn.drawEffects();
|
|
}
|
|
}
|
|
},
|
|
});
|
|
|
|
game.settings.register('token-variants', 'debug', {
|
|
scope: 'world',
|
|
config: false,
|
|
type: Boolean,
|
|
default: TVA_CONFIG.debug,
|
|
onChange: (val) => (TVA_CONFIG.debug = val),
|
|
});
|
|
|
|
if (typeof ForgeAPI !== 'undefined') {
|
|
game.settings.registerMenu('token-variants', 'forgeSearchPaths', {
|
|
name: game.i18n.localize('token-variants.settings.forge-search-paths.Name'),
|
|
hint: game.i18n.localize('token-variants.settings.forge-search-paths.Hint'),
|
|
icon: 'fas fa-search',
|
|
type: ForgeSearchPaths,
|
|
scope: 'client',
|
|
restricted: false,
|
|
});
|
|
}
|
|
|
|
game.settings.register('token-variants', 'tokenConfigs', {
|
|
scope: 'world',
|
|
config: false,
|
|
type: Array,
|
|
default: TVA_CONFIG.tokenConfigs,
|
|
onChange: (val) => (TVA_CONFIG.tokenConfigs = val),
|
|
});
|
|
|
|
game.settings.registerMenu('token-variants', 'tokenHUDSettings', {
|
|
name: game.i18n.localize('token-variants.settings.token-hud.Name'),
|
|
hint: game.i18n.localize('token-variants.settings.token-hud.Hint'),
|
|
scope: 'client',
|
|
icon: 'fas fa-images',
|
|
type: TokenHUDClientSettings,
|
|
restricted: false,
|
|
});
|
|
|
|
game.settings.registerMenu('token-variants', 'compendiumMapper', {
|
|
name: game.i18n.localize('token-variants.settings.compendium-mapper.Name'),
|
|
hint: game.i18n.localize('token-variants.settings.compendium-mapper.Hint'),
|
|
scope: 'world',
|
|
icon: 'fas fa-cogs',
|
|
type: CompendiumMapConfig,
|
|
restricted: true,
|
|
});
|
|
|
|
game.settings.register('token-variants', 'compendiumMapper', {
|
|
scope: 'world',
|
|
config: false,
|
|
type: Object,
|
|
default: TVA_CONFIG.compendiumMapper,
|
|
onChange: (val) => (TVA_CONFIG.compendiumMapper = val),
|
|
});
|
|
|
|
game.settings.register('token-variants', 'hudSettings', {
|
|
scope: 'client',
|
|
config: false,
|
|
type: Object,
|
|
default: TVA_CONFIG.hud,
|
|
onChange: (val) => (TVA_CONFIG.hud = val),
|
|
});
|
|
|
|
game.settings.registerMenu('token-variants', 'importExport', {
|
|
name: `Import/Export`,
|
|
hint: game.i18n.localize('token-variants.settings.import-export.Hint'),
|
|
scope: 'world',
|
|
icon: 'fas fa-toolbox',
|
|
type: ImportExport,
|
|
restricted: true,
|
|
});
|
|
|
|
// Read settings
|
|
const settings = game.settings.get('token-variants', 'settings');
|
|
mergeObject(TVA_CONFIG, settings);
|
|
|
|
if (isEmpty(TVA_CONFIG.searchFilters)) {
|
|
BASE_IMAGE_CATEGORIES.forEach((cat) => {
|
|
TVA_CONFIG.searchFilters[cat] = {
|
|
include: '',
|
|
exclude: '',
|
|
regex: '',
|
|
};
|
|
});
|
|
}
|
|
|
|
for (let uid in TVA_CONFIG.forgeSearchPaths) {
|
|
TVA_CONFIG.forgeSearchPaths[uid].paths = TVA_CONFIG.forgeSearchPaths[uid].paths.map((p) => {
|
|
if (!p.source) {
|
|
p.source = 'forgevtt';
|
|
}
|
|
if (!p.types) {
|
|
if (p.tiles) p.types = ['Tile'];
|
|
else p.types = ['Portrait', 'Token', 'PortraitAndToken'];
|
|
}
|
|
return p;
|
|
});
|
|
}
|
|
|
|
// 20/07/2023 Convert globalMappings to a new format
|
|
if (getType(settings.globalMappings) === 'Object') {
|
|
Hooks.once('ready', () => {
|
|
TVA_CONFIG.globalMappings = migrateMappings(settings.globalMappings);
|
|
setTimeout(() => updateSettings({ globalMappings: TVA_CONFIG.globalMappings }), 10000);
|
|
});
|
|
}
|
|
|
|
// Read client settings
|
|
TVA_CONFIG.hud = game.settings.get('token-variants', 'hudSettings');
|
|
}
|
|
|
|
export function migrateMappings(mappings, globalMappings = []) {
|
|
if (!mappings) return [];
|
|
if (getType(mappings) === 'Object') {
|
|
let nMappings = [];
|
|
for (const [effect, mapping] of Object.entries(mappings)) {
|
|
if (!mapping.label) mapping.label = effect.replaceAll('¶', '.');
|
|
if (!mapping.expression) mapping.expression = effect.replaceAll('¶', '.');
|
|
if (!mapping.id) mapping.id = randomID(8);
|
|
delete mapping.effect;
|
|
if (mapping.overlayConfig) mapping.overlayConfig.id = mapping.id;
|
|
delete mapping.overlayConfig?.effect;
|
|
nMappings.push(mapping);
|
|
}
|
|
// Convert parents to parentIDs
|
|
let combMappings = nMappings.concat(globalMappings);
|
|
for (const mapping of nMappings) {
|
|
if (mapping.overlayConfig?.parent) {
|
|
if (mapping.overlayConfig.parent === 'Token (Placeable)') {
|
|
mapping.overlayConfig.parentID = 'TOKEN';
|
|
} else {
|
|
const parent = combMappings.find((m) => m.label === mapping.overlayConfig.parent);
|
|
if (parent) mapping.overlayConfig.parentID = parent.id;
|
|
else mapping.overlayConfig.parentID = '';
|
|
}
|
|
delete mapping.overlayConfig.parent;
|
|
}
|
|
}
|
|
return nMappings;
|
|
}
|
|
return mappings;
|
|
}
|
|
|
|
export function getFlagMappings(object) {
|
|
if (!object) return [];
|
|
let doc = object.document ?? object;
|
|
const actorId = doc.actor?.id;
|
|
if (actorId) {
|
|
doc = game.actors.get(actorId);
|
|
if (!doc) return [];
|
|
}
|
|
|
|
// 23/07/2023
|
|
let mappings = doc.getFlag('token-variants', 'effectMappings') ?? [];
|
|
if (getType(mappings) === 'Object') {
|
|
mappings = migrateMappings(mappings, TVA_CONFIG.globalMappings);
|
|
doc.setFlag('token-variants', 'effectMappings', mappings);
|
|
}
|
|
return mappings;
|
|
}
|
|
|
|
export function exportSettingsToJSON() {
|
|
const settings = deepClone(TVA_CONFIG);
|
|
const filename = `token-variants-settings.json`;
|
|
saveDataToFile(JSON.stringify(settings, null, 2), 'text/json', filename);
|
|
}
|
|
|
|
export async function importSettingsFromJSON(json) {
|
|
if (typeof json === 'string') json = JSON.parse(json);
|
|
|
|
if (json.forgeSearchPaths)
|
|
for (let uid in json.forgeSearchPaths) {
|
|
json.forgeSearchPaths[uid].paths = json.forgeSearchPaths[uid].paths.map((p) => {
|
|
if (!p.source) {
|
|
p.source = 'forgevtt';
|
|
}
|
|
if (!p.types) {
|
|
if (p.tiles) p.types = ['Tile'];
|
|
else p.types = ['Portrait', 'Token', 'PortraitAndToken'];
|
|
}
|
|
return p;
|
|
});
|
|
}
|
|
|
|
// 09/07/2022 Convert filters to new format if old one is still in use
|
|
if (json.searchFilters && json.searchFilters.portraitFilterInclude != null) {
|
|
const filters = json.searchFilters;
|
|
json.searchFilters = {
|
|
Portrait: {
|
|
include: filters.portraitFilterInclude ?? '',
|
|
exclude: filters.portraitFilterExclude ?? '',
|
|
regex: filters.portraitFilterRegex ?? '',
|
|
},
|
|
Token: {
|
|
include: filters.tokenFilterInclude ?? '',
|
|
exclude: filters.tokenFilterExclude ?? '',
|
|
regex: filters.tokenFilterRegex ?? '',
|
|
},
|
|
PortraitAndToken: {
|
|
include: filters.generalFilterInclude ?? '',
|
|
exclude: filters.generalFilterExclude ?? '',
|
|
regex: filters.generalFilterRegex ?? '',
|
|
},
|
|
};
|
|
if (json.compendiumMapper) delete json.compendiumMapper.searchFilters;
|
|
}
|
|
|
|
// Global Mappings need special merge
|
|
if (json.globalMappings) {
|
|
const nMappings = migrateMappings(json.globalMappings);
|
|
for (const m of nMappings) {
|
|
const i = TVA_CONFIG.globalMappings.findIndex((mapping) => m.label === mapping.label);
|
|
if (i === -1) TVA_CONFIG.globalMappings.push(m);
|
|
else TVA_CONFIG.globalMappings[i] = m;
|
|
}
|
|
json.globalMappings = TVA_CONFIG.globalMappings;
|
|
}
|
|
|
|
updateSettings(json);
|
|
}
|
|
|
|
function _refreshFilters(filters, customCategories, updateTVAConfig = false) {
|
|
const categories = BASE_IMAGE_CATEGORIES.concat(customCategories ?? TVA_CONFIG.customImageCategories);
|
|
for (const filter in filters) {
|
|
if (!categories.includes(filter)) {
|
|
delete filters[filter];
|
|
if (updateTVAConfig) delete TVA_CONFIG.searchFilters[filter];
|
|
}
|
|
}
|
|
for (const category of customCategories) {
|
|
if (filters[category] == null) {
|
|
filters[category] = {
|
|
include: '',
|
|
exclude: '',
|
|
regex: '',
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function updateSettings(newSettings) {
|
|
const settings = mergeObject(deepClone(TVA_CONFIG), newSettings, { insertKeys: false });
|
|
// Custom image categories might have changed, meaning we may have filters that are no longer relevant
|
|
// or need to be added
|
|
if ('customImageCategories' in newSettings) {
|
|
_refreshFilters(settings.searchFilters, newSettings.customImageCategories, true);
|
|
if (settings.compendiumMapper?.searchOptions?.searchFilters != null) {
|
|
_refreshFilters(settings.compendiumMapper.searchOptions.searchFilters, newSettings.customImageCategories);
|
|
TVA_CONFIG.compendiumMapper.searchOptions.searchFilters = settings.compendiumMapper.searchOptions.searchFilters;
|
|
}
|
|
}
|
|
await game.settings.set('token-variants', 'settings', settings);
|
|
}
|
|
|
|
export function _arrayAwareDiffObject(original, other, { inner = false } = {}) {
|
|
function _difference(v0, v1) {
|
|
let t0 = getType(v0);
|
|
let t1 = getType(v1);
|
|
if (t0 !== t1) return [true, v1];
|
|
if (t0 === 'Array') return [!_arrayEquality(v0, v1), v1];
|
|
if (t0 === 'Object') {
|
|
if (isEmpty(v0) !== isEmpty(v1)) return [true, v1];
|
|
let d = _arrayAwareDiffObject(v0, v1, { inner });
|
|
return [!isEmpty(d), d];
|
|
}
|
|
return [v0 !== v1, v1];
|
|
}
|
|
|
|
// Recursively call the _difference function
|
|
return Object.keys(other).reduce((obj, key) => {
|
|
if (inner && !(key in original)) return obj;
|
|
let [isDifferent, difference] = _difference(original[key], other[key]);
|
|
if (isDifferent) obj[key] = difference;
|
|
return obj;
|
|
}, {});
|
|
}
|
|
|
|
function _arrayEquality(a1, a2) {
|
|
if (!(a2 instanceof Array) || a2.length !== a1.length) return false;
|
|
return a1.every((v, i) => {
|
|
if (getType(v) === 'Object') return Object.keys(_arrayAwareDiffObject(v, a2[i])).length === 0;
|
|
return a2[i] === v;
|
|
});
|
|
}
|
|
|
|
export function getSearchOptions() {
|
|
return {
|
|
keywordSearch: TVA_CONFIG.keywordSearch,
|
|
excludedKeywords: TVA_CONFIG.excludedKeywords,
|
|
runSearchOnPath: TVA_CONFIG.runSearchOnPath,
|
|
algorithm: TVA_CONFIG.algorithm,
|
|
searchFilters: TVA_CONFIG.searchFilters,
|
|
};
|
|
}
|