import { Grammar } from "./grammar.js"; /** * This class is intended as a placeholder for utility methods unrelated * to actual classes of the game system or of FoundryVTT */ export class Misc { static isFunction(v) { return v && {}.toString.call(v) === '[object Function]'; } static upperFirst(text) { return text.charAt(0).toUpperCase() + text.slice(1); } static lowerFirst(text) { return text.charAt(0).toLowerCase() + text.slice(1); } static toSignedString(number) { const value = parseInt(number) const isPositiveNumber = value != NaN && value > 0; return isPositiveNumber ? "+" + number : number } static modulo(n, m) { return ((n % m) + m) % m; } static sum() { return (a, b) => Number(a) + Number(b); } static ascending(orderFunction = x => x) { return (a, b) => Misc.sortingBy(orderFunction(a), orderFunction(b)); } static descending(orderFunction = x => x) { return (a, b) => Misc.sortingBy(orderFunction(b), orderFunction(a)); } static sortingBy(a, b) { if (a > b) return 1; if (a < b) return -1; return 0; } static typeName(type, subType) { return subType ? game.i18n.localize(`TYPES.${type}.${subType}`) : ''; } static arrayOrEmpty(items) { return items?.length ? items : []; } /** * Converts the value to an integer, or to 0 if undefined/null/not representing integer * @param {*} value value to convert to an integer using parseInt */ static toInt(value) { if (value == undefined) { return 0; } const parsed = parseInt(value); return isNaN(parsed) ? 0 : parsed; } static keepDecimals(num, decimals) { if (decimals <= 0 || decimals > 6) return num; const power10n = Math.pow(10, parseInt(decimals)); return Math.round(num * power10n) / power10n; } static getFractionHtml(diviseur) { if (!diviseur || diviseur <= 1) return undefined; switch (diviseur || 1) { case 2: return '½'; case 4: return '¼'; default: return '1/' + diviseur; } } static indexLowercase(list) { const obj = {}; const addToObj = (map, val) => { const key = Grammar.toLowerCaseNoAccent(val); if (key && !map[key]) map[key] = val } list.forEach(it => addToObj(obj, it)) return obj; } static concat(lists) { return lists.reduce((a, b) => a.concat(b), []); } static classify(items, classifier = it => it.type) { let itemsBy = {} Misc.classifyInto(itemsBy, items, classifier) return itemsBy } static classifyFirst(items, classifier) { let itemsBy = {}; for (const item of items) { const classification = classifier(item); if (!itemsBy[classification]) { itemsBy[classification] = item; } } return itemsBy; } static classifyInto(itemsBy, items, classifier = it => it.type) { for (const item of items) { const classification = classifier(item) let list = itemsBy[classification]; if (!list) { list = [] itemsBy[classification] = list } list.push(item) } } /** * @returns an array of incremental integers (including from / excluding to). * if max to) { return Array.from(Array(from - to).keys()).map(i => from - i) } return Array.from(Array(to - from).keys()).map(i => from + i) } static distinct(array) { return [...new Set(array)]; } static join(params, separator = '') { return params?.reduce(Misc.joining(separator)) ?? ''; } static joining(separator = '') { return (a, b) => a + separator + b; } static connectedGMOrUser(ownerId = undefined) { if (ownerId && game.user.id == ownerId) { return ownerId; } return Misc.firstConnectedGM()?.id ?? game.user.id; } static isRollModeHiddenToPlayer() { switch (game.settings.get("core", "rollMode")) { case CONST.DICE_ROLL_MODES.BLIND: case CONST.DICE_ROLL_MODES.SELF: return true; } return false } static getActiveUser(id) { return game.users.find(u => u.id == id && u.active); } static firstConnectedGM() { if (foundry.utils.isNewerVersion(game.release.version, '12.0')) { return game.users.activeGM } return game.users.find(u => u.isGM && u.active); } static connectedGMs() { return game.users.filter(u => u.isGM && u.active); } /** * This helper method allows to get the docuument, for a single user (either first connected GM, or the owner * if there is no connected GMs), or else return undefined. * * This allows for example update hooks that should apply modifications to actors to be called only for one * user (preventing the "User ... lacks permission to update Item" that was occuring on hooks when Item updates * were triggering other changes) * * @param {*} document the Document with is potentially an Actor * @returns the actor if either the game.user is the first connected GM, or if the game.user is the owner * and there is no connected GM */ static documentIfResponsible(document) { if (foundry.utils.isNewerVersion(game.release.version, '12.0')) { if (game.users.activeGM || (Misc.connectedGMs().length == 0 && Misc.isOwnerPlayer(document))) { return document } } else if (Misc.isFirstConnectedGM() || (Misc.connectedGMs().length == 0 && Misc.isOwnerPlayer(document))) { return document } return undefined } static isOwnerPlayer(document) { return document.testUserPermission && document.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER) } static isOwnerPlayerOrUniqueConnectedGM(actor) { return Misc.isOwnerPlayer(actor) ?? Misc.isFirstConnectedGM(); } /** * @returns true pour un seul utilisateur: le premier GM connecté par ordre d'id */ static isFirstConnectedGM() { return game.user == Misc.firstConnectedGM(); } static firstConnectedGMId() { return Misc.firstConnectedGM()?.id; } /* -------------------------------------------- */ static findPlayer(name) { return Misc.findFirstLike(name, game.users, { description: 'joueur' }); } /* -------------------------------------------- */ static findActor(name, actors = game.actors) { return Misc.findFirstLike(name, actors, { description: 'acteur' }); } /* -------------------------------------------- */ static findFirstLike(value, elements, options = {}) { options = foundry.utils.mergeObject({ mapper: it => it.name, preFilter: it => true, description: 'valeur', onMessage: m => ui.notifications.info(m) }, options); const subset = this.findAllLike(value, elements, options); if (subset.length == 0) { console.log(`Aucune ${options.description} pour ${value}`); return undefined } if (subset.length == 1) { return subset[0] } let single = subset.find(it => Grammar.toLowerCaseNoAccent(options.mapper(it)) == Grammar.toLowerCaseNoAccent(value)); if (!single) { single = subset[0]; const choices = Misc.join(subset.map(it => options.mapper(it)), '
'); options.onMessage(`Plusieurs choix de ${options.description}s possibles:
${choices}
Le premier sera choisi: ${options.mapper(single)}`); } return single; } static findAllLike(value, elements, options = {}) { options = foundry.utils.mergeObject({ mapper: it => it.name, preFilter: it => true, description: 'valeur', onMessage: m => ui.notifications.info(m) }, options); if (!value) { options.onMessage(`Pas de ${options.description} correspondant à une valeur vide`); return []; } value = Grammar.toLowerCaseNoAccent(value); const subset = elements.filter(options.preFilter) .filter(it => Grammar.toLowerCaseNoAccent(options.mapper(it))?.includes(value)); if (subset.length == 0) { options.onMessage(`Pas de ${options.description} correspondant à ${value}`); } return subset; } static cssRotation(angle) { const rotation = `rotate(${angle}deg)`; return { 'transform': rotation, '-ms-transform': rotation, '-moz-transform': rotation, '-webkit-transform': rotation, '-o-transform': rotation }; } }