Files
foundryvtt-reve-de-dragon/module/misc.js
T
VincentVk 697247e931 Fix: protection sur actions sans acteur/propriété
Cas rencontré une fois, un jet de chance d'un autre
joueur, pouvait être tenté.

Cas plus étrange, un jet de chance alors que le jet sauvegardé
n'avait pas d'actorId
2025-12-11 03:12:48 +01:00

323 lines
9.1 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 inRange(value, min, max) {
if (min > max) {
return Misc.inRange(value, max, min)
}
return Math.max(min, Math.min(value, max))
}
static sum() {
return (a, b) => Number(a) + Number(b);
}
static and(predicates) {
return value => predicates.map(predicate => predicate(value))
.reduce((v1, v2) => v1 && v2, true);
}
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 fakeId(base) {
return (base + foundry.utils.randomID(16)).substring(0, 16)
}
static typeName(type, subType) {
return subType ? game.i18n.localize(`TYPES.${type}.${subType}`)
: '';
}
static arrayOrEmpty(items) {
return items?.length ? items : [];
}
static findOrFirst(list, predicate) {
return list.find(predicate) ?? list[0]
}
/**
* 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 getFractionOneN(divider) {
if (!divider || divider == 1) {
return 1
}
switch (divider) {
case 2: return '&frac12;'
case 4: return '&frac14;'
case 8: return '&frac18;'
}
return `1/${divider}`
}
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 indexed(list, index = 'index') {
let i = 0;
return list.map(it => { it[index] = i++; return it })
}
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 async doIfOwner(document, action, orElse = async it => { ui.notifications.warn(`Vous n'avez pas le droit d'agir pour ${it.name}`) }) {
if (Misc.isOwnerPlayer(document)) {
return await action(document)
} else {
return await orElse(document ?? {name: '<aucune sélection>'})
}
}
static isOwnerPlayer(document) {
return document && 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(`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
};
}
}