Vincent Vandemeulebrouck
864194e3b4
Correction de message d'erreur 'User ... lacks permission to update Item ... in parent Actor' Causé par le traitement d'un hook onUpdateActor qui semble autorisé à modifier l'actor, mais ne l'est pas - lors de modifications/ajouts de blessures - lors de l'ajout d'effets
292 lines
8.3 KiB
JavaScript
292 lines
8.3 KiB
JavaScript
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<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.filter(u => document.testUserPermission(u, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)) == game.user
|
|
}
|
|
|
|
static isOwnerPlayerOrUniqueConnectedGM(actor) {
|
|
return Misc.isFirstOwnerPlayer(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)), '<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({
|
|
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
|
|
};
|
|
}
|
|
}
|