All checks were successful
		
		
	
	Release Creation / build (release) Successful in 2m32s
				
			
		
			
				
	
	
		
			285 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			285 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import { Grammar } from "./grammar.js";
 | |
| 
 | |
| const DEFAULT_FIND_OPTIONS = {
 | |
|   mapper: it => it.name,
 | |
|   preFilter: it => true,
 | |
|   description: 'valeur',
 | |
|   onMessage: m => ui.notifications.info(m)
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * 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) {
 | |
|     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<min, the array is decrementing integers 
 | |
|    */
 | |
|   static intArray(from, to) {
 | |
|     if (from > 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 || params.length == 0) ? '' : 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 (Misc.isFirstConnectedGM() || (Misc.connectedGMs().length == 0 && Misc.isFirstOwnerPlayer(document))) {
 | |
|       return document
 | |
|     }
 | |
|     return undefined
 | |
|   }
 | |
| 
 | |
|   static isOwnerPlayer(document) {
 | |
|     return document.testUserPermission && document.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)
 | |
|   }
 | |
| 
 | |
|   static isFirstOwnerPlayer(document) {
 | |
|     if (!document.testUserPermission) {
 | |
|       return false
 | |
|     }
 | |
|     return game.users.find(u => document.testUserPermission(u, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)) == game.user
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @returns true pour un seul utilisateur: le premier GM connecté par ordre d'id
 | |
|    */
 | |
|   static isFirstConnectedGM() {
 | |
|     return game.user == Misc.firstConnectedGM();
 | |
|   }
 | |
|   
 | |
|   static hasConnectedGM() {
 | |
|     return 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(DEFAULT_FIND_OPTIONS, options, { overwrite: true, inplace: false });
 | |
|     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)), '<br>');
 | |
|       options.onMessage(`Plusieurs choix de ${options.description}s possibles:<br>${choices}<br>Le premier sera choisi: ${options.mapper(single)}`);
 | |
|     }
 | |
|     return single;
 | |
|   }
 | |
| 
 | |
|   static findAllLike(value, elements, options = {}) {
 | |
|     options = foundry.utils.mergeObject(DEFAULT_FIND_OPTIONS, options, { overwrite: true, inplace: false });
 | |
|     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))
 | |
|       .sort(Misc.ascending(it => options.mapper(it)))
 | |
|     if (subset.length == 0 && options?.onMessage) {
 | |
|       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
 | |
|     };
 | |
|   }
 | |
| }
 |