// ------------------ generateRowID code from the Aaron ---------------------
const generateUUID = (function() {
"use strict";
let a = 0,
b = [];
return function() {
let c = (new Date()).getTime() + 0,
d = c === a;
a = c;
let e = new Array(8);
for (let f = 7; 0 <= f; f--) {
e[f] = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".charAt(c % 64);
c = Math.floor(c / 64);
}
c = e.join("");
if (d) {
let f = 11;
for (; 0 <= f && 63 === b[f]; f--) {
b[f] = 0;
}
b[f]++;
} else {
for (let f = 0; 12 > f; f++) {
b[f] = Math.floor(64 * Math.random());
}
}
for (let f = 0; 12 > f; f++) {
c += "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".charAt(b[f]);
}
return c;
};
}()),
generateRowID = function() {
"use strict";
return generateUUID().replace(/_/g, "Z");
};
//--------------- end generateRowID ----------------------------------------
const COF_BETA = true;
let COF_loaded = false;
// Le script utilise la partie COFantasy de la variable d'état state
// Pour plus de facilité, on utilise stateCOF = state.COFantasy
// Champs utilisés:
// - options : les options de jeu
// - setting_arran : toutes les fiches utilisent les règles Terres d'Arran
// - setting_mixte : on a un mixte de fiches classiques et Terres d'Arran
// - roundMarkerId : l'id du token utilisé pour l'aura d'initiative
// - combat : défini si le jeu est en mode tour par tour, contient :
// - pageId : la pageid du combat
// - activeTokenId : id du token dont c'est le tour
// - activeTokenName : nom du token dont c'est le tour, au cas où l'id change
// - tour : numéro de tour dans le combat
// - init : niveau d'initiative courant
// - armeesDesMorts : map de token id vers perso
// - auras : liste des auras actives
// - aurasCounts : computeur pour id des auras
// - usureOff : on ne compte plus l'usure du combat
// - personnageCibleCree : pour savoir si on a créé un personnage cible (avec 0 PV pour centrer les aoe)
// - tablesCrees : pour savoir si on a créé les tables par défaut
// - gameMacros : la liste des macros créées par le script
// - chargeFantastique : tout ce dont on a besoin pour une charge fantastique en cours (TODO: passer sous combat)
// - eventId : compteur d'events pour avoir une id unique
// - tokensTemps : liste de tokens à durée de vie limitée, effacés à la fin du combat
// - effetAuD20 : les effets qui se produisent à chaque jet de dé.
// chaque effet est déterminé par un champ, puis pour chaque champ,
// - min: valeur minimale du dé pour déclencher
// - max: valeur maximale du dé pour déclencher
// - fct: nom de la fonction à appeler
// - nomFin: nom à afficher pour le statut et mettre fin aux événements
// par exemple, foudreDuTemps pour les foudres du temps
// - tenebresMagiques : état général de ténèbres magiques
// - jetsEnCours : pour laisser le MJ montrer ou non un jet qui lui a été montré à lui seul
// - currentAttackDisplay : pour pouvoir remontrer des display aux joueurs
// - pause : le jeu est en pause
// - prescience : un personnage sur la carte de combat a la capacité prescience (TODO: passer sous combat)
// - nextPrescience : pour le changement de tour car prescience ne revient que d'un tour
// - afterDisplay : données à afficher après un display
// - version : la version du script en cours, pour détecter qu'on change de version
// statistiques : des statistiques pour les jets de dés
// statistiquesEnPause
var COFantasy = COFantasy || function() {
"use strict";
const versionFiche = 5.03;
const PIX_PER_UNIT = 70;
const HISTORY_SIZE = 200;
const BS_LABEL = 'text-transform: uppercase; display: inline; padding: .2em .6em .3em; font-size: 75%; line-height: 2; color: #fff; text-align: center; white-space: nowrap; vertical-align: baseline; border-radius: .25em;';
const BS_LABEL_SUCCESS = 'background-color: #5cb85c;';
const BS_LABEL_INFO = 'background-color: #5bc0de;';
const BS_LABEL_WARNING = 'background-color: #f0ad4e;';
const BS_LABEL_DANGER = 'background-color: #d9534f;';
const DEFAULT_DYNAMIC_INIT_IMG = 'https://s3.amazonaws.com/files.d20.io/images/4095816/086YSl3v0Kz3SlDAu245Vg/thumb.png?1400535580';
const IMG_INVISIBLE = 'https://s3.amazonaws.com/files.d20.io/images/24377109/6L7tn91HZLAQfrLKQI7-Ew/thumb.png?1476950708';
const IMG_BOMB = 'https://s3.amazonaws.com/files.d20.io/images/361033841/dmwnChkZNCI9a0_uKfGcNg/thumb.png?1695976505';
let markerCatalog = {};
let eventHistory = [];
let updateNextInitSet = new Set();
const flashyInitMarkerScale = 1.6;
const attaqueAMainsNues = {
name: 'Mains nues',
attSkill: '@{ATKCAC}',
attNbDices: 1,
attDice: 4,
attDMBonusCommun: 0,
attCarBonus: '@{FOR}',
crit: 20,
divers: '',
portee: 0,
typeDegats: 'contondant',
options: '--tempDmg',
};
const defaultOptions = {
regles: {
explications: "Options qui influent sur les règles du jeu",
type: 'options',
val: {
divers: {
explications: "Options diverses",
type: 'options',
val: {
forme_d_arbre_amelioree: {
explications: "+50% à l'effet de la peau d'écorce en forme d'arbre.",
val: true,
type: 'bool'
},
poudre_explosif: {
explications: "Les armes à poudre font des dégâts explosifs",
val: true,
type: 'bool'
},
interchangeable_attaque: {
explications: "La capacité interchangeable donne moins de DEF mais plus d'attaque",
val: true,
type: 'bool'
},
coups_critiques_etendus: {
explications: "Coup critique à une attaque dès qu'elle dépasse DEF + 10",
val: false,
type: 'bool'
},
echec_critique_boule_de_feu: {
explications: "Nombre de mètre dont le centre d'une boule de feu peut être déplacé de manière aléatoire en cas d'échec critique. La probabilité est inversement proportionelle à la distance.",
val: 12,
type: 'int'
}
}
},
dommages: {
explications: "Règles optionnelles sur les dommages",
type: 'options',
val: {
blessures_graves: {
explications: "Si on arrive à 0 PV, on perd 1PR, et si on a plus de PR, on a une blessure grave.",
val: true,
type: 'bool'
},
degats_importants: {
explications: "Si les DMs dépassent CON+niveau en une attaque, on applique aussi la règle de blessure grave. Si la valeur de cette option est 0, la règles n'est pas appliquée. Sinon, la règle n'est appliquée que si, de plus, les DMs dépassent maxPV / valeur.",
val: 3,
type: 'int'
},
dm_minimum: {
explications: "Dégâts minimum d'une attaque ou autre source de DM.",
val: 0,
type: 'int'
},
crit_elementaire: {
explications: "Les DMs constants d'un autre type que celui de l'arme sont aussi multipliés en cas de critique",
val: false,
type: 'bool'
},
max_rune_protection: {
explications: "Les DMs qu'une rune de protection est capable d'absorber sont limités à 10x le rang du forgesort dans la voie des runes",
val: true,
type: 'bool'
},
dm_explosifs: {
explications: "Tous les dés de DM sont explosifs",
val: false,
type: 'bool'
}
}
},
haute_DEF: {
explications: "Options de jeu pour gérer la haute DEF",
type: 'options',
val: {
usure_DEF: {
explications: "Malus de -2 en DEF tous les n tours. Mettre à 0 pour ne pas avoir de malus d'usure",
val: 6,
type: 'int'
},
bonus_attaque_groupe: {
explications: "Lors d'une attaque de groupe, bonus à la touche par créature supplémentaire",
val: 2,
type: 'int'
},
crit_attaque_groupe: {
explications: "Lors d'une attaque de groupe, si le jet de touche dépasse DEF + cette valeur, les dommages sont doublés (0 = jamais)",
val: 5,
type: 'int'
}
}
},
initiative: {
explications: "Options qui influent sur les règles du jeu",
type: 'options',
val: {
initiative_variable: {
explications: "Ajoute 1d6 à l'initiative, lancé une fois par combat par type de créature",
val: false,
type: 'bool'
},
initiative_variable_individuelle: {
explications: "Lancer l'initiative variable pour chaque créature (nécessite d'activer l'Initiative variable)",
val: false,
type: 'bool'
},
joueurs_lancent_init: {
explications: "Fait apparaître un bouton pour que les joueurs lancent leur initiative (nécessite d'activer l'Initiative variable)",
val: false,
type: 'bool'
}
}
},
mana: {
explications: "Options de Mana",
type: 'options',
val: {
mana_totale: {
explications: "Tous les sorts ont un coût, celui des tempêtes de mana est multiplié par 3",
val: false,
type: 'bool'
},
contrecoup: {
explications: "Avec la Mana Totale, permet au lanceur de sort de payer un déficit de PM en PV (COF p. 181)",
val: false,
type: 'bool'
},
brulure_de_magie: {
explications: "Permettre d'utiliser ses PV comme PM (Magie de CO Terres d'Arran, incompatible avec Mana Totale)",
val: false,
type: 'bool'
},
elixirs_sorts: {
explications: "Toutes fabrications d'élixir sont considérées comme des sorts (qui peuvent coûter de la mana)",
val: true,
type: 'bool'
},
}
}
}
},
affichage: {
explications: "Options d'affichage",
type: 'options',
val: {
MJ_voit_actions: {
explications: "À chaque nouveau personnage en combat, montre le choix d'actions au MJ, même pour les PJs.",
val: false,
type: 'bool'
},
MJ_valide_affichage_attaques: {
explications: "Les résultats des attaques sont d'abord montrées au MJ seul, qui peut ensuite les montrer aux joueurs",
val: false,
type: 'bool'
},
MJ_valide_affichage_jets: {
explications: "Les résultats des jets de caractéristiques sont d'abord montrées au MJ seul, qui peut ensuite les montrer aux joueurs",
val: false,
type: 'bool'
},
avatar_dans_cadres: {
explications: "Si faux, on utilise l'image du token.",
val: true,
type: 'bool'
},
manoeuvres: {
explications: "Affiche les manoeuvres dans la liste d'actions",
val: true,
type: 'bool'
},
actions_par_defaut: {
explications: "Sans ability #Actions#, affiche la liste des abilities",
val: true,
type: 'bool'
},
montre_def: {
explications: "montre la DEF des adversaires dans les cadres de combat",
val: true,
type: 'bool'
},
duree_effets: {
explications: "Le script indique la durée des effets associés aux tokens",
val: false,
type: 'bool'
},
init_dynamique: {
explications: "Fait apparaître une aura dynamique sur le token qui a l'initiative",
val: true,
type: 'bool'
},
markers_personnalises: {
explications: "Utilisation des markers personnalisés commençant par cof",
val: true,
type: 'bool'
},
table_crit: {
explications: "Utilisation d'une table de critiques nommée Echec-Critique-Contact",
val: false,
type: 'bool'
},
depense_mana: {
explications: "Le script précise la quantité de mana utilisée dans le chat à chaque fois",
val: false,
type: 'bool'
}
}
},
images: {
explications: "Images par défaut",
type: 'options',
val: {
image_init: {
explications: "Image utilisée pour indiquer le personnage dont c'est le tour",
type: 'image',
val: DEFAULT_DYNAMIC_INIT_IMG
},
image_double: {
explications: 'Image utilisée pour la capacité dédoublement',
type: 'image',
val: "https://s3.amazonaws.com/files.d20.io/images/33854984/q10B3KtWsCxcMczLo4BSUw/thumb.png?1496303265"
},
image_ombre: {
explications: "Image utilisée pour l'ombre mortelle",
type: 'image',
val: "https://s3.amazonaws.com/files.d20.io/images/2781735/LcllgIHvqvu0HAbWdXZbJQ/thumb.png?13900368485"
},
image_arbre: {
explications: "Image utilisée pour la forme d'arbre",
type: 'image',
val: "https://s3.amazonaws.com/files.d20.io/images/52767134/KEGYUXeKnxZr5dbDwQEO4Q/thumb.png?15248300835"
},
image_mur_de_force: {
explication: "Image utilisée pour un mur de force sphérique",
type: 'image',
val: "https://s3.amazonaws.com/files.d20.io/images/33213510/5r3NGSso1QBJMJewTEKv0A/thumb.png?1495195634"
},
image_mur_de_vent: {
explication: "Image utilisée pour un mur de vent sphérique",
type: 'image',
val: "https://s3.amazonaws.com/files.d20.io/images/308931095/X5zH4itb9QI9La8O7KfMBQ/thumb.png?1665585092"
},
prison_vegetale: {
explication: "Image utilisée pour la prison végétale",
type: 'image',
val: "https://s3.amazonaws.com/files.d20.io/images/254738719/c97DFw6JlEePDVXBf-MPsA/thumb.png?1636471250"
},
zone_de_vie: {
explication: "Image utilisée pour les zones de vie",
type: 'image',
val: "https://s3.amazonaws.com/files.d20.io/images/349749304/q3q75jWu9Izlci5YB688WA/thumb.png?1689005544"
}
}
},
sons: {
explications: "Sons par défaut",
type: 'options',
val: {
attaque_echec_critique: {
explication: "Son utilisé pour les échecs critiques d'attaques",
type: 'son',
val: ''
},
attaque_reussite_critique: {
explication: "Son utilisé pour les réussites critiques d'attaques",
type: 'son',
val: ''
}
}
},
macros_a_jour: {
explications: "Met automatiquement les macros à jour",
type: 'bool',
val: true
}
};
function copyOptions(dst, src) {
for (let o in src) {
let opt = src[o];
let isOption = opt.type == 'options';
if (dst[o] === undefined) {
dst[o] = {
explications: opt.explications,
val: {},
type: opt.type,
};
if (!isOption) dst[o].val = opt.val;
} else {
if (dst[o].explications != opt.explications)
dst[o].explications = opt.explications;
if (dst[o].type != opt.type)
dst[o].type = opt.type;
}
if (isOption) copyOptions(dst[o].val, opt.val);
}
}
//Liste de tables par défaut
const gameTables = [{
name: "Echec-Critique-Contact",
showplayers: false,
items: [{
name: "FOR - Bousculé : le personnage est renversé par son adversaire. Il subit un dé malus au test de FOR si l’adversaire " +
"est d’une catégorie de taille supérieure et bénéficie d’un dé bonus dans le cas inverse. " +
"Il subit une attaque gratuite de la part d’un adversaire pendant qu’il est étalé au sol (-5 en DEF).",
weight: 1,
}, {
name: "DEX - Maladresse : le personnage laisse tomber au sol l'objet avec lequel il attaque. S’il essaye de le ramasser, " +
"il subit une attaque gratuite.",
weight: 1,
}, {
name: "CON - Coup de mou: le personnage subit l’état affaibli pendant 3 rounds. Ou il peut annuler cet état en reprenant" +
"son souffle par une action limitée.",
weight: 1,
}, {
name: "INT - Erreur tactique : le personnage subit une attaque (gratuite) d’un adversaire à son contact.",
weight: 1,
}, {
name: "SAG - Distrait : le personnage se laisse distraire et ne voit pas venir la prochaine attaque, " +
"l’adversaire bénéficiera d’un bonus de +10.",
weight: 1,
}, {
name: "CHA - Ridicule : le personnage fait un faux mouvement à la fois douloureux et ridicule, il subit " +
"l’état étourdi pendant un round pour reprendre contenance.",
weight: 1,
}, {
name: "Votre arme se brise. S’il s’agit d’une arme magique, le dé DM est simplement réduit d'une catégorie " +
"(2d6/d12=>d10=>d8=>d6=>d4=>d3) jusqu’à la fin du combat.",
weight: 1,
}, {
name: "Une pièce d’armure bouge et elle devient plus gênante que protectrice : malus en DEF et en attaque pour le reste du combat. Cuir : -1, Maille : -2, Plaque -3.",
weight: 1,
}, {
name: "Simple échec de l'attaque",
weight: 12,
}, ],
}, ];
let stateCOF = state.COFantasy;
let reglesOptionelles; // = stateCOF.options.regles.val;
// List of states:
const cof_states = {
assomme: 'status_pummeled',
mort: 'status_dead',
surpris: 'status_lightning-helix',
renverse: 'status_back-pain',
aveugle: 'status_bleeding-eye',
affaibli: 'status_half-heart',
etourdi: 'status_half-haze',
paralyse: 'status_fishing-net',
ralenti: 'status_snail',
immobilise: 'status_cobweb',
endormi: 'status_sleepy',
apeure: 'status_screaming',
invisible: 'status_ninja-mask',
blesse: 'status_arrowed',
encombre: 'status_frozen-orb',
penombre: 'status_archery-target',
enseveli: 'status_edge-crack',
chef: 'status_black-flag'
};
//Remplis quand on sait quels sont les markers dans setStateCOF
const etat_de_marker = {};
const effet_de_marker = {};
// Donne le nom de l'attribut, selon qu'il concerne un mook ou un personnage
// unique
// perso peut ne pas avoir de token
function fullAttributeName(perso, attribute, options) {
if (perso.token && (!options || !options.charAttr)) {
let link = perso.token.get('bar1_link');
if (link === '') return attribute + '_' + perso.token.get('name');
}
return attribute;
}
//Retourne une liste d'attributs
//personnage peut ne pas avoir de token
function tokenAttribute(personnage, name) {
let fullName = fullAttributeName(personnage, name);
return findObjs({
_type: 'attribute',
_characterid: personnage.charId,
name: fullName
});
}
function charAttribute(charId, name, option) {
return findObjs({
_type: 'attribute',
_characterid: charId,
name: name
}, option);
}
function toInt(n, def) {
let res = parseInt(n);
if (isNaN(res)) return def;
return res;
}
function attrAsInt(attr, def, defPresent) {
if (attr.length === 0) return def;
if (defPresent === undefined) defPresent = def;
return toInt(attr[0].get('current'), defPresent);
}
function attrAsBool(attr) {
if (attr.length === 0) return false;
attr = attr[0].get('current');
if (attr == '0' || attr == 'false') return false;
if (attr) return true;
return false;
}
// Attention, def, la valeur par défaut, doit être la même que sur la fiche
// personnage peut ne pas avoir de token
function ficheAttribute(personnage, name, def) {
let attr = charAttribute(personnage.charId, name, {
caseInsensitive: true
});
if (attr.length === 0) return def;
return attr[0].get('current');
}
function ficheAttributeMax(personnage, name, def) {
let attr = charAttribute(personnage.charId, name, {
caseInsensitive: true
});
if (attr.length === 0) return def;
return attr[0].get('max');
}
//personnage peut ne pas avoir de token
function ficheAttributeAsInt(personnage, name, def, defPresent) {
let attr = charAttribute(personnage.charId, name, {
caseInsensitive: true
});
if (attr === undefined) return def;
return attrAsInt(attr, def, defPresent);
}
//Il faut une valeur par défaut, qui correspond à celle de la fiche
function ficheAttributeAsBool(personnage, name, def) {
let attr = charAttribute(personnage.charId, name, {
caseInsensitive: true
});
if (attr.length === 0) return def;
return attrAsBool(attr);
}
//Attention à ne pas utiliser si l'attribut ne dépend pas du token
//defPresent est optionnel
//personnage peut ne pas avoir de token
function attributeAsInt(personnage, name, def, defPresent) {
let attr = tokenAttribute(personnage, name);
return attrAsInt(attr, def, defPresent);
}
//personnage peut ne pas avoir de token
function attributeAsBool(personnage, name) {
let attr = tokenAttribute(personnage, name);
return attrAsBool(attr);
}
function charAttributeAsInt(perso, name, def, defPresent) {
let attr = charAttribute(perso.charId, name);
return attrAsInt(attr, def, defPresent);
}
function charAttributeAsBool(perso, name) {
let attr = charAttribute(perso.charId, name);
return attrAsBool(attr);
}
function assignPredicate(pred, name, val) {
if (pred[name]) {
if (!Array.isArray(pred[name])) pred[name] = [pred[name]];
pred[name].push(val);
} else pred[name] = val;
}
function predicateOfRaw(raw) {
let pred = {};
let last, assign;
//On coupe d'abord par ligne
let lignes = raw.split('\n');
lignes.forEach(function(ligne) {
let indexPredComplexe = ligne.indexOf('::');
let indexCommentaire = ligne.indexOf('//');
let valeurPredComplexe = '';
if (indexPredComplexe > 0 &&
(indexCommentaire == -1 || indexCommentaire > indexPredComplexe)) {
valeurPredComplexe = ligne.substring(indexPredComplexe + 2);
ligne = ligne.substring(0, indexPredComplexe);
} else if (indexCommentaire > 0) {
ligne = ligne.substring(0, indexCommentaire);
}
ligne.split(/,| /).forEach(function(p) {
p = p.trim();
if (p === '') return;
if (p === ':') {
if (last) assign = true;
return;
}
let i = p.indexOf(':');
if (i < 0) {
if (last) {
if (assign) {
assignPredicate(pred, last, p);
assign = false;
} else pred[last] = true;
}
last = p;
return;
}
//p contient ':' mais pas seulement
if (i === 0) {
if (last) {
assignPredicate(pred, last, p.substring(1));
last = undefined;
} else {
last = p.substring(1);
}
assign = false;
return;
} //p ne commence pas par ':'
if (last) pred[last] = true;
if (i == p.length - 1) {
last = p.substring(0, i);
assign = true;
} else {
assignPredicate(pred, p.substring(0, i), p.substring(i + 1));
last = undefined;
assign = false;
}
});
if (last && valeurPredComplexe !== '') {
assignPredicate(pred, last, valeurPredComplexe);
last = undefined;
}
});
if (last) pred[last] = true;
return pred;
}
function charPredicateAsBool(charId, name) {
let pred = stateCOF.predicats[charId];
if (pred) return pred[name];
let raw = '';
let attr = charAttribute(charId, 'predicats_script', {
caseInsensitive: true
});
if (attr.length > 0) {
raw = attr[0].get('current');
}
let perso = {
charId
};
let labelArmeGauche = 0;
let armures;
let labelArmure = ficheAttribute(perso, 'torseequipe', '0');
if (labelArmure && labelArmure != '-1') {
armures = armures || listAllArmors(perso);
let armure = armures[labelArmure];
if (armure) {
let rawArmure = fieldAsString(armure, 'effetarmure', '');
if (rawArmure) raw += '\n' + rawArmure;
}
}
let labelCasque = ficheAttribute(perso, 'teteequipe', '0');
if (labelCasque && labelCasque != '-1') {
armures = armures || listAllArmors(perso);
let casque = armures[labelCasque];
if (casque) {
let rawCasque = fieldAsString(casque, 'effetarmure', '');
if (rawCasque) raw += '\n' + rawCasque;
}
}
let labelArme = ficheAttributeAsInt(perso, 'maindroite', 0);
let labelGauche = ficheAttribute(perso, 'maingauche', '0');
if (typeof labelGauche == 'string' && labelGauche.startsWith('b')) {
if (labelGauche != 'b-1') {
armures = armures || listAllArmors(perso);
let labelBouclier = labelGauche.substring(1);
let bouclier = armures[labelBouclier];
if (bouclier) {
let rawBouclier = fieldAsString(bouclier, 'effetarmure', '');
if (rawBouclier) raw += '\n' + rawBouclier;
}
}
} else if (labelGauche != '2m') labelArmeGauche = toInt(labelGauche);
let att;
let attaques;
if (labelArme) {
attaques = listAllAttacks({
charId
});
let att = attaques[labelArme];
if (att) {
let rawArme = fieldAsString(att, 'armepredicats', '');
if (rawArme) raw += '\n' + rawArme;
}
}
//Ensuite l'arme gauche
if (labelArmeGauche) {
if (!attaques) {
attaques = attaques || listAllAttacks({
charId
});
att = attaques[labelArmeGauche];
}
if (att) {
let rawArme = fieldAsString(att, 'armepredicats', '');
if (rawArme) raw += '\n' + rawArme;
}
}
pred = predicateOfRaw(raw);
stateCOF.predicats[charId] = pred;
return pred[name];
}
function getLabelArme(perso, cote, estMook) {
if (estMook === undefined) {
estMook = perso.token && perso.token.get('bar1_link') === '';
}
if (estMook) {
if (cote == 'droite') return attributeAsInt(perso, 'maindroite', 0);
let attr = tokenAttribute(perso, 'maingauche');
if (attr.length === 0) return '0';
return attr[0].get('current');
} else {
if (cote == 'droite') return ficheAttributeAsInt(perso, 'maindroite', 0);
return ficheAttribute(perso, 'maingauche', '0');
}
}
function setLabelArme(perso, cote, val, estMook, evt) {
let a = 'main' + cote;
if (estMook) setTokenAttr(perso, a, val, evt);
else setFicheAttr(perso, a, val, evt);
}
//Problème : ça ne peut pas marcher pour les boucliers en main gauche
//car ça utilise lui-même un bouclier...
function getPredicates(perso) {
if (perso.predicates === undefined) {
const estMook = perso.token && perso.token.get('bar1_link') === '';
if (!estMook && stateCOF.predicats[perso.charId]) {
perso.predicates = stateCOF.predicats[perso.charId];
return perso.predicates;
}
let raw = ficheAttribute(perso, 'predicats_script', '');
if (perso.armesEnMain) {
if (perso.arme && perso.arme.predicats)
raw += '\n' + perso.arme.predicats;
if (perso.armeGauche && perso.armeGauche.predicats)
raw += '\n' + perso.armeGauche.predicats;
} else if (perso.arme) { //possible si appelé depuis armesEnMain
if (perso.arme.predicats)
raw += '\n' + perso.arme.predicats;
} else { //il faut chercher les prédicats des armes en main
//On n'appelle pas armesEnMain pour éviter la récursion
//et pour éviter trop de calcul
let labelArmeGauche = 0;
let armures;
let labelArmure = ficheAttribute(perso, 'torseequipe', '0');
if (labelArmure && labelArmure != '-1') {
armures = armures || listAllArmors(perso);
let armure = armures[labelArmure];
if (armure) {
let rawArmure = fieldAsString(armure, 'effetarmure', '');
if (rawArmure) raw += '\n' + rawArmure;
}
}
let labelCasque = ficheAttribute(perso, 'teteequipe', '0');
if (labelCasque && labelCasque != '-1') {
armures = armures || listAllArmors(perso);
let casque = armures[labelCasque];
if (casque) {
let rawCasque = fieldAsString(casque, 'effetarmure', '');
if (rawCasque) raw += '\n' + rawCasque;
}
}
let labelArme = getLabelArme(perso, 'droite', estMook);
let labelGauche = getLabelArme(perso, 'gauche', estMook);
if (typeof labelGauche == 'string' && labelGauche.startsWith('b')) {
if (labelGauche != 'b-1') {
armures = armures || listAllArmors(perso);
let labelBouclier = labelGauche.substring(1);
let bouclier = armures[labelBouclier];
if (bouclier) {
let rawBouclier = fieldAsString(bouclier, 'effetarmure', '');
if (rawBouclier) raw += '\n' + rawBouclier;
}
}
} else if (labelGauche != '2m') labelArmeGauche = toInt(labelGauche);
let attaques;
if (labelArme) {
attaques = listAllAttacks(perso);
let att = attaques[labelArme];
if (att) {
let rawArme = fieldAsString(att, 'armepredicats', '');
if (rawArme) raw += '\n' + rawArme;
}
}
//Ensuite l'arme gauche
if (labelArmeGauche) {
if (!attaques) {
attaques = listAllAttacks(perso);
}
let att = attaques[labelArmeGauche];
if (att) {
let rawArme = fieldAsString(att, 'armepredicats', '');
if (rawArme) raw += '\n' + rawArme;
}
}
}
let pred = predicateOfRaw(raw);
perso.predicates = pred;
if (!estMook) stateCOF.predicats[perso.charId] = pred;
}
return perso.predicates;
}
function predicateAsBool(perso, name) {
let pred = getPredicates(perso);
if (Array.isArray(pred)) pred = pred[0];
return pred[name];
}
function predicateAsInt(perso, name, def, defPresent) {
let pred = getPredicates(perso);
let r = pred[name];
if (r === undefined) return def;
if (defPresent !== undefined) def = defPresent;
if (Array.isArray(r)) r = r[0];
if (r === true) return def;
return toInt(r, def);
}
function predicatesNamed(perso, name) {
let pred = getPredicates(perso);
let r = pred[name];
if (!r) return [];
if (Array.isArray(r)) return r;
return [r];
}
function predicateOrAttributeAsBool(perso, name) {
let p = predicateAsBool(perso, name);
if (p) return p;
return attributeAsBool(perso, name);
}
function error(msg, obj) {
log(msg);
log(obj);
if (msg) {
try {
sendChat('COFantasy', msg);
} catch (e) {
msg = msg.replace('[', '[ ');
sendChat('COFantasy', "Message sans jet : " + msg);
}
}
}
function determineSettingDeJeu() {
let characters = findObjs({
_type: 'character'
});
let charsGenerique = [];
let charsArran = [];
characters.forEach(function(c) {
let typePerso = charAttribute(c.id, 'type_personnage', {
caseInsensitive: true
});
if (typePerso.length > 0 && typePerso[0].get('current') == 'PNJ')
return; //Les fiches de PNJ sont les mêmes
let setting = charAttribute(c.id, 'option_setting', {
caseInsensitive: true
});
if (setting.length === 0) {
charsGenerique.push(c);
return;
}
if (setting[0].get('current') == 'arran') charsArran.push(c);
else charsGenerique.push(c);
});
if (charsArran.length <= charsGenerique.length) {
log("Utilisation des règles COF génériques");
if (charsArran.length > 0) {
error("Attention, des personnages suivent les options de jeu des Terres d'Arran (voir le log pour la liste)", charsArran);
charsArran.forEach(function(c) {
let g = getObj('character', c.id);
if (g) log(g.get('name'));
else log(c);
});
stateCOF.setting_mixte = true;
}
return;
}
log("Utilisation des règles des Terres d'Arran");
if (charsGenerique.length > 0) {
error("Attention, des personnages ne suivent pas les options de jeu des Terres d'Arran (voir le log pour la liste)", charsGenerique);
charsGenerique.forEach(function(c) {
let g = getObj('character', c.id);
if (g) log(g.get('name'));
else log(c);
});
stateCOF.setting_mixte = true;
return;
}
stateCOF.setting_arran = true;
}
function persoArran(perso) {
if (stateCOF.setting_arran) return true;
if (!stateCOF.setting_mixte) return false;
if (perso.arran === undefined) {
perso.arran =
ficheAttribute(perso, 'option_setting', 'generique') == 'arran';
}
return perso.arran;
}
let statusForInitAlly;
let statusForInitEnemy;
function registerMarkerEffet(marker, effet, mEffet, ms) {
let m = markerCatalog[marker];
if (m) {
mEffet.statusMarker = m.tag;
effet_de_marker[m.tag] = effet;
} else if (ms) {
if (effet_de_marker[ms] && effet_de_marker[ms] != effet) {
sendChat('COF', effet_de_marker[ms] + " et " + effet + " ont le même icone");
}
effet_de_marker[ms] = effet;
} else {
log("Marker " + m + " introuvable. Pas de marker pour l'effet " + effet);
}
}
function splitIdName(idn) {
let pos = idn.indexOf(' ');
if (pos < 1 || pos >= idn.length) {
error("idName mal formé", idn);
return;
}
let name = idn.substring(pos + 1);
return {
id: idn.substring(0, pos),
name: name
};
}
//Renvoie le token et le charId. Si l'id ne correspond à rien, cherche si
//on trouve un nom de token, sur la page passée en argument (ou sinon
//sur la page active de la campagne)
function persoOfId(id, name, pageId, allPages) {
let token = getObj('graphic', id);
if (token === undefined) {
if (name === undefined) return undefined;
if (pageId === undefined) {
pageId = Campaign().get('playerpageid');
}
let tokens = findObjs({
_type: 'graphic',
_subtype: 'token',
_pageid: pageId,
name: name
});
if (tokens.length === 0) {
if (allPages) {
let pages = findObjs({
_type: 'page'
});
pages.find(function(p) {
if (p.id == pageId) return false;
if (p.get('archived')) return false;
tokens = findObjs({
_type: 'graphic',
_subtype: 'token',
_pageid: pageId,
name: name
});
return tokens.length > 0;
});
if (tokens.length === 0) return undefined;
} else return undefined;
}
if (tokens.length > 1) {
error("Ambigüité sur le choix d'un token : il y a " +
tokens.length + " tokens nommés " + name, tokens);
}
token = tokens[0];
}
let charId = token.get('represents');
if (charId === '') {
error("le token sélectionné ne représente pas de personnage", token);
return undefined;
}
return {
token: token,
charId: charId
};
}
//Retourne le perso correspondant à un token id suivi du nom de token
//Permet d'avoir une information robuste en cas d'interruption du script
//peuple tokName
function persoOfIdName(idn, pageId, allPages) {
let sp = splitIdName(idn);
if (sp === undefined) return;
let perso = persoOfId(sp.id, sp.name, pageId, allPages);
if (perso === undefined) {
log("Impossible de trouver le personnage correspondant à " + sp.name);
return;
}
perso.tokName = perso.token.get('name');
if (perso.tokName == sp.name) return perso;
log("En cherchant le token " + idn + ", on trouve " + perso.tokName);
log(perso);
return perso;
}
function idName(perso) {
return perso.token.id + ' ' + perso.token.get('name');
}
let roundMarker;
const roundMarkerSpec = {
represents: '',
rotation: 0,
layer: 'map',
name: 'Init marker',
aura1_color: '#ff00ff',
aura2_color: '#00ff00',
imgsrc: DEFAULT_DYNAMIC_INIT_IMG,
shownname: false,
light_hassight: false,
has_bright_light_vision: false,
has_night_vision: false,
is_drawing: true
};
let threadSync = 0;
function removeRoundMarker() {
if (roundMarker) {
roundMarker.remove();
roundMarker = undefined;
stateCOF.roundMarkerId = undefined;
} else {
stateCOF.roundMarkerId = undefined;
let roundMarkers = findObjs({
_type: 'graphic',
represents: '',
name: 'Init marker',
layer: 'map',
});
roundMarkers.forEach(function(rm) {
rm.remove();
});
}
}
function activateRoundMarker(sync, token) {
if (!stateCOF.combat) {
removeRoundMarker();
threadSync = 0;
return;
}
if (sync != threadSync) return;
if (token) {
// Cas spéciaux du cavaliers
let pageId = token.get('pageid');
let personnage = persoOfId(token.id);
let monteSur = tokenAttribute(personnage, 'monteSur');
let estMontePar = tokenAttribute(personnage, 'estMontePar');
let monture;
let cavalier;
if (monteSur.length > 0) {
cavalier = personnage;
monture = persoOfIdName(monteSur[0].get('current'), pageId);
if (monture !== undefined) token = monture.token;
} else if (estMontePar.length > 0) {
monture = personnage;
cavalier = persoOfIdName(estMontePar[0].get('current'), pageId);
}
removeRoundMarker();
roundMarkerSpec._pageid = pageId;
let tokenLayer = token.get('layer');
if (tokenLayer !== 'objects') roundMarkerSpec.layer = tokenLayer;
else roundMarkerSpec.layer = 'map';
roundMarkerSpec.left = token.get('left');
roundMarkerSpec.top = token.get('top');
let width = (token.get('width') + token.get('height')) / 2 * flashyInitMarkerScale;
roundMarkerSpec.width = width;
roundMarkerSpec.height = width;
roundMarkerSpec.imgsrc = stateCOF.options.images.val.image_init.val;
let localImage;
let gmNotes = token.get('gmnotes');
try {
gmNotes = _.unescape(decodeURIComponent(gmNotes)).replace(' ', ' ');
gmNotes = linesOfNote(gmNotes);
gmNotes.find(function(l) {
if (l.startsWith('init_aura:')) {
roundMarkerSpec.imgsrc = l.substring(10).trim();
return true;
}
return false;
});
} catch (uriError) {
log("Erreur de décodage URI dans la note GM de " + token.get('name') + " : " + gmNotes);
}
roundMarker = createObj('graphic', roundMarkerSpec);
if (roundMarker === undefined && localImage) {
error("Image locale de " + token.get('name') + " incorrecte (" + roundMarkerSpec.imgsrc + ")", gmNotes);
roundMarkerSpec.imgsrc = stateCOF.options.images.val.image_init.val;
roundMarker = createObj('graphic', roundMarkerSpec);
}
if (roundMarker === undefined && roundMarkerSpec.imgsrc != DEFAULT_DYNAMIC_INIT_IMG) {
error("Image d'aura d'initiative incorrecte (" + roundMarkerSpec.imgsrc + ")", gmNotes);
roundMarkerSpec.imgsrc = DEFAULT_DYNAMIC_INIT_IMG;
roundMarker = createObj('graphic', roundMarkerSpec);
}
if (roundMarker === undefined) {
error("Impossible de créer le token pour l'aura dynamique", roundMarkerSpec);
return false;
}
stateCOF.roundMarkerId = roundMarker.id;
if (roundMarkerSpec.layer === 'map') toFront(roundMarker);
// Ne pas amener une monture montée en avant pour éviter de cacher le cavalier
if (cavalier && monture) {
toFront(monture.token);
toFront(cavalier.token);
} else {
toFront(token);
}
setTimeout(_.bind(activateRoundMarker, undefined, sync), 200);
} else if (roundMarker) { //rotation
let rotation = roundMarker.get('rotation');
roundMarker.set('rotation', (rotation + 0.5) % 365);
let timeout = 100;
//let page = getObj('page', roundMarker.get('pageid'));
//if (page && (page.get('dynamic_lighting_enabled') || page.get('showlighting'))) timeout = 2000;
setTimeout(_.bind(activateRoundMarker, undefined, sync), timeout);
}
}
function removeTokenFlagAura(token) {
if (stateCOF.options.affichage.val.init_dynamique.val) {
removeRoundMarker();
stateCOF.roundMarkerId = undefined;
return;
}
if (aura_token_on_turn) {
token.set('aura2_radius', '');
token.set('showplayers_aura2', false);
} else {
// Cas des tokens personnalisés
if (statusForInitEnemy && statusForInitAlly) {
token.set(statusForInitAlly, false);
token.set(statusForInitEnemy, false);
} else token.set('status_flying-flag', false);
}
}
function trouveOuCreeCible() {
let persos = findObjs({
_type: 'character',
name: 'Cible',
controlledby: 'all'
});
if (persos.length > 0) return persos[0];
let pages = findObjs({
_type: 'page'
});
if (pages.length > 0) {
let pageId = pages[0].id;
let charCible = createObj('character', {
name: 'Cible',
controlledby: 'all',
inplayerjournals: 'all',
avatar: 'https://s3.amazonaws.com/files.d20.io/images/33041174/5JdDVh-34C-kZglTE1aq-w/max.png?1494837870',
});
if (charCible) {
let attrPV = charAttribute(charCible.id, 'PV', {
caseInsensitive: true
});
if (attrPV.length > 0) attrPV = attrPV[0];
else attrPV = createObj('attribute', {
name: 'PV',
characterid: charCible.id,
current: 0,
max: 0
});
setAttrs(charCible.id, {
type_personnage: 'PNJ'
});
let tokenCible = createObj('graphic', {
name: 'Cible',
layer: 'objects',
_pageid: pageId,
imgsrc: 'https://s3.amazonaws.com/files.d20.io/images/33041174/5JdDVh-34C-kZglTE1aq-w/thumb.png?1494837870',
represents: charCible.id,
width: PIX_PER_UNIT,
height: PIX_PER_UNIT,
bar1_link: attrPV ? attrPV.id : ''
});
if (tokenCible) {
setDefaultTokenForCharacter(charCible, tokenCible);
tokenCible.remove();
}
return charCible;
}
}
}
//Appelé au lancement du script, mise à jour de certaines variables globales
function setStateCOF() {
stateCOF = state.COFantasy;
stateCOF.predicats = {}; //prédicats par charId.
if (stateCOF.roundMarkerId) {
roundMarker = getObj('graphic', stateCOF.roundMarkerId);
if (roundMarker === undefined) {
log("Le marqueur d'init a changé d'id");
let roundMarkers = findObjs({
_type: 'graphic',
represents: '',
name: 'Init marker',
});
if (roundMarkers.length > 0) {
roundMarker = roundMarkers[0];
stateCOF.roundMarkerId = roundMarker.id;
roundMarkers.forEach(function(rm) {
if (rm.id != roundMarker.id) rm.remove();
});
} else {
roundMarker = undefined;
stateCOF.roundMarkerId = undefined;
}
}
}
let combat = stateCOF.combat;
if (combat && combat.pageId) {
let pageCombat = getObj('page', combat.pageId);
if (pageCombat === undefined) {
if (stateCOF.roundMarkerId && roundMarker) {
combat.pageId = roundMarker.get('pageid');
} else {
combat.pageId = Campaign().get('playerpageid');
}
}
}
if (!stateCOF.personnageCibleCree) {
trouveOuCreeCible();
stateCOF.personnageCibleCree = true;
}
//Création des tables par défaut
if (!stateCOF.tablesCrees) {
let allTables = findObjs({
_type: 'rollabletable',
});
gameTables.forEach(function(gameTable) {
let table = allTables.find(function(table) {
return table.get('name') == gameTable.name;
});
if (table === undefined) {
table = createObj('rollabletable', {
name: gameTable.name,
showplayers: gameTable.showplayers,
});
gameTable.items.forEach(function(tableItem) {
tableItem.rollabletableid = table.id;
createObj('tableitem', tableItem);
});
}
});
stateCOF.tablesCrees = true;
}
// Le setting
determineSettingDeJeu();
// Les options de jeu
if (stateCOF.options === undefined) stateCOF.options = {};
copyOptions(stateCOF.options, defaultOptions);
reglesOptionelles = stateCOF.options.regles.val;
// Les macros utiles en jeu
if (stateCOF.options.macros_a_jour.val) {
let macros = findObjs({
_type: 'macro'
});
let players = findObjs({
_type: 'player'
});
let mjs = [];
players.forEach(function(p) {
if (playerIsGM(p.id)) mjs.push(p.id);
});
let inBar = [];
if (stateCOF.gameMacros) {
//Check modified or removed macros
stateCOF.gameMacros.forEach(function(gm) {
let ngm = gameMacros.find(function(ngm) {
return ngm.name == gm.name;
});
if (ngm) {
if (ngm.action == gm.action && ngm.visibleto == gm.visibleto && ngm.istokenaction == gm.istokenaction) return;
macros.forEach(function(m) {
if (m.get('name') != ngm.name) return;
if (ngm.action != gm.action && m.get('action') == gm.action)
m.set('action', ngm.action);
if (ngm.visibleto != gm.visibleto && m.get('visibleto') == gm.visibleto)
m.set('visibleto', ngm.visibleto);
if (ngm.istokenaction != gm.istokenaction && m.get('istokenaction') == gm.istokenaction)
m.set('istokenaction', ngm.istokenaction);
sendChat('COF', '/w GM Macro ' + ngm.name + ' mise à jour.');
});
} else {
ngm = gameMacros.find(function(ngm) {
return ngm.oldName == gm.name;
});
if (ngm) {
macros.forEach(function(m) {
if (m.get('name') != ngm.oldName) return;
if (ngm.action == gm.action && ngm.visibleto == gm.visibleto && ngm.istokenaction == gm.istokenaction) {
sendChat('COF', '/w GM Macro ' + gm.name + ' change de nom et devient ' + ngm.name);
}
m.set('name', ngm.name);
if (ngm.action != gm.action && m.get('action') == gm.action)
m.set('action', ngm.action);
if (ngm.visibleto != gm.visibleto && m.get('visibleto') == gm.visibleto)
m.set('visibleto', ngm.visibleto);
if (ngm.istokenaction != gm.istokenaction && m.get('istokenaction') == gm.istokenaction)
m.set('istokenaction', ngm.istokenaction);
sendChat('COF', '/w GM Macro ' + ngm.name + ' mise à jour.');
});
} else {
macros.forEach(function(m) {
if (m.get('name') != gm.name) return;
if (m.get('action') != gm.action) return;
m.remove();
sendChat('COF', '/w GM Macro ' + gm.name + ' effacée.');
});
}
}
});
//Nouvelles macros
gameMacros.forEach(function(ngm) {
let gm = stateCOF.gameMacros.find(function(gm) {
return ngm.name == gm.name;
});
if (!gm) {
let prev =
macros.find(function(macro) {
return macro.get('name') == ngm.name;
});
if (prev === undefined) {
sendChat('COF', '/w GM Macro ' + ngm.name + ' créée.');
if (ngm.inBar) inBar.push(ngm.name);
mjs.forEach(function(playerId, i) {
if (i === 0 || ngm.visibleto === '') {
ngm.playerid = playerId;
createObj('macro', ngm);
}
});
}
}
});
} else {
//Peut-être la première fois, vérifier les macros
if (stateCOF.macros) {
//ancienne version, et on avait copié les macros
//on enlève juste Escalier, et on remplace par Monter et Descendre
let mesc = macros.find(function(m) {
return m.get('name') == 'Escalier';
});
if (mesc) {
createObj('macro', {
name: 'Monter',
action: "!cof-escalier",
visibleto: '',
istokenaction: true,
inBar: false,
_playerid: mesc.playerid
});
createObj('macro', {
name: 'Descendre',
action: "!cof-escalier bas",
visibleto: '',
istokenaction: true,
inBar: false,
_playerid: mesc.playerid
});
mesc.remove();
}
} else {
gameMacros.forEach(function(m) {
let prev =
macros.find(function(macro) {
return macro.get('name') == m.name;
});
if (prev === undefined) {
sendChat('COF', '/w GM Macro ' + m.name + ' créée.');
if (m.inBar) inBar.push(m.name);
mjs.forEach(function(playerId, i) {
if (i === 0 || m.visibleto === '') {
m.playerid = playerId;
createObj('macro', m);
}
});
}
});
}
}
if (inBar.length > 0) {
sendChat('COF', "/w GM Macros à mettre dans la barre d'action du MJ : " + inBar.join(', '));
}
stateCOF.gameMacros = gameMacros;
}
// Récupération des token Markers attachés à la campagne image, nom, tag, Id
const markers = JSON.parse(Campaign().get('token_markers'));
markers.forEach(function(m) {
markerCatalog[m.name] = m;
});
// Option Markers personnalisés activé
if (stateCOF.options.affichage.val.markers_personnalises.val) {
const cof_states_perso = {
assomme: 'status_cof-assomme',
surpris: 'status_cof-surpris',
renverse: 'status_cof-renverse',
aveugle: 'status_cof-aveugle',
affaibli: 'status_cof-affaibli',
etourdi: 'status_cof-etourdi',
paralyse: 'status_cof-paralyse',
ralenti: 'status_cof-ralenti',
immobilise: 'status_cof-immobilise',
endormi: 'status_cof-endormi',
apeure: 'status_cof-apeure',
invisible: 'status_cof-invisible',
blesse: 'status_cof-blesse',
encombre: 'status_cof-encombre',
penombre: 'status_cof-penombre',
//enseveli: 'status_edge-crack' -> À dessiner
chef: 'status_cof-chef',
};
// On boucle sur la liste des états pour vérifier que les markers sont bien présents !
let markersAbsents = [];
let ancientSet = true;
for (let etat in cof_states_perso) {
let markerName = cof_states_perso[etat].substring(7);
let marker_perso = markerCatalog[markerName];
if (marker_perso) {
cof_states[etat] = 'status_' + marker_perso.tag;
ancientSet = false;
} else {
markersAbsents.push(markerName);
}
}
// Cas particulier des deux markers d'initiative
if (markerCatalog['cof-init-ally']) {
statusForInitAlly = 'status_' + markerCatalog['cof-init-ally'].tag;
} else {
markersAbsents.push('cof-init-ally');
}
if (markerCatalog['cof-init-enemy']) {
statusForInitEnemy = 'status_' + markerCatalog['cof-init-enemy'].tag;
} else {
markersAbsents.push('cof-init-enemy');
}
// Cas des markers d'effet temporaire, 3 cas particuliers :
// uniquement le tag sans "status_" devant
for (let effet in messageEffetTemp) {
let m = messageEffetTemp[effet];
let ms = m.statusMarker;
switch (effet) {
case 'asphyxie':
registerMarkerEffet('cof-asphyxie', effet, m, ms);
break;
case 'saignementsSang':
registerMarkerEffet('cof-saigne', effet, m, ms);
break;
case 'prisonVegetale':
registerMarkerEffet('cof-prison-vegetale', effet, m, ms);
break;
default:
if (ms) {
if (effet_de_marker[ms] && effet_de_marker[ms] != effet) {
sendChat('COF', effet_de_marker[ms] + " et " + effet + " ont le même icone");
}
effet_de_marker[ms] = effet;
}
}
}
for (let effet in messageEffetCombat) {
let m = messageEffetCombat[effet];
let ms = m.statusMarker;
switch (effet) {
case 'enflamme':
registerMarkerEffet('cof-flamme', effet, m, ms);
break;
default:
if (ms) {
if (effet_de_marker[ms] && effet_de_marker[ms] != effet) {
sendChat('COF', effet_de_marker[ms] + " et " + effet + " ont le même icone");
}
effet_de_marker[ms] = effet;
}
}
}
if (!ancientSet) {
markersAbsents.forEach(function(m) {
log("Marker " + m + " introuvable");
});
log("Markers personnalisés activés.");
} else {
log("Utilisation des markers par défaut");
}
}
//Construction de la table markers => etat
for (let etat in cof_states) {
let marker = cof_states[etat].substring(7);
etat_de_marker[marker] = etat;
}
stateCOF.jetsEnCours = undefined;
}
function etatRendInactif(etat) {
let res =
etat == 'mort' || etat == 'surpris' || etat == 'assomme' ||
etat == 'etourdi' || etat == 'paralyse' || etat == 'endormi' ||
etat == 'apeure';
return res;
}
// retourne un tableau contenant la liste des ID de joueurs connectés controllant le personnage lié au Token
function getPlayerIds(perso) {
let character = getObj('character', perso.charId);
if (character === undefined) return;
let charControlledby = character.get('controlledby');
if (charControlledby === '') return [];
let playerIds = [];
charControlledby.split(',').forEach(function(controlledby) {
let player = getObj('player', controlledby);
if (player === undefined) return;
if (player.get('online')) playerIds.push(controlledby);
});
return playerIds;
}
function characterPageIds(charId) {
let character = getObj('character', charId);
if (character === undefined) return;
let charControlledBy = character.get('controlledby');
let playersIds = new Set();
if (charControlledBy !== '') {
charControlledBy.split(",").forEach(function(controlledby) {
if (controlledby == 'all') {
let players = findObjs({
_type: 'player'
});
players.forEach(function(p) {
if (p.get('online')) playersIds.add(p.id);
});
}
let player = getObj('player', controlledby);
if (player === undefined) return;
if (player.get('online')) playersIds.add(controlledby);
});
}
if (playersIds.size === 0) {
findObjs({
_type: 'player'
}).forEach(function(p) {
if (playerIsGM(p.id)) playersIds.add(p.id);
});
}
let res = new Set();
playersIds.forEach(function(pid) {
res.add(getPageId(pid));
});
return res;
}
//PNJ au sens de la fiche utilisée, pas forcément en jeu
//perso peut ne pas avoir de token
function persoEstPNJ(perso) {
if (perso.pnj) return true;
else if (perso.pj) return false;
const typePerso = ficheAttribute(perso, 'type_personnage', 'PJ');
perso.pnj = (typePerso == 'PNJ');
perso.pj = !perso.pnj;
return perso.pnj;
}
function getAttackName(attackLabel, perso) {
const attaques = listAllAttacks(perso);
const arme = attaques[attackLabel];
if (arme === undefined) return;
return arme.armenom;
}
function getState(personnage, etat) {
let token = personnage.token;
let charId = personnage.charId;
let res = false;
let attrInvisible = tokenAttribute(personnage, 'tokenInvisible');
if (attrInvisible.length > 0 && token.id == attrInvisible[0].get('max')) {
let tokenInvisible = getObj('graphic', attrInvisible[0].get('current'));
if (tokenInvisible) token = tokenInvisible;
}
if (token !== undefined) {
res = token.get(cof_states[etat]);
if (token.get('bar1_link') === '') return res;
// else, look for the character value, if any
if (charId === undefined) charId = token.get('represents');
personnage.charId = charId;
}
if (charId === '') {
error("token with a linked bar1 but representing no character", token);
return false;
}
if (etat == 'affaibli') { //special case due to new character sheet
let de = ficheAttributeAsInt(personnage, 'ETATDE', 20);
if (de === 20) {
if (res && token !== undefined) token.set(cof_states[etat], false);
return false;
} else if (de === 12) {
if (!res && token !== undefined) token.set(cof_states[etat], true);
return true;
}
}
let attr = findObjs({
_type: 'attribute',
_characterid: charId,
name: etat
});
if (attr.length === 0) {
if (res && token !== undefined) token.set(cof_states[etat], false);
return false;
}
if (!res && token !== undefined) token.set(cof_states[etat], true);
return true;
}
//Met le champ field à value du token dans evt, pour permettre le undo
//Retourne evt.affectes[token.id]
function affectToken(token, field, value, evt) {
evt.affectes = evt.affectes || {};
let aff = evt.affectes[token.id];
if (aff === undefined) {
aff = {
affecte: token,
prev: {}
};
evt.affectes[token.id] = aff;
}
if (aff.prev[field] === undefined) aff.prev[field] = value;
return aff;
}
function estAffaibli(perso) {
if (perso.affaibli !== undefined) return perso.affaibli;
if (getState(perso, 'affaibli') || getState(perso, 'blesse') ||
attributeAsBool(perso, 'poisonAffaiblissant') ||
attributeAsBool(perso, 'poisonAffaiblissantLong')) {
perso.affaibli = true;
return true;
}
perso.affaibli = false;
return false;
}
function setToken(token, field, newValue, evt) {
let prevValue = token.get(field);
affectToken(token, field, prevValue, evt);
token.set(field, newValue);
}
function isActive(perso) {
let inactif =
getState(perso, 'mort') || getState(perso, 'surpris') ||
getState(perso, 'assomme') || getState(perso, 'etourdi') ||
getState(perso, 'paralyse') || getState(perso, 'endormi') ||
getState(perso, 'apeure') || attributeAsBool(perso, 'statueDeBois') ||
attributeAsBool(perso, 'souffleDeMort') || attributeAsBool(perso, 'petrifie');
return !inactif;
}
function sendChar(charId, msg, checkAlias) {
let dest = '';
if (charId) {
if (checkAlias) {
let attrDisplay = charAttribute(charId, 'displayname', {
caseInsensitive: true
});
if (attrDisplay.length > 0 && attrDisplay[0].get('current') == '@{alias}') {
let attrAlias = charAttribute(charId, 'alias', {
caseInsensitive: true
});
if (attrAlias.length > 0) {
msg = attrAlias[0].get('current') + ' ' + msg;
} else {
dest = 'character|' + charId;
}
} else {
dest = 'character|' + charId;
}
} else {
dest = 'character|' + charId;
}
}
sendChat(dest, msg);
}
//Chuchote le message à tous les joueurs présents qui controllent le
//personnage, plus le MJ
function whisperChar(charId, msg) {
let character = getObj('character', charId);
if (character) {
let controlled = character.get('controlledby');
if (controlled.includes('all')) sendChar(charId, msg);
else {
controlled.split(',').forEach(function(c) {
if (c !== '' && !playerIsGM(c)) {
let p = getObj('player', c);
if (p && p.get('online')) {
sendChar(charId, '/w "' + p.get('_displayname') + '" ' + msg);
}
}
});
sendChar(charId, "/w GM " + msg);
}
} else {
sendChar(charId, "/w GM " + msg);
}
}
function nomPerso(perso) {
if (perso.tokName) return perso.tokName;
if (perso.token) {
perso.tokName = perso.token.get('name');
return perso.tokName;
}
if (perso.alias === undefined) {
perso.alias =
ficheAttribute(perso, 'displayname', '@{character_name}') == '@{alias}';
}
if (perso.alias) perso.tokName = ficheAttribute(perso, 'alias', 'inconnu');
else perso.tokName = ficheAttribute(perso, 'character_name', 'inconnu');
return perso.tokName;
}
//perso peut ne pas avoir de token
function sendPerso(perso, msg, secret) {
if (perso.token && perso.token.get('bar1_link') === '') {
msg = perso.token.get('name') + ' ' + msg;
if (secret) {
let character = getObj('character', perso.charId);
if (character) {
let controlled = character.get('controlledby');
if (controlled.includes('all')) sendChat('', msg);
else {
controlled.split(',').forEach(function(c) {
if (c !== '' && !playerIsGM(c)) {
let p = getObj('player', c);
if (p && p.get('online')) {
sendChat('', '/w "' + p.get('_displayname') + '" ' + msg);
}
}
});
sendChat('', "/w GM " + msg);
}
} else sendChat('', msg);
} else sendChat('', msg);
} else {
if (secret) whisperChar(perso.charId, msg);
else {
if (perso.alias === undefined) {
perso.alias =
ficheAttribute(perso, 'displayname', '@{character_name}') == '@{alias}';
}
if (perso.alias) sendChat('', nomPerso(perso) + ' ' + msg);
else sendChar(perso.charId, msg);
}
}
}
const couleurType = {
'normal': {
background: '#F1E6DA',
color: '#000'
},
'magique': {
background: '#FFFFFF',
color: '#534200'
},
'maladie': { //Pour l'instant, comme normal
background: '#F1E6DA',
color: '#000'
},
'feu': {
background: '#FF3011',
color: '#440000'
},
'froid': {
background: '#77FFFF',
color: '#004444'
},
'acide': {
background: '#80BF40',
color: '#020401'
},
'sonique': {
background: '#E6CCFF',
color: '#001144'
},
'electrique': {
background: '#FFFF80',
color: '#222200'
},
'poison': {
background: '#5A752F',
color: '#DDDDAA'
},
'argent': {
background: '#F1E6DA',
color: '#C0C0C0'
},
'drain': {
background: '#0D1201',
color: '#E8F5C9'
},
'energie': {
background: '#FFEEAA',
color: '#221100'
},
};
// options: bonus:int, deExplosif:bool, nbDes:int, type, maxResult
// resultatDesSeuls (rempli par la fonction si true)
//Renvoie 1dk + bonus, avec le texte
//champs val et roll
//de peut être un nombre > 0 ou bien le résultat de parseDice
function rollDePlus(de, options) {
options = options || {};
options.nbDes = options.nbDes || 1;
if (de.dice !== undefined) {
if (!de.nbDe) {
return {
val: de.bonus,
roll: '' + de.bonus
};
}
options.nbDes = options.nbDes || de.nbDe;
options.bonus = options.bonus || de.bonus;
de = de.dice;
}
let count = options.nbDes;
let bonus = options.bonus || 0;
let explose = options.deExplosif || false;
let texteJetDeTotal = '';
let jetTotal = 0;
do {
let jetDe = randomInteger(de);
if (options.maxResult) jetDe = de;
texteJetDeTotal += jetDe;
jetTotal += jetDe;
explose = explose && (jetDe === de);
if (explose) {
texteJetDeTotal += ',';
} else {
count--;
if (count > 0) {
texteJetDeTotal += ',';
}
}
} while ((explose || count > 0) && jetTotal < 1000);
if (options.resultatDesSeuls) options.resultatDesSeuls = jetTotal;
let res = {
val: jetTotal + bonus
};
let style = 'display: inline-block; border-radius: 5px; padding: 0 4px;';
let type = options.type || 'normal';
let couleurs = couleurType[type];
if (couleurs === undefined) couleurs = couleurType.normal;
style += ' background-color: ' + couleurs.background + ';';
style += ' color: ' + couleurs.color + ';';
let msg = '';
msg += res.val + "";
res.roll = msg;
return res;
}
//Si evt est défini, alors on considère qu'il faut y mettre la valeur actuelle
function updateCurrentBar(perso, barNumber, val, evt, maxVal) {
let token = perso.token;
let prevToken;
let HTdeclared;
try {
HTdeclared = HealthColors;
} catch (e) {
if (e.name != "ReferenceError") throw (e);
}
if (HTdeclared) {
//Pour pouvoir annuler les effets de HealthColor sur le statut
affectToken(token, 'statusmarkers', token.get('statusmarkers'), evt);
prevToken = JSON.parse(JSON.stringify(token));
}
let fieldv = 'bar' + barNumber + '_value';
let fieldm;
if (maxVal) fieldm = 'bar' + barNumber + '_max';
let attrId = token.get('bar' + barNumber + '_link');
let attr;
if (attrId !== '') attr = getObj('attribute', attrId);
if (attr === undefined) {
let prevVal = token.get(fieldv);
if (evt) affectToken(token, fieldv, prevVal, evt);
token.set(fieldv, val);
if (maxVal) {
if (evt) affectToken(token, fieldm, token.get(fieldm), evt);
token.set(fieldm, val);
}
if (HTdeclared) HealthColors.Update(token, prevToken);
return;
}
if (evt) {
evt.attributes = evt.attributes || [];
evt.attributes.push({
attribute: attr,
current: attr.get('current'),
max: attr.get('max'),
});
}
token.set(fieldv, val); //On le fait aussi pour forcer la mise à jour de la barre
let aset = {
current: val,
withWorker: true
};
if (maxVal) aset.max = maxVal;
attr.setWithWorker(aset);
if (HTdeclared) HealthColors.Update(token, prevToken);
//Gestion du lien des PVs entre familier et son maître
if (barNumber == 1) {
let nomPersoLie = predicateAsBool(perso, 'PVPartagesAvec');
if (nomPersoLie) {
let charLie = findObjs({
_type: 'character',
name: nomPersoLie
});
if (charLie.length > 0) {
let attrLie = charAttribute(charLie[0].id, 'pv', {
caseInsensitive: true
});
if (attrLie.length > 0) {
attrLie = attrLie[0];
if (evt) {
evt.attributes.push({
attribute: attrLie,
current: attrLie.get('current'),
max: attrLie.get('max')
});
}
attrLie.setWithWorker(aset);
if (val < 1 && evt) { //Il faut aussi faire mourir l'autre perso
let pageId = token.get('pageid');
let charIdLie = charLie[0].id;
let tokensLies = findObjs({
_type: 'graphic',
_subtype: 'token',
_pageid: pageId,
represents: charIdLie
});
if (tokensLies.length === 0) {
tokensLies = findObjs({
_type: 'graphic',
_subtype: 'token',
represents: charIdLie
});
}
if (tokensLies.length > 0) {
let persoLie = {
charId: charIdLie,
token: tokensLies[0]
};
mort(persoLie, undefined, evt);
}
}
}
}
}
}
}
//retourne un entier
function getIntValeurOfEffet(perso, effet, def, predDef) {
let attrsVal = tokenAttribute(perso, effet + 'Valeur');
if (attrsVal.length === 0) {
if (predDef) return predicateAsInt(perso, predDef, def);
return def;
}
return toInt(attrsVal[0].get('current'), def);
}
function forceLightingRefresh(pageId) {
let page = getObj('page', pageId);
if (!page) return;
page.set('force_lighting_refresh', true);
}
function getTokenFields(token, pageId, charId) {
return {
_pageid: pageId || token.get('pageid'),
imgsrc: token.get('imgsrc'),
represents: charId || token.get('represents'),
left: token.get('left'),
top: token.get('top'),
width: token.get('width'),
height: token.get('height'),
rotation: token.get('rotation'),
layer: token.get('layer'),
flipv: token.get('flipv'),
fliph: token.get('fliph'),
name: token.get('name'),
tooltip: token.get('tooltip'),
show_tooltip: token.get('show_tooltip'),
controlledby: token.get('controlledby'),
bar1_link: token.get('bar1_link'),
bar2_link: token.get('bar2_link'),
bar3_link: token.get('bar3_link'),
bar1_value: token.get('bar1_value'),
bar2_value: token.get('bar2_value'),
bar3_value: token.get('bar3_value'),
bar1_max: token.get('bar1_max'),
bar2_max: token.get('bar2_max'),
bar3_max: token.get('bar3_max'),
bar_location: token.get('bar_location'),
compact_bar: token.get('compact_bar'),
aura1_radius: token.get('aura1_radius'),
aura2_radius: token.get('aura2_radius'),
aura1_color: token.get('aura1_color'),
aura2_color: token.get('aura2_color'),
aura1_square: token.get('aura1_square'),
aura2_square: token.get('aura2_square'),
tint_color: token.get('tint_color'),
statusmarkers: token.get('statusmarkers'),
showname: token.get('showname'),
showplayers_name: token.get('showplayers_name'),
showplayers_bar1: token.get('showplayers_bar1'),
showplayers_bar2: token.get('showplayers_bar2'),
showplayers_bar3: token.get('showplayers_bar3'),
showplayers_aura1: token.get('showplayers_aura1'),
showplayers_aura2: token.get('showplayers_aura2'),
playersedit_name: token.get('playersedit_name'),
playersedit_bar1: token.get('playersedit_bar1'),
playersedit_bar2: token.get('playersedit_bar2'),
playersedit_bar3: token.get('playersedit_bar3'),
playersedit_aura1: token.get('playersedit_aura1'),
playersedit_aura2: token.get('playersedit_aura2'),
lastmove: token.get('lastmove'),
sides: token.get('sides'),
currentSide: token.get('currentSide'),
lockMovement: token.get('lockMovement'),
/* Dynamic Lighting */
has_bright_light_vision: token.get('has_bright_light_vision'),
has_night_vision: token.get('has_night_vision'),
night_vision_distance: token.get('night_vision_distance'),
emits_bright_light: token.get('emits_bright_light'),
bright_light_distance: token.get('bright_light_distance'),
emits_low_light: token.get('emits_low_light'),
low_light_distance: token.get('low_light_distance'),
light_sensitivity_multiplier: token.get('light_sensitivity_multiplier'),
night_vision_effect: token.get('night_vision_effect'),
has_limit_field_of_vision: token.get('has_limit_field_of_vision'),
limit_field_of_vision_center: token.get('limit_field_of_vision_center'),
limit_field_of_vision_total: token.get('limit_field_of_vision_total'),
has_limit_field_of_night_vision: token.get('has_limit_field_of_night_vision'),
limit_field_of_night_vision_center: token.get('limit_field_of_night_vision_center'),
limit_field_of_night_vision_total: token.get('limit_field_of_night_vision_total'),
has_directional_bright_light: token.get('has_directional_bright_light'),
directional_bright_light_center: token.get('directional_bright_light_center'),
directional_bright_light_total: token.get('directional_bright_light_total'),
has_directional_dim_light: token.get('has_directional_dim_light'),
directional_dim_light_center: token.get('directional_dim_light_center'),
directional_dim_light_total: token.get('directional_dim_light_total'),
light_color: token.get('light_color'),
/* Legacy Dynamic Lighting */
light_radius: token.get('light_radius'),
light_dimradius: token.get('light_dimradius'),
light_otherplayers: token.get('light_otherplayers'),
light_hassight: token.get('light_hassight'),
light_angle: token.get('light_angle'),
light_losangle: token.get('light_losangle'),
light_multiplier: token.get('light_multiplier'),
adv_fow_view_distance: token.get('adv_fow_view_distance'),
gmnotes: token.get('gmnotes'),
};
}
function caracOfMod(m, accent) {
switch (m) {
case 'FOR':
return 'force';
case 'DEX':
if (accent) return 'dextérité';
return 'dexterite';
case 'CON':
return 'constitution';
case 'INT':
return 'intelligence';
case 'SAG':
return 'sagesse';
case 'CHA':
return 'charisme';
default:
return;
}
}
//Retourne le mod de la caractéristque entière.
//si carac n'est pas une carac, retourne 0
//perso peut ne pas avoir de token ou être juste un charId
function modCarac(perso, carac) {
if (perso.charId === undefined) perso = {
charId: perso
};
let mod = 0;
if (persoEstPNJ(perso)) {
switch (carac) {
case 'force':
case 'FORCE':
mod = ficheAttributeAsInt(perso, 'pnj_for', 0);
break;
case 'dexterite':
case 'DEXTERITE':
mod = ficheAttributeAsInt(perso, 'pnj_dex', 0);
break;
case 'constitution':
case 'CONSTITUTION':
mod = ficheAttributeAsInt(perso, 'pnj_con', 0);
break;
case 'intelligence':
case 'INTELLIGENCE':
mod = ficheAttributeAsInt(perso, 'pnj_int', 0);
break;
case 'sagesse':
case 'SAGESSE':
mod = ficheAttributeAsInt(perso, 'pnj_sag', 0);
break;
case 'charisme':
case 'CHARISME':
mod = ficheAttributeAsInt(perso, 'pnj_cha', 0);
break;
default:
return 0;
}
mod -= Math.floor(attributeAsInt(perso, 'affaiblissementde' + carac, 0) / 2);
} else {
// On a une fiche de PJ
let valCarac =
ficheAttributeAsInt(perso, carac, 10) - attributeAsInt(perso, 'affaiblissementde' + carac, 0);
mod = Math.floor((valCarac - 10) / 2);
}
if (carac == 'force' || carac == 'FORCE') {
if (attributeAsBool(perso, 'mutationMusclesHypertrophies')) mod += 2;
if (attributeAsBool(perso, 'grandeTaille')) mod += 2;
if (attributeAsBool(perso, 'lycanthropie')) mod += 1;
} else if ((carac == 'DEXTERITE' || carac == 'dexterite') && attributeAsBool(perso, 'mutationSilhouetteFiliforme')) mod += 4;
return mod;
}
//Renvoie stateCOF.combat, garanti non false
function initPerso(personnage, evt, recompute) {
return initiative([{
_id: personnage.token.id
}], evt, recompute);
}
//perso peut ne pas avoir de token
// si strict, on retourne undefined s'il n'existe pas d'attaque de ce label
function getWeaponStats(perso, attackLabel, strict) {
let weaponStats = {
name: 'Attaque',
attSkill: '@{ATKCAC}',
attNbDices: 1,
attDice: 4,
attDMBonusCommun: 0,
crit: 20,
divers: '',
portee: 0,
typeDegats: 'contondant',
options: '',
};
if (attackLabel === undefined) {
if (strict) return;
return weaponStats;
}
let attaques = listAllAttacks(perso);
let att = attaques[attackLabel];
if (att === undefined) {
if (strict) return;
weaponStats.name = attackLabel;
return weaponStats;
}
return weaponStatsOfAttack(perso, attackLabel, att);
}
function purgeCachePredicats(perso) {
const estMook = perso.token && perso.token.get('bar1_link') === '';
if (!estMook) stateCOF.predicats[perso.charId] = undefined;
perso.predicates = undefined;
}
function purgeCacheArme(perso) {
perso.armesEnMain = undefined; //il faut enlever le cache sur l'arme en main
perso.arme = undefined;
purgeCachePredicats(perso);
}
//renvoie le nom de l'arme si l'arme est déjà tenue en main
// options.seulementDroite permet de ne rengainer que l'arme droite ou de forcer à porter une arme gauche en main droite
// options.gauche permet de rengainer ou porter l'arme en main gauche
// options.deuxMains permet de prendre une arme à 2 mains
// options.armeGaucheLabel permet de dégainer à la fois labelArme en main principale et ce label en arme gauche. On doit pouvoir l'abuser pour dégainer d'un coté et rengainer de l'autre.
// Ces 4 options sont mutuellement exclusives
// options.weaponStats permet de donner les stats de l'arme. On ignore alors l'argument labelArme
function degainerArme(perso, labelArme, evt, options = {}) {
if (options.gauche && options.seulementDroite) {
error("Dégainer arme aves les options gauche et droite", options);
return;
}
let nouvelleArme; //Les stats de la nouvelle arme. Si on a 2 armes, c'est l'arme principale
let nouvelleArmeGauche; //Les stats de la nouvelle arme gauche si on a 2 armes
let labelArmeGauche; //défini seulement si on dégaine l'arme gauche
// et toujours différent de labelArme
let rengainerArmePrincipale = false;
let rengainerArmeGauche = false;
if (options.weaponStats) {
nouvelleArme = options.weaponStats;
labelArme = nouvelleArme.label;
} else if (labelArme && labelArme !== '')
nouvelleArme = getWeaponStats(perso, labelArme, true);
if (options.armeGaucheLabel) { //On dégaine 2 armes
if (options.armeGaucheLabel == labelArme) {
sendPerso("ne peut dégainer la même arme dans les deux mains");
return;
}
let arme = getWeaponStats(perso, options.armeGaucheLabel, true);
//Possible qu'on ait mis une arme gauche en premier et une arme droite en second
if (arme && nouvelleArme.armeGauche && !arme.armeGauche) {
nouvelleArmeGauche = nouvelleArme;
labelArmeGauche = labelArme;
labelArme = options.armeGaucheLabel;
nouvelleArme = arme;
} else {
labelArmeGauche = options.armeGaucheLabel;
nouvelleArmeGauche = arme;
}
rengainerArmePrincipale = true;
rengainerArmeGauche = true;
} else if (options.gauche) {
rengainerArmeGauche = true;
} else if (options.seulementDroite) {
rengainerArmePrincipale = true;
} else if (options.deuxMains) {
rengainerArmePrincipale = true;
rengainerArmeGauche = true;
} else {
//On peut décider en fonction du type de l'arme
if (nouvelleArme) {
if (nouvelleArme.armeGauche) {
options.gauche = true;
rengainerArmeGauche = true;
} else if (nouvelleArme.deuxMains) {
options.deuxMains = true;
rengainerArmePrincipale = true;
rengainerArmeGauche = true;
} else {
rengainerArmePrincipale = true;
}
} else {
rengainerArmePrincipale = true;
rengainerArmeGauche = true;
}
}
// On regarde ce qu'on déjà a en main
let labelArmeActuelle;
let labelArmeActuelleGauche = '';
let ancienneArme;
let message = nomPerso(perso) + " ";
let envoieMessage = function(m) {
if (options.messages) message += m;
else sendPerso(perso, m, options.secret);
};
let changementDePrise; //si vrai, alors rengainerArmePrincipale == false
const estMook = perso.token && perso.token.get('bar1_link') === '';
labelArmeActuelle = getLabelArme(perso, 'droite', estMook);
let labelGauche = getLabelArme(perso, 'gauche', estMook);
let armeActuelleTenueADeuxMains;
let tientUnBouclier;
if (labelGauche == '2m') {
//On tient l'arme actuelle à 2 mains.
rengainerArmePrincipale = rengainerArmePrincipale || rengainerArmeGauche;
armeActuelleTenueADeuxMains = true;
} else if (typeof labelGauche == 'string' && labelGauche.startsWith('b')) {
//On a un bouclier en main gauche
tientUnBouclier = true;
} else {
labelArmeActuelleGauche = parseInt(labelGauche);
if (isNaN(labelArmeActuelleGauche)) labelArmeActuelleGauche = 0;
}
let rienAFaire;
if (options.deuxMains) {
if (labelArmeActuelle == labelArme) {
rengainerArmePrincipale = false;
if (armeActuelleTenueADeuxMains) rienAFaire = true;
else {
changementDePrise = true;
message += "prend son arme à deux mains";
rengainerArmeGauche = false;
}
}
} else if (options.gauche) {
rienAFaire = labelArmeActuelleGauche == labelArme;
} else if (options.seulementDroite) {
rienAFaire = labelArmeActuelle == labelArme;
} else { //soit 2 armes, soit pas précisé
if (labelArmeActuelle == labelArme) {
if (armeActuelleTenueADeuxMains) {
rengainerArmePrincipale = false;
changementDePrise = true;
message += "prend son arme à une main";
} else {
if (labelArmeGauche) //on dégaine 2 armes
rienAFaire = labelArmeActuelleGauche == labelArmeGauche;
else {
//Soit on ne dégaine que la même arme, soit c'est un ordre de rengainer (labelArme === undefined)
rienAFaire = labelArme || !labelArmeActuelleGauche;
}
}
}
}
if (rienAFaire) {
//Pas besoin de dégainer ni de rengainer
if (options.weaponStats) return options.weaponStats.name;
if (nouvelleArme) return nouvelleArme.name;
return;
}
//Messages quand on rengaine des armes, et fin des lumières
if (labelArmeActuelle) {
if (rengainerArmePrincipale) {
ancienneArme = getWeaponStats(perso, labelArmeActuelle);
if (ancienneArme === undefined) {
error("Impossible de trouver l'arme en main", labelArmeActuelle);
return;
}
if (attributeAsBool(perso, 'forgeron(' + labelArmeActuelle + ')')) {
finDEffetDeNom(perso, 'forgeron(' + labelArmeActuelle + ')', evt);
}
let m = "rengaine " + ancienneArme.name;
if (nouvelleArme || (labelArmeActuelleGauche && labelArmeActuelleGauche != labelArmeActuelle)) {
m += ' et ';
}
envoieMessage(m);
if (bonusPlusViteQueSonOmbre(perso, ancienneArme))
updateNextInit(perso);
if (ancienneArme.eclaire) {
let pageId = perso.token.get('pageid');
eteindreUneLumiere(perso, pageId, undefined, 'eclaire_' + labelArmeActuelle, evt);
}
}
} else { //pas d'arme principale en main
rengainerArmePrincipale = false;
}
if (labelArmeActuelleGauche) {
if (rengainerArmeGauche) {
let ancienneArmeGauche = getWeaponStats(perso, labelArmeActuelleGauche);
if (ancienneArmeGauche === undefined) {
error("Impossible de trouver l'arme en main gauche", labelArmeActuelleGauche);
return;
}
if (attributeAsBool(perso, 'forgeron(' + labelArmeActuelleGauche + ')')) {
finDEffetDeNom(perso, 'forgeron(' + labelArmeActuelleGauche + ')', evt);
}
if (options.messages) {
if (ancienneArme) message += ancienneArmeGauche.name + ", et ";
else message += "rengaine " + ancienneArmeGauche.name + " et ";
} else sendPerso(perso, "rengaine " + ancienneArmeGauche.name, options.secret);
if (ancienneArmeGauche.eclaire) {
let pageId = perso.token.get('pageid');
eteindreUneLumiere(perso, pageId, undefined, 'eclaire_' + labelArmeActuelleGauche, evt);
}
}
} else { //Pas d'arme en main gauche
rengainerArmeGauche = false;
}
let remetBouclier;
//Puis on dégaine
//mais on vérifie que l'arme existe, sinon c'est juste un ordre de rengainer
if (nouvelleArme === undefined) {
if (labelArmeActuelle) {
purgeCacheArme(perso);
if (!options.gauche) setLabelArme(perso, 'droite', 0, estMook, evt);
if (!options.seulementDroite) {
if (!tientUnBouclier && ficheAttributeAsBool(perso, 'defbouclier', false)) {
let bouclier;
if (estMook) bouclier = 'b-1';
else bouclier = 'b' + ficheAttributeMax(perso, 'maingauche', '-1');
sendPerso(perso, "remet son bouclier", options.secret);
setLabelArme(perso, 'auche', bouclier, estMook, evt);
remetBouclier = true;
} else {
setLabelArme(perso, 'gauche', 0, estMook, evt);
}
}
}
return;
}
if (nouvelleArme.deuxMains || options.deuxMains || nouvelleArmeGauche) {
if (tientUnBouclier) {
sendPerso(perso, "enlève son bouclier", options.secret);
}
} else if (changementDePrise ||
(rengainerArmeGauche && !nouvelleArmeGauche) ||
armeActuelleTenueADeuxMains) {
if (!tientUnBouclier && ficheAttributeAsBool(perso, 'defbouclier', false)) {
let bouclier;
if (estMook) bouclier = 'b-1';
else bouclier = 'b' + ficheAttributeMax(perso, 'maingauche', '-1');
sendPerso(perso, "remet son bouclier", options.secret);
setLabelArme(perso, 'gauche', bouclier, estMook, evt);
purgeCacheArme(perso);
remetBouclier = true;
}
}
if (labelArmeActuelle) { //On avait une arme en main
if (options.gauche) {
if (nouvelleArme) setLabelArme(perso, 'gauche', labelArme, estMook, evt);
else setLabelArme(perso, 'gauche', 0, estMook, evt);
if (rengainerArmePrincipale) setLabelArme(perso, 'droite', 0, estMook, evt);
} else {
if (nouvelleArme) setLabelArme(perso, 'droite', labelArme, estMook, evt);
else setLabelArme(perso, 'droite', 0, estMook, evt);
if (labelArmeGauche) setLabelArme(perso, 'gauche', labelArmeGauche, estMook, evt);
else if (nouvelleArme && (nouvelleArme.deuxMains || options.deuxMains))
setLabelArme(perso, 'gauche', '2m', estMook, evt);
else if (!remetBouclier &&
(changementDePrise || (ancienneArme && ancienneArme.deuxMains)))
setLabelArme(perso, 'gauche', '0', estMook, evt);
}
purgeCacheArme(perso);
} else { //On n'avait pas d'arme en main
if (stateCOF.combat && nouvelleArme && nouvelleArme.portee === 0 &&
predicateAsBool(perso, 'frappeDuVide') &&
!attributeAsBool(perso, 'limiteParCombat_dejaFrappeContact')) {
setTokenAttr(perso, 'limiteParTour_frappeDuVidePossible', true, evt);
}
if (options.gauche) {
setLabelArme(perso, 'gauche', labelArme, estMook, evt);
} else {
if (labelArmeGauche)
setLabelArme(perso, 'gauche', labelArmeGauche, estMook, evt);
else if (options.deuxMains || nouvelleArme.deuxMains)
setLabelArme(perso, 'gauche', '2m', estMook, evt);
setLabelArme(perso, 'droite', labelArme, estMook, evt);
}
purgeCacheArme(perso);
}
if (options.messages) {
if (changementDePrise) {
if (nouvelleArmeGauche) message += ", et dégaine " + nouvelleArmeGauche.name;
} else if (nouvelleArme) {
message += "dégaine " + nouvelleArme.name;
if (nouvelleArmeGauche) message += " et " + nouvelleArmeGauche.name;
} else if (nouvelleArmeGauche) message += "dégaine " + nouvelleArmeGauche.name;
options.messages.push(message);
} else {
if (changementDePrise) {
if (nouvelleArmeGauche) message += ", et dégaine " + nouvelleArmeGauche.name;
} else if (nouvelleArme) {
message = "dégaine " + nouvelleArme.name;
if (nouvelleArmeGauche) message += " et " + nouvelleArmeGauche.name;
} else if (nouvelleArmeGauche) message += "dégaine " + nouvelleArmeGauche.name;
sendPerso(perso, message, options.secret);
}
//L'éclairage des nouvelles armes
if (nouvelleArme && !changementDePrise) {
let radius = nouvelleArme.eclaire;
if (radius && radius > 0) {
let dimRadius = nouvelleArme.eclaireFaible;
if (dimRadius === undefined || dimRadius >= radius) dimRadius = '';
ajouteUneLumiere(perso, 'eclaire_' + labelArme, radius, dimRadius, evt);
}
if (bonusPlusViteQueSonOmbre(perso, nouvelleArme)) updateNextInit(perso);
}
if (nouvelleArmeGauche) {
let radius = nouvelleArmeGauche.eclaire;
if (radius && radius > 0) {
let dimRadius = nouvelleArmeGauche.eclaireFaible;
if (dimRadius === undefined || dimRadius >= radius) dimRadius = '';
ajouteUneLumiere(perso, 'eclaire_' + labelArmeGauche, radius, dimRadius, evt);
}
}
}
//options peut contenir
// msg: un message à afficher
// maxVal: la valeur max de l'attribut
// secret: le message n'est pas affiché pour tout le monde.
// charAttr: si présent, on utilise un attribut de personnage
// renvoie l'attribut créé ou mis à jour
function setTokenAttr(personnage, attribute, value, evt, options) {
let charId = personnage.charId;
let token = personnage.token;
let maxval = '';
if (options && options.maxVal !== undefined) maxval = options.maxVal;
if (options && options.msg !== undefined) {
sendPerso(personnage, options.msg, options.secret);
}
evt.attributes = evt.attributes || [];
let fullAttribute = fullAttributeName(personnage, attribute, options);
if (!fullAttribute) {
let args = {
personnage,
attribute,
value,
options
};
let name = 'inconnu';
if (token) name = token.get('name');
error("Création d'un attribut undefined pour " + name, args);
return;
}
let attr = findObjs({
_type: 'attribute',
_characterid: charId,
name: fullAttribute
});
if (attr.length === 0) {
attr = createObj('attribute', {
characterid: charId,
name: fullAttribute,
current: value,
max: maxval
});
evt.attributes.push({
attribute: attr,
});
if (token) {
let pageId = token.get('pageid');
switch (attribute) {
case 'agrandissement':
personnage.taille = undefined;
let arme = armesEnMain(personnage);
if (arme.armeDeGrand) {
let taille = taillePersonnage(personnage, 4);
if (taille == 5) {
arme.deuxMains = false;
sendPerso(personnage, "peut maintenant tenir " + arme.name + " à une main");
degainerArme(personnage, arme.label, evt);
}
}
let width = token.get('width');
let height = token.get('height');
affectToken(token, 'width', width, evt);
affectToken(token, 'height', height, evt);
width += width / 2;
height += height / 2;
token.set('width', width);
token.set('height', height);
break;
case 'formeDArbre':
//On copie les PVs pour pouvoir les restaurer à la fin de l'effet
setTokenAttr(personnage, 'anciensPV', token.get('bar1_value'), evt, {
maxVal: token.get('bar1_max')
});
//On va créer une copie de token, mais avec une image d'arbre
let tokenFields = getTokenFields(token, pageId, personnage.charId);
let tokenArbre;
let imageArbre = predicateAsBool(personnage, 'tokenFormeDArbre');
if (imageArbre) {
tokenFields.imgsrc = imageArbre;
tokenArbre = createObj('graphic', tokenFields);
}
if (tokenArbre === undefined) {
tokenFields.imgsrc = stateCOF.options.images.val.image_arbre.val;
tokenArbre = createObj('graphic', tokenFields);
}
if (tokenArbre) {
evt.tokens = evt.tokens || [];
evt.tokens.push(tokenArbre);
//On met l'ancien token dans le gmlayer, car si l'image vient du marketplace, il est impossible de le recréer depuis l'API
setToken(token, 'layer', 'gmlayer', evt);
setTokenAttr(personnage, 'changementDeToken', true, evt);
replaceInTurnTracker(token.id, tokenArbre.id, evt);
personnage.token = tokenArbre;
token = tokenArbre;
}
//On met maintenant les nouveaux PVs
//selon Kegron http://www.black-book-editions.fr/forums.php?topic_id=4800&tid=245841#msg245841
let niveau = ficheAttributeAsInt(personnage, 'niveau', 1);
let nouveauxPVs = getIntValeurOfEffet(personnage, 'formeDArbre', niveau * 5);
updateCurrentBar(personnage, 1, nouveauxPVs, evt, nouveauxPVs);
//L'initiative change
initPerso(personnage, evt, true);
break;
case 'bloqueManoeuvre':
case 'enveloppePar':
case 'prisonVegetale':
case 'toiles':
case 'statueDeBois':
case 'estGobePar':
case 'agrippeParUnDemon':
case 'etreinteScorpionPar':
case 'estEcrasePar':
case 'petrifie':
case 'paralysieRoublard':
nePlusSuivre(personnage, pageId, evt);
lockToken(personnage, evt);
break;
case 'armeeDesMorts':
affectToken(personnage.token, "aura2_radius", personnage.token.get("aura2_radius"), evt);
affectToken(personnage.token, "aura2_color", personnage.token.get("aura2_color"), evt);
affectToken(personnage.token, "showplayers_aura2", personnage.token.get("showplayers_aura2"), evt);
personnage.token.set("aura2_radius", 20);
personnage.token.set("aura2_color", "#b6d7a8");
personnage.token.set("showplayers_aura2", true);
break;
case 'armeeDesMortsPuissant':
personnage.token.set("aura2_radius", 28);
break;
case 'armeeDesMortsTempeteDeManaIntense':
personnage.token.set("aura2_radius", Math.floor(20 * Math.sqrt(1 + value)));
break;
case 'masqueDuPredateur':
{
//L'initiative change
initPerso(personnage, evt, true);
let lie = personnageAmeLiee(personnage);
if (lie) {
setTokenAttr(lie, 'masqueDuPredateurAmeLiee', value, evt, {
maxVal: options.maxVal
});
let valeur = getIntValeurOfEffet(personnage, 'masqueDuPredateur', modCarac(personnage, 'sagesse'));
valeur = Math.floor(valeur / 2);
if (valeur > 1)
setTokenAttr(lie, 'masqueDuPredateurAmeLieeValeur', valeur, evt);
initPerso(lie, evt, true);
}
}
break;
case 'masqueMortuaire':
{
let lie = personnageAmeLiee(personnage);
if (lie) {
setTokenAttr(lie, 'masqueMortuaireAmeLiee', value, evt, {
maxVal: options.maxVal
});
}
}
break;
case 'peauDEcorce':
{
let lie = personnageAmeLiee(personnage);
if (lie) {
setTokenAttr(lie, 'peauDEcorceAmeLiee', value, evt, {
maxVal: options.maxVal
});
let valeur = getIntValeurOfEffet(personnage, 'peauDEcorce', 1, 'voieDesVegetaux');
valeur = Math.floor(valeur / 2);
if (valeur > 1)
setTokenAttr(lie, 'peauDEcorceAmeLieeValeur', valeur, evt);
}
}
break;
}
}
return attr;
}
attr = attr[0];
evt.attributes.push({
attribute: attr,
current: attr.get('current'),
max: attr.get('max')
});
attr.set('current', value);
if (options && options.maxVal !== undefined) attr.set('max', maxval);
return attr;
}
// evt peut être undefined
// options peut avoir les champs msg et secret
function removeTokenAttr(personnage, attribute, evt, options) {
attribute = fullAttributeName(personnage, attribute, options);
let attr = findObjs({
_type: 'attribute',
_characterid: personnage.charId,
name: attribute
});
if (attr.length === 0) return;
if (options && options.msg !== undefined) {
sendPerso(personnage, options.msg, options.secret);
}
attr = attr[0];
if (evt) {
evt.deletedAttributes = evt.deletedAttributes || [];
evt.deletedAttributes.push(attr);
}
attr.remove();
switch (attribute) {
case 'enveloppePar':
case 'agrippeParUnDemon':
case 'etreinteScorpionPar':
case 'estGobePar':
case 'estEcrasePar':
unlockToken(personnage, evt);
}
}
function removeCharAttr(charId, attribute, evt, msg) {
removeTokenAttr({
charId: charId
}, attribute, evt, {
msg: msg
});
}
//cherche l'attribut attribute de valeur par défaut def
//et lui ajoute la valeur val. Crée l'attribut si besoin
//retourne la nouvelle valeur de l'attribut
function addToAttributeAsInt(perso, attribute, def, val, evt) {
evt.attributes = evt.attributes || [];
let fullAttribute = fullAttributeName(perso, attribute);
let attr = findObjs({
_type: 'attribute',
_characterid: perso.charId,
name: fullAttribute
});
if (attr.length === 0) {
attr = createObj('attribute', {
characterid: perso.charId,
name: fullAttribute,
current: def + val,
});
evt.attributes.push({
attribute: attr,
});
return def + val;
}
attr = attr[0];
let c = parseInt(attr.get('current'));
evt.attributes.push({
attribute: attr,
current: c
});
if (isNaN(c)) c = def;
c += val;
attr.set('current', c);
return c;
}
function persoOfToken(token) {
let charId = token.get('represents');
if (charId === '') {
return undefined;
}
return {
token,
charId
};
}
//options:
//fromTemp si on est en train de supprimer un effet temporaire
//affectToken si on a déjà changé le statusmarkers (on vient donc d'un changement à la main d'un marker
function setState(personnage, etat, value, evt, options) {
let token = personnage.token;
if (value && predicateAsBool(personnage, 'immunite_' + etat)) {
sendPerso(personnage, 'ne peut pas être ' + stringOfEtat(etat, personnage));
return false;
}
options = options || {};
let aff = options.affectToken ||
affectToken(token, 'statusmarkers', token.get('statusmarkers'), evt);
if (stateCOF.combat && value && etatRendInactif(etat) &&
(options.affectToken || isActive(personnage)) &&
(etat != 'surpris' || !compagnonPresent(personnage, 'surveillance'))
) {
removeFromTurnTracker(personnage, evt);
}
if (!options.affectToken) token.set(cof_states[etat], value);
if (token.get('bar1_link') !== '') {
let charId = personnage.charId;
if (charId === '') {
error("token with a linked bar1 but representing no character", token);
return true;
}
if (etat == 'affaibli') { //special case due to new character sheet
let v = 20;
if (value) v = 12;
setFicheAttr(personnage, 'ETATDE', v, evt, {
default: 20
});
} else {
let attrEtat =
findObjs({
_type: 'attribute',
_characterid: charId,
name: etat
});
if (value) {
if (attrEtat.length === 0) {
attrEtat =
createObj('attribute', {
characterid: charId,
name: etat,
current: value
});
evt.attributes = evt.attributes || [];
evt.attributes.push({
attribute: attrEtat,
});
}
} else {
if (attrEtat.length > 0) {
evt.deletedAttributes = evt.deletedAttributes || [];
evt.deletedAttributes.push(attrEtat[0]);
attrEtat[0].remove();
}
}
}
}
if (!value) { //On enlève le save si il y en a un
removeTokenAttr(personnage, etat + 'Save', evt);
removeTokenAttr(personnage, etat + 'SaveParTour', evt);
}
let pageId = token.get('pageid');
if (etat == 'aveugle') {
// On change la vision du token
let page = getObj('page', pageId);
let udl = page && page.get('dynamic_lighting_enabled');
if (udl) {
if (aff.prev.has_limit_field_of_vision === undefined)
aff.prev.has_limit_field_of_vision = token.get('has_limit_field_of_vision');
if (aff.prev.has_limit_field_of_night_vision === undefined)
aff.prev.has_limit_field_of_night_vision = token.get('has_limit_field_of_night_vision');
} else {
if (aff.prev.light_losangle === undefined)
aff.prev.light_losangle = token.get('light_losangle');
}
if (value) {
if (udl) {
token.set('has_limit_field_of_vision', true);
token.set('has_limit_field_of_night_vision', true);
} else {
token.set('light_losangle', 0);
}
//Normalement, ne peut plus suivre personne ?
//Si il peut parce qu'il touche ou tient une corde, réutiliser la macro
//pour suivre
nePlusSuivre(personnage, pageId, evt);
} else {
if (!(options.fromTemp))
removeTokenAttr(personnage, 'aveugleTemp', evt);
if (udl) {
token.set('has_limit_field_of_vision', false);
token.set('has_limit_field_of_night_vision', false);
} else {
token.set('light_losangle', 360);
}
}
} else if (etat == 'invisible') {
let attrInvisible = tokenAttribute(personnage, 'tokenInvisible');
if (value) {
if (attrInvisible.length === 0) {
//On va créer une copie de token, mais avec une image invisible et aura visible seulement de ceux qui contrôlent le token
let tokenFields = getTokenFields(token, pageId, personnage.charId);
tokenFields.layer = 'objects';
tokenFields.aura1_radius = 0;
tokenFields.aura1_color = "#FF9900";
tokenFields.aura1_square = false;
tokenFields.showplayers_aura1 = false;
tokenFields.statusmarkers = '';
tokenFields.showplayers_name = false;
tokenFields.showplayers_bar1 = false;
tokenFields.showplayers_bar2 = false;
tokenFields.showplayers_bar3 = false;
tokenFields.imgsrc = IMG_INVISIBLE;
let tokenInvisible = createObj('graphic', tokenFields);
if (tokenInvisible) {
if (tokenFields.has_bright_light_vision) {
tokenInvisible.set('has_bright_light_vision', true);
forceLightingRefresh(pageId);
}
evt.tokens = evt.tokens || [];
evt.tokens.push(tokenInvisible);
//On met l'ancien token dans le gmlayer, car si l'image vient du marketplace, il est impossible de le recréer depuis l'API
setToken(token, 'layer', 'gmlayer', evt);
setTokenAttr(personnage, 'tokenInvisible', token.id, evt, {
maxVal: tokenInvisible.id
});
let combat = stateCOF.combat;
if (stateCOF.options.affichage.val.init_dynamique.val &&
roundMarker && combat && (
(!stateCOF.chargeFantastique && combat.activeTokenId == token.id) ||
(stateCOF.chargeFantastique && stateCOF.chargeFantastique.activeTokenId == token.id))) {
setToken(roundMarker, 'layer', 'gmlayer', evt);
}
}
}
} else { //On enlève l'état invisible
if (attrInvisible.length > 0) {
let tokenOriginel = getObj('graphic', attrInvisible[0].get('current'));
if (!tokenOriginel) {
if (token.get('layer') == 'gmlayer') tokenOriginel = token;
else {
tokenOriginel =
findObjs({
_type: 'graphic',
_subtype: 'token',
_pageid: token.get('pageid'),
layer: 'gmlayer',
represents: personnage.charId,
name: token.get('name')
});
if (tokenOriginel.length > 0) tokenOriginel = tokenOriginel[0];
else {
error("Impossible de retrouver le token de départ de " + token.get('name') + " quand on enlève l'état invisible", attrInvisible);
tokenOriginel = false;
}
}
}
let tokenCourant = getObj('graphic', attrInvisible[0].get('max'));
if (!tokenCourant) {
if (token.get('layer') == 'objects') tokenCourant = token;
else {
tokenCourant =
findObjs({
_type: 'graphic',
_subtype: 'token',
_pageid: token.get('pageid'),
layer: 'objects',
represents: personnage.charId,
name: token.get('name')
});
if (tokenCourant.length > 0) tokenCourant = tokenCourant[0];
else {
error("Impossible de retrouver le token visible de " + token.get('name') + " quand on enlève l'état invisible", attrInvisible);
tokenCourant = false;
}
}
}
removeTokenAttr(personnage, 'tokenInvisible', evt);
if (!options.fromTemp) {
removeTokenAttr(personnage, 'invisibleTemp', evt);
}
if (tokenOriginel) {
setToken(tokenOriginel, 'layer', 'objects', evt);
if (tokenCourant) {
setToken(tokenOriginel, 'left', tokenCourant.get('left'), evt);
setToken(tokenOriginel, 'top', tokenCourant.get('top'), evt);
setToken(tokenOriginel, 'width', tokenCourant.get('width'), evt);
setToken(tokenOriginel, 'height', tokenCourant.get('height'), evt);
setToken(tokenOriginel, 'rotation', tokenCourant.get('rotation'), evt);
setToken(tokenOriginel, 'flipv', tokenCourant.get('flipv'), evt);
setToken(tokenOriginel, 'fliph', tokenCourant.get('fliph'), evt);
if (tokenCourant.get('bar1_link') === '') {
setToken(tokenOriginel, 'bar1_value', tokenCourant.get('bar1_value'), evt);
}
setToken(tokenOriginel, 'bar2_value', tokenCourant.get('bar2_value'), evt);
setToken(tokenOriginel, 'aura2_radius', tokenCourant.get('aura2_radius'), evt);
setToken(tokenOriginel, 'aura2_color', tokenCourant.get('aura2_color'), evt);
setToken(tokenOriginel, 'aura2_square', tokenCourant.get('aura2_square'), evt);
setToken(tokenOriginel, 'showplayers_aura2', tokenCourant.get('showplayers_aura2'), evt);
setToken(tokenOriginel, 'statusmarkers', tokenCourant.get('statusmarkers'), evt);
setToken(tokenOriginel, 'light_angle', tokenCourant.get('light_angle'), evt);
setToken(tokenOriginel, 'has_limit_field_of_vision', tokenCourant.get('has_limit_field_of_vision'), evt);
setToken(tokenOriginel, 'has_limit_field_of_night_vision', tokenCourant.get('has_limit_field_of_night_vision'), evt);
}
}
if (tokenCourant) deleteTokenWithUndo(tokenCourant, evt);
}
}
} else if (value) {
switch (etat) {
case 'mort':
{
//On s'assure de mettre les PV de la cible à 0 (pour les insta kills sans dommages)
if (token.get('bar1_value') > 0) updateCurrentBar(personnage, 1, 0, evt);
nePlusSuivre(personnage, pageId, evt);
lockToken(personnage, evt);
if (stateCOF.combat) {
setTokenAttr(personnage, 'a0PVDepuis', stateCOF.combat.tour, evt, {
maxVal: stateCOF.combat.init
});
removeTokenAttr(personnage, 'rageDuBerserk', evt);
}
let persoMonte = tokenAttribute(personnage, 'estMontePar');
if (persoMonte.length > 0) {
const cavalier = persoOfIdName(persoMonte[0].get('current'), pageId);
if (cavalier !== undefined) {
removeTokenAttr(cavalier, 'monteSur', evt);
}
removeTokenAttr(personnage, 'estMontePar', evt);
removeTokenAttr(personnage, 'positionSurMonture', evt);
}
//On libère les personnages enveloppés, si il y en a.
let attrEnveloppe = tokenAttribute(personnage, 'enveloppe');
attrEnveloppe.forEach(function(a) {
let cible = persoOfIdName(a.get('current'), pageId);
if (cible) {
let envDM = a.get('max');
if (envDM.startsWith('etreinte')) {
//On a une étreinte, on enlève donc l'état immobilisé
setState(cible, 'immobilise', false, evt);
}
evt.deletedAttributes = evt.deletedAttributes || [];
let attrCible = tokenAttribute(cible, 'enveloppePar');
attrCible.forEach(function(a) {
let cube = persoOfIdName(a.get('current', pageId));
if (cube === undefined) {
evt.deletedAttributes.push(a);
a.remove();
} else if (cube.token.id == personnage.token.id) {
sendPerso(cible, 'se libère de ' + cube.tokName);
toFront(cible.token);
evt.deletedAttributes.push(a);
a.remove();
}
unlockToken(cible, evt);
});
}
evt.deletedAttributes.push(a);
a.remove();
});
//Si le mort est enveloppé, il est relaché
attrEnveloppe = tokenAttribute(personnage, 'enveloppePar');
attrEnveloppe.forEach(function(a) {
let cube = persoOfIdName(a.get('current'), pageId);
if (cube) {
let envDiff = a.get('max');
if (envDiff.startsWith('etreinte')) {
//On a une étreinte, on enlève donc l'état immobilisé
setState(personnage, 'immobilise', false, evt);
}
evt.deletedAttributes = evt.deletedAttributes || [];
let attrCube = tokenAttribute(cube, 'enveloppe');
attrCube.forEach(function(a) {
let cible = persoOfIdName(a.get('current', pageId));
if (cible === undefined) {
evt.deletedAttributes.push(a);
a.remove();
} else if (cible.token.id == personnage.token.id) {
sendPerso(cube, 'relache ' + nomPerso(personnage));
evt.deletedAttributes.push(a);
a.remove();
}
});
}
evt.deletedAttributes.push(a);
a.remove();
});
//On libère les personnages agrippés, si il y en a.
let attrAgrippe = tokenAttribute(personnage, 'agrippe');
attrAgrippe.forEach(function(a) {
let cible = persoOfIdName(a.get('current'), pageId);
if (cible) {
evt.deletedAttributes = evt.deletedAttributes || [];
let attrCible = tokenAttribute(cible, 'estAgrippePar');
attrCible.forEach(function(a) {
let agrippant = persoOfIdName(a.get('current', pageId));
if (agrippant.token.id == personnage.token.id) {
sendPerso(cible, 'se libère de ' + agrippant.tokName);
toFront(cible.token);
if (a.get('max')) setState(cible, 'immobilise', false, evt);
evt.deletedAttributes.push(a);
a.remove();
}
});
removeTokenAttr(cible, 'agrippeParUnDemon', evt);
}
evt.deletedAttributes.push(a);
a.remove();
});
//On libère les personnages dévorés, si il y en a.
let attrDevore = tokenAttribute(personnage, 'devore');
attrDevore.forEach(function(a) {
let cible = persoOfIdName(a.get('current'), pageId);
if (cible) {
evt.deletedAttributes = evt.deletedAttributes || [];
let attrCible = tokenAttribute(cible, 'estDevorePar');
attrCible.forEach(function(a) {
let agrippant = persoOfIdName(a.get('current', pageId));
if (agrippant.token.id == personnage.token.id) {
sendPerso(cible, 'se libère de ' + agrippant.tokName);
toFront(cible.token);
setState(cible, 'immobilise', false, evt);
evt.deletedAttributes.push(a);
a.remove();
}
});
}
evt.deletedAttributes.push(a);
a.remove();
});
//On libère les personnages écrasés, si il y en a.
let attrEcrase = tokenAttribute(personnage, 'ecrase');
attrEcrase.forEach(function(a) {
let cible = persoOfIdName(a.get('current'), pageId);
if (cible) {
evt.deletedAttributes = evt.deletedAttributes || [];
let attrCible = tokenAttribute(cible, 'estEcrasePar');
attrCible.forEach(function(a) {
let agrippant = persoOfIdName(a.get('current', pageId));
if (agrippant.token.id == personnage.token.id) {
sendPerso(cible, 'se libère de ' + agrippant.tokName);
toFront(cible.token);
evt.deletedAttributes.push(a);
a.remove();
}
});
}
evt.deletedAttributes.push(a);
a.remove();
});
//On libère les personnages avalés, si il y en a.
let attrGobe = tokenAttribute(personnage, 'aGobe');
attrGobe.forEach(function(a) {
let cible = persoOfIdName(a.get('current'), pageId);
if (cible) {
evt.deletedAttributes = evt.deletedAttributes || [];
let attrCible = tokenAttribute(cible, 'estGobePar');
attrCible.forEach(function(a) {
let gobant = persoOfIdName(a.get('current', pageId));
if (gobant.token.id == personnage.token.id) {
sendPerso(cible, 'peut enfin sortir du ventre de ' + gobant.tokName);
toFront(cible.token);
evt.deletedAttributes.push(a);
a.remove();
unlockToken(cible, evt);
}
});
}
evt.deletedAttributes.push(a);
a.remove();
});
//On libère les personnages sous étreinte et immolation
let attrEtreinteImmole = tokenAttribute(personnage, 'etreinteImmole');
attrEtreinteImmole.forEach(function(a) {
let cible = persoOfIdName(a.get('current'), pageId);
if (cible) {
evt.deletedAttributes = evt.deletedAttributes || [];
let attrCible = tokenAttribute(cible, 'etreinteImmolePar');
attrCible.forEach(function(a) {
let agrippant = persoOfIdName(a.get('current', pageId));
if (agrippant.token.id == personnage.token.id) {
sendPerso(cible, 'se libère de ' + agrippant.tokName);
toFront(cible.token);
setState(cible, 'immobilise', false, evt);
evt.deletedAttributes.push(a);
a.remove();
}
});
}
evt.deletedAttributes.push(a);
a.remove();
});
//On libère les personnages sous étreinte de scorpion
let attrEtreinteScorpion = tokenAttribute(personnage, 'etreinteScorpionSur');
attrEtreinteScorpion.forEach(function(a) {
let cible = persoOfIdName(a.get('current'), pageId);
if (cible) {
evt.deletedAttributes = evt.deletedAttributes || [];
let attrCible = tokenAttribute(cible, 'etreinteScorpionPar');
let attrRatee = tokenAttribute(cible, 'etreinteScorpionRatee');
attrCible.forEach(function(a) {
let agrippant = persoOfIdName(a.get('current', pageId));
if (agrippant.token.id == personnage.token.id) {
sendPerso(cible, 'se libère de ' + agrippant.tokName);
toFront(cible.token);
evt.deletedAttributes.push(a);
a.remove();
attrRatee.forEach(function(attrR) {
evt.deletedAttributes.push(attrR);
attrR.remove();
});
}
});
}
evt.deletedAttributes.push(a);
a.remove();
});
//On fait l'explosion finale
if (predicateAsBool(personnage, 'explosionFinale')) {
let label = predicateAsBool(personnage, 'explosionFinale');
let character = getObj('character', personnage.charId);
if (character === undefined) {
error("Impossible de trouver le personnage de " + personnage.token.get('name'), personnage);
return true;
}
let msg = {
who: 'GM',
selected: [{
_id: personnage.token.id
}]
};
let pids = character.get('controlledby');
pids = pids.split(',');
if (pids[0] === '' || pids[0] == 'all') {
pids = findObjs({
type: 'player'
}).map(function(p) {
return p.id;
});
}
if (pids.length > 0) {
if (pids.length === 0) {
msg.playerid = pids[0];
} else {
msg.playerid = pids.find(playerIsGM);
if (msg.playerid === undefined) msg.playerid = pids[0];
}
msg.content = '!cof-explosion ' + label;
attaqueExplosion(msg);
}
}
//On termine les effets temporaires liés au personnage
let etlAttr = tokenAttribute(personnage, 'effetsTemporairesLies');
if (etlAttr.length > 0) {
etlAttr = etlAttr[0];
evt.deletedAttributes = evt.deletedAttributes || [];
let etl = etlAttr.get('current').split(',');
etl.forEach(function(attrId) {
let attrEffet = getObj('attribute', attrId);
if (attrEffet === undefined) return;
let nomAttrEffet = attrEffet.get('name');
let charId = attrEffet.get('characterid');
if (estEffetTemp(nomAttrEffet)) {
finDEffet(attrEffet, effetTempOfAttribute(attrEffet), nomAttrEffet, charId, evt);
} else if (estEffetCombat(nomAttrEffet)) {
let mc = messageFin(personnage, messageEffetCombat[effetCombatOfAttribute(attrEffet)]);
if (mc && mc !== '') sendChar(charId, mc, true);
evt.deletedAttributes.push(attrEffet);
attrEffet.remove();
}
});
evt.deletedAttributes.push(etlAttr);
etlAttr.remove();
}
//On enlève les auras
if (stateCOF.combat &&
(predicateAsBool(personnage, 'auraDrainDeForce') || attributeAsBool(personnage, 'aura'))
) {
let auras = stateCOF.combat.auras;
if (auras) {
if (!evt.combat) evt.combat = {...stateCOF.combat
};
stateCOF.combat.auras = auras.filter(function(aura) {
return personnage.token.id == aura.origineId;
});
}
}
if (attributeAsBool(personnage, 'objetAnime')) {
let attr = tokenAttribute(personnage, 'objetAnime')[0];
let nom = attr.get('name');
finDEffet(attr, 'objetAnime', nom, personnage.charId, evt);
} else if (charAttributeAsBool(personnage, 'armeeConjuree')) {
removeFromTurnTracker(personnage, evt);
deleteTokenWithUndo(personnage.token, evt);
sendPerso(personnage, 'disparaît');
let armeeChar = getObj('character', personnage.charId);
if (armeeChar) {
evt.deletedCharacters = evt.deletedCharacters || [];
evt.deletedCharacters.push({
id: personnage.charId,
name: armeeChar.get('name'),
avatar: armeeChar.get('avatar'),
attributes: findObjs({
_type: 'attributes',
_characterid: personnage.charId
}),
abilities: findObjs({
_type: 'ability',
_characterid: personnage.charId
})
});
armeeChar.remove();
}
} else if (!estNonVivant(personnage)) {
//Cherche si certains peuvent siphoner l'âme
let allToks =
findObjs({
_type: 'graphic',
_pageid: pageId,
_subtype: 'token',
layer: 'objects'
});
//On cherche d'abord si un siphon des âmes est prioritaire
let prioriteSiphon = [];
allToks = allToks.filter(function(tok) {
if (tok.id == token.id) return false;
let p = persoOfToken(tok);
if (p === undefined) return false;
if (getState(p, 'mort')) return false;
if (distanceCombat(token, tok, pageId) > 20) return false;
if (predicateAsBool(p, 'siphonDesAmes')) {
prioriteSiphon.push({
perso: p,
priorite: predicateAsInt(p, 'siphonDesAmesPrioritaire', 0)
});
}
return true;
});
if (prioriteSiphon.length > 0) {
let pvMax = parseInt(personnage.token.get('bar1_max'));
if (isNaN(pvMax) || pvMax < 1) pvMax = 1;
if (estPJ(personnage) || predicateAsBool(personnage, 'PVPartagesAvec')) {
let siphoneur = prioriteSiphon[0].perso;
let bonus = predicateAsInt(siphoneur, 'siphonDesAmes', 0);
let jetSiphon = "(1d6";
if (bonus > 0) jetSiphon += '+' + bonus;
jetSiphon += ")";
sendChat('COF', "/w GM " + personnage.token.get('name') + " est un personnage joueur, possible qu'il ne soit pas vraiment mort mais juste inconscient. S'il est vraiment mort, faire le siphon des âmes par " + siphoneur.token.get('name') + " à la main " + jetSiphon);
} else {
prioriteSiphon.sort(function(a, b) {
return b.priorite - a.priorite;
});
let fraction = 100;
let fractionPriorite = fraction;
let priorite = prioriteSiphon[0].priorite;
prioriteSiphon.forEach(function(x) {
if (x.priorite < priorite) {
priorite = x.priorite;
fractionPriorite = fraction;
}
let p = x.perso;
if (fractionPriorite < 1) {
whisperChar(p.charId, "ne réussit pas à siphoner l'âme de " + token.get('name') + " un autre pouvoir l'a siphonée avant lui");
return true;
}
let bonus = predicateAsInt(p, 'siphonDesAmes', 0);
let nbDes = 1 + Math.floor(pvMax / 60);
let soin = rollDePlus(6, {
bonus,
nbDes
});
let soinTotal = soin.val;
//Le montant total des soins ne peut excéder les pv max du personnage qui vient de mourrir.
let display = true;
if (soinTotal > pvMax) {
soinTotal = pvMax;
display = false;
}
if (soinTotal < 1) soinTotal = 1;
soin.val = soinTotal;
soin.val = Math.ceil(soin.val * fractionPriorite / 100);
//Cherche si il y a un perso lié
let lie = personnageAmeLiee(p, pageId, allToks);
soigneToken(p, soin.val, evt,
function(s) {
let siphMsg = "siphone l'âme de " + token.get('name') +
". " + onGenre(p, 'Il', 'Elle') + " récupère ";
if (s == soinTotal) {
if (display) siphMsg += soin.roll + " pv.";
else siphMsg += s + " pv (jet " + soin.roll + ").";
fraction = 0;
} else {
siphMsg += s + " pv (jet " + soin.roll + ").";
fraction -= Math.ceil(s * 100 / soinTotal);
}
pvMax -= s;
whisperChar(p.charId, siphMsg);
if (pvMax > 0 && lie && (s > 4 || s < soinTotal)) {
let soin2 = soinTotal - s + Math.floor(s / 5);
soigneToken(lie, soin2, evt, function(s) {
let siphMsg =
"récupère une partie de l'âme de " + token.get('name') +
" siphonée par " + nomPerso(p) + ". " + onGenre(lie, 'Il', 'Elle') + " récupère " + s + " pv.";
if (s == soin2) {
fraction = 0;
} else {
fraction -= Math.ceil(s * 100 / soin2);
}
pvMax -= s;
whisperChar(lie.charId, siphMsg);
}, function() {
whisperChar(lie.charId, "sent qu'" + onGenre(lie, 'il', 'elle') + " aurait pu bénéficier d'une âme");
});
}
},
function() {
if (lie) {
soigneToken(lie, soin.val, evt, function(s) {
let siphMsg = "siphone l'âme de " + token.get('name') +
" grâce à " + nomPerso(p) + ". " + onGenre(lie, 'Il', 'Elle') + " récupère ";
if (s == soinTotal) {
siphMsg += soin.roll + " pv.";
fraction = 0;
} else {
siphMsg += s + " pv (jet " + soin.roll + ").";
fraction -= Math.ceil(s * 100 / soinTotal);
}
pvMax -= s;
whisperChar(lie.charId, siphMsg);
}, function() {
whisperChar(p.charId,
"est déjà au maximum de point de vie. " + onGenre(p, 'Il', 'Elle') + " laisse échapper l'âme de " + token.get('name'));
});
} else {
whisperChar(p.charId,
"est déjà au maximum de point de vie. " + onGenre(p, 'Il', 'Elle') + " laisse échapper l'âme de " + token.get('name'));
}
});
});
}
}
}
break;
}
case 'immobilise':
case 'surpris':
case 'etourdi':
case 'paralyse':
case 'apeure':
nePlusSuivre(personnage, pageId, evt);
lockToken(personnage, evt);
break;
case 'assomme':
case 'endormi':
nePlusSuivre(personnage, pageId, evt);
lockToken(personnage, evt);
if (stateCOF.combat) {
removeTokenAttr(personnage, 'rageDuBerserk', evt);
}
}
} else { //value est false
if (etat == 'mort' && stateCOF.combat)
removeTokenAttr(personnage, 'a0PVDepuis', evt);
if (!options.fromTemp)
removeTokenAttr(personnage, etat + 'Temp', evt);
}
if (!value) {
unlockToken(personnage, evt);
if (stateCOF.combat && etatRendInactif(etat) && isActive(personnage) ||
etat == 'aveugle') updateInit(token, evt);
}
return true;
}
//fonction avec callback, mais synchrone
// n'ajoute pas evt à l'historique
function soigneToken(perso, soins, evt, callTrue, callMax, options) {
options = options || {};
let token = perso.token;
let bar1 = parseInt(token.get("bar1_value"));
let pvmax = parseInt(token.get("bar1_max"));
if (isNaN(bar1) || isNaN(pvmax)) {
error("Soins sur un token sans points de vie", token);
if (callMax) callMax();
return;
}
let updateBar1;
if (bar1 >= pvmax) bar1 = pvmax;
else updateBar1 = true;
if (soins < 0) soins = 0;
if (predicateAsBool(perso, 'vitaliteEpique')) soins *= 2;
let nonSoignable = 0;
//Update des dm suivis
let attrs = findObjs({
_type: 'attribute',
_characterid: perso.charId,
});
let regSuivis = '^DMSuivis([^_]+)';
let link = token.get('bar1_link');
if (link === '') regSuivis += "_" + token.get('name') + '$';
else regSuivis += '$';
regSuivis = new RegExp(regSuivis);
let soinsSuivis = soins;
let soinsImpossible = new Set(options.saufDMType);
attrs.forEach(function(a) {
if (soinsSuivis === 0) return;
let an = a.get('name');
an = an.match(regSuivis);
if (an && an.length > 0) {
let ds = parseInt(a.get('current'));
if (ds > 0) {
if (an[0].length < 2) {
error("Match non trouvé pour les soins", an);
return;
}
if (soinsImpossible.has(an[1])) {
nonSoignable += ds;
} else {
if (ds > soinsSuivis) {
evt.attributes = evt.attributes || [];
evt.attributes.push({
attribute: a,
current: ds
});
ds -= soinsSuivis;
a.set('current', ds);
soinsSuivis = 0;
} else {
soinsSuivis -= ds;
ds = 0;
}
}
} else ds = 0;
if (ds === 0) {
evt.deletedAttributes = evt.deletedAttributes || [];
evt.deletedAttributes.push(a);
a.remove();
}
}
});
pvmax -= nonSoignable;
if (pvmax <= 0) {
if (callMax) callMax();
return;
}
if (bar1 === 0) {
if (attributeAsBool(perso, 'etatExsangue')) {
removeTokenAttr(perso, 'etatExsangue', evt, {
msg: "retrouve des couleurs"
});
} else if (getState(perso, 'mort')) {
setState(perso, 'renverse', true, evt);
setState(perso, 'mort', false, evt);
}
}
if (predicateAsBool(perso, 'vieArtificielle')) {
soins = Math.floor(soins / 2);
}
bar1 += soins;
let soinsEffectifs = soins;
if (bar1 > pvmax) {
if (attributeAsBool(perso, 'formeDArbre')) {
let apv = tokenAttribute(perso, 'anciensPV');
if (apv.length > 0) {
apv = apv[0];
let anciensPV = parseInt(apv.get('current'));
let anciensMax = parseInt(apv.get('max'));
if (!(isNaN(anciensPV) || isNaN(anciensMax)) &&
anciensPV < anciensMax) {
let soinsTransferes = bar1 - pvmax;
if (anciensMax - anciensPV < soinsTransferes)
soinsTransferes = anciensMax - anciensPV;
anciensPV += soinsTransferes;
bar1 -= soinsTransferes;
setTokenAttr(perso, 'anciensPV', anciensPV, evt, {
maxVal: anciensMax
});
}
}
}
// On cherche si il y a des DM temporaires à soigner
if (bar1 > pvmax) {
let hasMana = (ficheAttributeAsInt(perso, 'PM', 0) > 0);
let dmgTemp;
const estMook = token.get('bar1_link') === '';
if (hasMana) {
if (estMook) dmgTemp = attributeAsInt(perso, 'DMTEMP', 0);
else dmgTemp = ficheAttributeAsInt(perso, 'DMTEMP', 0);
} else {
dmgTemp = toInt(token.get('bar2_value'), 0);
}
if (dmgTemp > 0) {
let newDmgTemp = dmgTemp - bar1 + pvmax;
if (newDmgTemp < 0) {
newDmgTemp = 0;
bar1 -= dmgTemp;
} else bar1 = pvmax;
if (hasMana) setTokenAttr(perso, 'DMTEMP', newDmgTemp, evt);
else updateCurrentBar(perso, 2, newDmgTemp, evt);
}
soinsEffectifs -= (bar1 - pvmax);
bar1 = pvmax;
}
}
if (bar1 == pvmax && attributeAsBool(perso, 'osBrises')) {
removeTokenAttr(perso, 'osBrises', evt, {
msg: "soigne ses os"
});
}
if (updateBar1) updateCurrentBar(perso, 1, bar1, evt);
if (soinsEffectifs > 0) {
if (!options.recuperation) {
if (attributeAsBool(perso, 'blessureQuiSaigne')) {
removeTokenAttr(perso, 'blessureQuiSaigne', evt, {
msg: ": les soins referment la blessure"
});
removeTokenAttr(perso, 'blessureQuiSaignePuissant', evt);
removeTokenAttr(perso, 'blessureQuiSaigneValeur', evt);
removeTokenAttr(perso, 'blessureQuiSaigneSaveParTour', evt);
removeTokenAttr(perso, 'blessureQuiSaigneSaveParTourType', evt);
removeTokenAttr(perso, 'blessureQuiSaigneTempeteDeManaIntense', evt);
removeTokenAttr(perso, 'blessureQuiSaigneOptions', evt);
}
if (attributeAsBool(perso, 'reactionAllergique')) {
removeTokenAttr(perso, 'reactionAllergique', evt, {
msg: ": les soins mettent fin à la réaction allergique"
});
}
}
if (callTrue) callTrue(soinsEffectifs);
} else {
if (callMax) callMax();
}
}
function computeScale(pageId) {
const page = getObj("page", pageId);
let scale = parseFloat(page.get('scale_number'));
if (isNaN(scale) || scale <= 0) return 1.0;
let cellSize = parseFloat(page.get('snapping_increment'));
if (!isNaN(cellSize) && cellSize > 0) scale /= cellSize;
const unit = page.get('scale_units');
switch (unit) {
case 'ft':
scale *= 0.3048;
break;
case 'cm':
scale *= 0.01;
break;
case 'km':
scale *= 1000;
break;
case 'mi':
scale *= 1609.34;
break;
case 'in':
scale *= 0.0254;
break;
}
return scale;
}
// si le token est plus grand que thresh, réduit la distance
function tokenSize(tok, thresh) {
let size = (tok.get('width') + tok.get('height')) / 2;
if (size > thresh) return ((size - thresh) / 2);
return 0;
}
function vecteurUnitaire(pt1, pt2) {
let x = pt2.x - pt1.x;
let y = pt2.y - pt1.y;
let n = Math.sqrt(x * x + y * y);
x = x / n;
y = y / n;
return {
x,
y
};
}
function distancePoints(pt1, pt2) {
let x = pt2.x - pt1.x;
let y = pt2.y - pt1.y;
return Math.sqrt(x * x + y * y);
}
function distanceTokenPrev(token, prev) {
let x = token.get('left') - prev.left;
let y = token.get('top') - prev.top;
return Math.sqrt(x * x + y * y);
}
//Distance en pixels entre 2 tokens
function distancePixToken(tok1, tok2) {
let x = tok1.get('left') - tok2.get('left');
let y = tok1.get('top') - tok2.get('top');
return Math.sqrt(x * x + y * y);
}
function pointOfToken(token) {
return {
x: token.get('left'),
y: token.get('top')
};
}
//Distance en pixels entre un token et un segment
//le segment est donné par ses extrémités, sous forme de {x, y}
function distancePixTokenSegment(token, pt1, pt2) {
let pt = pointOfToken(token);
let seg = {
x: pt2.x - pt1.x,
y: pt2.y - pt1.y
};
let vec = {
x: pt.x - pt1.x,
y: pt.y - pt1.y
}; //vecteur de pt1 à pt
//On regarde d'abord si le projeté de token sur (pt1, pt2) est dans le segment
let ps = seg.x * vec.x + seg.y * vec.y;
if (ps <= 0) { //On est avant pt1
return Math.sqrt(vec.x * vec.x + vec.y * vec.y);
}
let dseg = seg.x * seg.x + seg.y * seg.y;
if (ps >= dseg) { //On est après pt2, on retourne donc la distance pt pt2
let x = pt.x - pt2.x;
let y = pt.y - pt2.y;
return Math.sqrt(x * x + y * y);
}
//On calcule le déterminant de vec et seg
let det = vec.x * seg.y - vec.y * seg.x;
//Et on divise par la longueur du segment
return Math.abs(det) / Math.sqrt(dseg);
}
//options peut avoir les champs:
// - strict1 = true si on considère que tok1 doit avoir une taille nulle
// - strict2
// - allonge
function distanceCombat(tok1, tok2, pageId, options) {
if (pageId === undefined) {
pageId = tok1.get('pageid');
}
options = options || {};
//perso montés
let pseudoTok1 = tok1;
if (!options.strict1) {
let perso1 = persoOfToken(tok1);
if (perso1) {
let attrMonture1 = tokenAttribute(perso1, 'monteSur');
if (attrMonture1.length > 0) {
let pseudoPerso1 = persoOfIdName(attrMonture1[0].get('current'), pageId);
if (pseudoPerso1) pseudoTok1 = pseudoPerso1.token;
}
}
}
let pseudoTok2 = tok2;
if (!options.strict2) {
let perso2 = persoOfToken(tok2);
if (perso2) {
let attrMonture2 = tokenAttribute(perso2, 'monteSur');
if (attrMonture2.length > 0) {
let pseudoPerso2 = persoOfIdName(attrMonture2[0].get('current'), pageId);
if (pseudoPerso2) pseudoTok2 = pseudoPerso2.token;
}
}
}
let scale = computeScale(pageId);
let distance_pix = distancePixToken(pseudoTok1, pseudoTok2);
if (!options.strict1) distance_pix -= tokenSize(pseudoTok1, PIX_PER_UNIT / 2);
if (!options.strict2) distance_pix -= tokenSize(pseudoTok2, PIX_PER_UNIT / 2);
if (options.allonge) distance_pix -= (options.allonge * PIX_PER_UNIT) / scale;
if ((!options.strict1 || !options.strict2) && distance_pix < PIX_PER_UNIT * 1.3) return 0; //cases voisines
return ((distance_pix / PIX_PER_UNIT) * scale);
}
//Attention, seulement faire pour les tokens avec une image dans la librairie
//C'est toujours le cas pour un token créé par le script
function deleteTokenWithUndo(token, evt) {
let tokenFields = getTokenFields(token);
evt.deletedTokens = evt.deletedTokens || [];
evt.deletedTokens.push(tokenFields);
token.remove();
}
//L'argument effetC doit être le nom complet, pas la base
//evt.deletedAttributes doit être défini
function enleverEffetAttribut(charId, effetC, attrName, extension, evt) {
let attrSave = attributeExtending(charId, attrName, effetC, extension);
attrSave.
forEach(function(attrS) {
evt.deletedAttributes.push(attrS);
attrS.remove();
});
}
function finDEffet(attr, effet, attrName, charId, evt, options) { //L'effet arrive en fin de vie, doit être supprimé
options = options || {};
evt.deletedAttributes = evt.deletedAttributes || [];
let res;
let newInit = [];
let efComplet = effetComplet(effet, attrName);
//Si on a un attrSave, alors on a déjà imprimé le message de fin d'effet
if (options.attrSave) { //on a un attribut associé à supprimer)
evt.deletedAttributes.push(options.attrSave);
options.attrSave.remove();
} else if (options.gardeAutresAttributs === undefined) { //On cherche si il y en a un
enleverEffetAttribut(charId, efComplet, attrName, 'SaveParTour', evt);
enleverEffetAttribut(charId, efComplet, attrName, 'SaveActifParTour', evt);
enleverEffetAttribut(charId, efComplet, attrName, 'SaveParTourType', evt);
}
let mEffet = messageEffetTemp[effet];
if (mEffet === undefined) mEffet = messageEffetCombat[effet];
if (mEffet && mEffet.statusMarker) {
iterTokensOfAttribute(charId, options.pageId, effet, attrName, function(token) {
affectToken(token, 'statusmarkers', token.get('statusmarkers'), evt);
token.set('status_' + mEffet.statusMarker, false);
}, {
tousLesTokens: true
});
}
let character;
let combat = stateCOF.combat;
switch (effet) {
case 'affecteParAura': //voir si l'aura est toujours là
if (combat && combat.auras && efComplet.length > 15) {
let id = efComplet.substring(15, efComplet.length - 1);
let aura = combat.auras.find(function(a) {
return a.id == id;
});
if (aura) {
let origine = persoOfId(aura.origineId, aura.origineName, options.pageId);
if (origine) {
let affectes = 0;
let pageId = origine.token.get('pageid');
iterTokensOfAttribute(charId, pageId, efComplet, attrName, function(token) {
if (token.id == origine.token.id) return;
let perso = {
charId,
token
};
affectes += appliquerAura(origine, [perso], pageId, aura, evt, true);
});
if (affectes) return; //On n'efface pas l'attribut !
}
}
}
break;
case 'agrandissement': //redonner sa taille normale
character = getObj('character', charId);
if (character === undefined) {
error("Personnage introuvable");
return;
}
character.get('_defaulttoken', function(normalToken) {
if (normalToken === '') return;
normalToken = JSON.parse(normalToken);
let largeWidth = normalToken.width + normalToken.width / 2;
let largeHeight = normalToken.height + normalToken.height / 2;
iterTokensOfAttribute(charId, options.pageId, effet, attrName, function(token) {
let width = token.get('width');
let height = token.get('height');
affectToken(token, 'width', width, evt);
token.set('width', normalToken.width);
affectToken(token, 'height', height, evt);
token.set('height', normalToken.height);
let perso = {
token,
charId
};
let arme = armesEnMain(perso);
if (arme && arme.armeDeGrand) {
let taille = taillePersonnage(perso, 4);
if (taille == 4) {
sendPerso(perso, "ne peut plus tenir " + arme.name + " à une main");
degainerArme(perso, arme.label, evt, {
deuxMains: true
});
}
}
}, {
filterAffected: function(token) {
if (token.get('width') == largeWidth) return true;
if (token.get('height') == largeHeight) return true;
return false;
}
});
});
break;
case 'aveugleTemp':
iterTokensOfAttribute(charId, options.pageId, effet, attrName,
function(token) {
setState({
token: token,
charId: charId
}, 'aveugle', false, evt, {
fromTemp: true
});
}, {
tousLesTokens: true
});
break;
case 'penombreTemp':
iterTokensOfAttribute(charId, options.pageId, effet, attrName,
function(token) {
setState({
token: token,
charId: charId
}, 'penombre', false, evt, {
fromTemp: true
});
}, {
tousLesTokens: true
});
break;
case 'ralentiTemp':
iterTokensOfAttribute(charId, options.pageId, effet, attrName,
function(token) {
setState({
token: token,
charId: charId
}, 'ralenti', false, evt, {
fromTemp: true
});
}, {
tousLesTokens: true
});
break;
case 'paralyseTemp':
case 'paralyseGoule':
iterTokensOfAttribute(charId, options.pageId, effet, attrName,
function(token) {
setState({
token: token,
charId: charId
}, 'paralyse', false, evt, {
fromTemp: true
});
}, {
tousLesTokens: true
});
break;
case 'immobiliseTemp':
iterTokensOfAttribute(charId, options.pageId, effet, attrName,
function(token) {
setState({
token: token,
charId: charId
}, 'immobilise', false, evt, {
fromTemp: true
});
}, {
tousLesTokens: true
});
break;
case 'etourdiTemp':
iterTokensOfAttribute(charId, options.pageId, effet, attrName,
function(token) {
setState({
token: token,
charId: charId
}, 'etourdi', false, evt, {
fromTemp: true
});
}, {
tousLesTokens: true
});
break;
case 'affaibliTemp':
iterTokensOfAttribute(charId, options.pageId, effet, attrName,
function(token) {
setState({
token: token,
charId: charId
}, 'affaibli', false, evt, {
fromTemp: true
});
}, {
tousLesTokens: true
});
break;
case 'assommeTemp':
iterTokensOfAttribute(charId, options.pageId, effet, attrName,
function(token) {
setState({
token: token,
charId: charId
}, 'assomme', false, evt, {
fromTemp: true
});
}, {
tousLesTokens: true
});
break;
case 'invisibleTemp':
case 'intangibleInvisible':
iterTokensOfAttribute(charId, options.pageId, effet, attrName,
function(token) {
setState({
token: token,
charId: charId
}, 'invisible', false, evt, {
fromTemp: true
});
}, {
tousLesTokens: true
});
break;
case 'apeureTemp':
case 'peurEtourdi':
iterTokensOfAttribute(charId, options.pageId, effet, attrName,
function(token) {
setState({
token: token,
charId: charId
}, 'apeure', false, evt, {
fromTemp: true
});
}, {
tousLesTokens: true
});
break;
case 'ombreMortelle':
case 'dedoublement':
iterTokensOfAttribute(charId, options.pageId, effet, attrName, function(token) {
deleteTokenWithUndo(token, evt);
});
break;
case 'murDeForce':
iterTokensOfAttribute(charId, options.pageId, effet, attrName, function(token) {
let attrM = tokenAttribute({
charId: charId,
token: token
}, 'murDeForceId');
if (attrM.length === 0) return;
let imageMur = getObj('graphic', attrM[0].get('current'));
if (imageMur) {
imageMur.remove();
}
attrM[0].remove();
});
break;
case 'zoneDeVie':
let attrIdName = efComplet + 'Id';
iterTokensOfAttribute(charId, options.pageId, efComplet, attrName, function(token) {
let perso = {
token,
charId
};
let attr = tokenAttribute(perso, attrIdName);
if (attr.length === 0) return;
let image = getObj('graphic', attr[0].get('current'));
if (image) {
image.remove();
}
attr[0].remove();
});
break;
case 'regeneration': //faire les soins restants
let toursRestant = parseInt(attr.get('current'));
iterTokensOfAttribute(charId, options.pageId, effet, attrName,
function(token) {
let perso = {
token,
charId
};
if (!isNaN(toursRestant) && toursRestant > 0) {
let regen = getIntValeurOfEffet(perso, 'regeneration', 3);
let soins = regen * (toursRestant + attributeAsInt(perso, 'regenerationTempeteDeManaIntense', 0));
soigneToken(perso, soins, evt,
function(s) {
options.print = function(m) {}; //Pour ne pas afficher le message final.
let tempsEffectif = Math.ceil(s / regen);
sendPerso(perso, "récupère encore " + s + " PV en " + tempsEffectif + " tours.");
});
}
//Régénération d'une carac physique affaiblie de 1d4, si il y en a.
if (attributeAsInt(perso, 'affaiblissementdeconstitution', 0) > 0) {
let d4 = rollDePlus(4);
diminueAffaiblissement(perso, 'constitution', d4.val, evt);
sendPerso(perso, "récupère " + d4.roll + " points de constitution");
} else if (attributeAsInt(perso, 'affaiblissementdeforce', 0) > 0) {
let d4 = rollDePlus(4);
diminueAffaiblissement(perso, 'force', d4.val, evt);
sendPerso(perso, "récupère " + d4.roll + " points de force");
} else if (attributeAsInt(perso, 'affaiblissementdedexterite', 0) > 0) {
let d4 = rollDePlus(4);
diminueAffaiblissement(perso, 'dexterite', d4.val, evt);
sendPerso(perso, "récupère " + d4.roll + " points de dextérité");
}
});
break;
case 'demonInvoque':
case 'predateurConjure':
case 'arbreAnime':
case 'objetAnime':
case 'degradationZombie': //effacer le personnage
//Dans le cas d'un Zombie, diminuer la limite du nécromant si nécessaire
if (effet == 'degradationZombie') {
let attrNecromant = charAttribute(charId, 'necromant');
if (attrNecromant.length > 0) {
let id = attrNecromant[0].get('current');
let necromant = persoOfId(id, id, options.pageId);
if (necromant) {
let attrNbZombie = tokenAttribute(necromant, 'zombiesControles');
if (attrNbZombie.length > 0) {
let nbZombie = attrAsInt(attrNbZombie, 1);
if (nbZombie > 1)
setTokenAttr(necromant, 'zombiesControles', nbZombie - 1, evt);
else attrNbZombie[0].remove();
}
}
}
}
if (effet == 'objetAnime') {
let attr = charAttribute(charId, 'objetAnimePar');
if (attr.length > 0) {
let nid = attr[0].get("current");
let lanceur = persoOfIdName(nid, options.pageId);
if (lanceur) {
let attrNbObjets = tokenAttribute(lanceur, 'niveauDesObjetsAnimes');
if (attrNbObjets.length > 0) {
let niveauObjets = ficheAttributeAsInt({
charId
}, 'niveau', 1);
let nbObjets = attrAsInt(attrNbObjets, niveauObjets);
if (nbObjets > niveauObjets)
setTokenAttr(lanceur, 'niveauDesObjetsAnimes', nbObjets - niveauObjets, evt);
else attrNbObjets[0].remove();
}
}
}
}
//On efface d'abord les attributs et les abilities
let charAttributes = findObjs({
_type: 'attribute',
_characterid: charId
});
charAttributes.forEach(
function(otherAttr) {
if (otherAttr.id != attr.id) otherAttr.remove();
}
);
let charAbilities = findObjs({
_type: 'ability',
_characterid: charId
});
charAbilities.forEach(
function(ab) {
ab.remove();
}
);
if (effet == 'arbreAnime' || (effet == 'objetAnime' && charPredicateAsBool(charId, 'animeAPartirDExistant'))) {
iterTokensOfAttribute(charId, options.pageId, effet, attrName,
function(token) {
let perso = {
token: token,
charId: charId
};
let nA = removeFromTurnTracker(perso, evt);
if (nA) {
res = res || {};
res.oldTokenId = token.id;
res.newTokenId = nA.nextId;
}
setToken(token, 'bar1_link', '', evt);
setToken(token, 'bar1_value', '', evt);
setToken(token, 'bar1_max', '', evt);
setToken(token, 'showplayers_bar1', false, evt);
setToken(token, 'represents', '', evt);
setToken(token, 'showname', false, evt);
setToken(token, 'showplayers_name', false, evt);
setToken(token, 'name', '', evt);
});
} else {
iterTokensOfAttribute(charId, options.pageId, effet, attrName, function(token) {
let perso = {
token: token,
charId: charId
};
let nP = removeFromTurnTracker(perso, evt);
if (nP) {
res = res || {};
res.oldTokenId = token.id;
res.newTokenId = nP.nextId;
}
deleteTokenWithUndo(token, evt);
});
}
attr.remove();
let msgFin = messageFin({
charId
}, mEffet);
if (options.print && mEffet) options.print(msgFin);
else {
sendChar(charId, msgFin, true);
options.print = function(m) {};
}
character = getObj('character', charId);
if (character) {
evt.deletedCharacters = evt.deletedCharacters || [];
let deletedChar = {
id: charId,
name: character.get('name'),
avatar: character.get('avatar'),
attributes: charAttributes,
abilities: charAbilities,
allies: []
};
// Retrait du perso de toutes les listes d'alliés
for (const [perso, alliesPerso] of Object.entries(alliesParPerso)) {
if (alliesPerso.has(charId)) {
deletedChar.allies.push(perso);
alliesPerso.delete(charId);
}
}
character.remove();
evt.deletedCharacters.push(deletedChar);
}
return res; //Pas besoin de faire le reste, car plus de perso
case 'formeDArbre':
{
let iterTokOptions = {
filterAffected: function(token) {
return token.get('layer') == 'objects';
}
};
iterTokensOfAttribute(charId, options.pageId, effet, attrName,
function(token) {
let perso = {
token: token,
charId: charId
};
let tokenChange = attributeAsBool(perso, 'changementDeToken');
if (tokenChange) {
let tokenMJ =
findObjs({
_type: 'graphic',
_subtype: 'token',
_pageid: token.get('pageid'),
layer: 'gmlayer',
represents: charId,
name: token.get('name')
});
if (tokenMJ.length === 0) return;
removeTokenAttr(perso, 'changementDeToken', evt);
let nouveauToken = tokenMJ[0];
setToken(nouveauToken, 'layer', 'objects', evt);
setToken(nouveauToken, 'left', token.get('left'), evt);
setToken(nouveauToken, 'top', token.get('top'), evt);
setToken(nouveauToken, 'width', token.get('width'), evt);
setToken(nouveauToken, 'height', token.get('height'), evt);
setToken(nouveauToken, 'rotation', token.get('rotation'), evt);
setToken(nouveauToken, 'bar2_value', token.get('bar2_value'), evt);
setToken(nouveauToken, 'aura1_radius', token.get('aura1_radius'), evt);
setToken(nouveauToken, 'aura1_color', token.get('aura1_color'), evt);
setToken(nouveauToken, 'aura1_square', token.get('aura1_square'), evt);
setToken(nouveauToken, 'showplayers_aura1', token.get('showplayers_aura1'), evt);
setToken(nouveauToken, 'aura2_radius', token.get('aura2_radius'), evt);
setToken(nouveauToken, 'aura2_color', token.get('aura2_color'), evt);
setToken(nouveauToken, 'aura2_square', token.get('aura2_square'), evt);
setToken(nouveauToken, 'showplayers_aura2', token.get('showplayers_aura2'), evt);
setToken(nouveauToken, 'statusmarkers', token.get('statusmarkers'), evt);
setToken(nouveauToken, 'light_angle', token.get('light_angle'), evt);
setToken(nouveauToken, 'has_limit_field_of_vision', token.get('has_limit_field_of_vision'), evt);
setToken(nouveauToken, 'has_limit_field_of_night_vision', token.get('has_limit_field_of_night_vision'), evt);
if (combat) {
replaceInTurnTracker(token.id, nouveauToken.id, evt);
}
res = res || {};
res.oldTokenId = token.id;
res.newTokenId = nouveauToken.id;
res.newToken = nouveauToken;
deleteTokenWithUndo(token, evt);
token = nouveauToken;
perso.token = nouveauToken;
}
let apv = tokenAttribute(perso, 'anciensPV');
if (apv.length > 0) {
updateCurrentBar(perso, 1, apv[0].get('current'), evt, apv[0].get('max'));
removeTokenAttr(perso, 'anciensPV', evt);
if (combat) {
newInit.push({
_id: token.id
});
}
}
},
iterTokOptions);
break;
}
case 'agitAZeroPV':
iterTokensOfAttribute(charId, options.pageId, effet, attrName, function(token) {
let perso = {
charId: charId,
token: token
};
let pv = token.get('bar1_value');
if (pv == 0) { //jshint ignore:line
mort(perso, undefined, evt);
} else {
//On note qu'il l'a déjà fait pour qu'il ne puisse le refaire dans le combat
setTokenAttr(perso, 'aAgiAZeroPV', true, evt);
}
});
break;
case 'forgeron':
case 'armeEnflammee':
iterTokensOfAttribute(charId, options.pageId, efComplet, attrName, function(token) {
let perso = {
token: token,
charId: charId
};
eteindreUneLumiere(perso, options.pageId, undefined, efComplet, evt);
});
break;
case 'effetRetarde':
if (efComplet.length > 14) {
let effetRetarde = efComplet.substring(13, efComplet.length - 1);
if (_.has(cof_states, effetRetarde)) {
iterTokensOfAttribute(charId, options.pageId, efComplet, attrName, function(token) {
let perso = {
token: token,
charId: charId
};
if (getState(perso, 'mort')) return;
setState(perso, effetRetarde, true, evt);
});
} else if (estEffetTemp(effetRetarde)) {
options.print = function(m) {}; //Pour ne pas afficher le message final.
let pp = effetRetarde.indexOf('(');
let mEffetRetarde = (pp > 0) ? messageEffetTemp[effetRetarde.substring(effetRetarde, pp)] : messageEffetTemp[effetRetarde];
let ef = {
effet: effetRetarde,
duree: 1,
message: mEffetRetarde,
whisper: true,
};
iterTokensOfAttribute(charId, options.pageId, efComplet, attrName, function(token) {
let perso = {
token,
charId
};
if (getState(perso, 'mort')) return;
if (!combat) {
sendChat('', "Il restait un effet retardé " + effetRetarde + " qui devait se déclencher pour " + token.get('name'));
return;
}
let duree = getIntValeurOfEffet(perso, efComplet, 1);
ef.duree = duree;
setEffetTemporaire(perso, ef, duree, evt, {});
});
} else {
options.print = function(m) {}; //Pour ne pas afficher le message final.
iterTokensOfAttribute(charId, options.pageId, efComplet, attrName, function(token) {
let perso = {
token: token,
charId: charId
};
if (getState(perso, 'mort')) return;
let val = true;
let valAttr = tokenAttribute(perso, efComplet + 'Valeur');
if (valAttr.length > 0) val = valAttr[0].get('current');
whisperChar(charId, effetRetarde + ' ' + val);
setTokenAttr(perso, effetRetarde, val, evt, {});
});
}
}
break;
case 'poisonAffaiblissantLatent':
options.print = function(m) {}; //Pour ne pas afficher le message final.
iterTokensOfAttribute(charId, options.pageId, efComplet, attrName, function(token) {
let perso = {
token: token,
charId: charId
};
if (getState(perso, 'mort')) return;
whisperChar(charId, "Le poison commence à faire effet !");
setTokenAttr(perso, 'poisonAffaiblissantLong', true, evt, {});
});
break;
case 'messageRetarde':
if (efComplet.length > 16) {
let messageRetarde = efComplet.substring(15, efComplet.length - 1);
iterTokensOfAttribute(charId, options.pageId, efComplet, attrName, function(token) {
whisperChar(charId, messageRetarde);
//Puis on regarde si il y a une valeur à afficher
let perso = {
token: token,
charId: charId
};
let valAttr = tokenAttribute(perso, efComplet + 'Valeur');
if (valAttr.length > 0)
whisperChar(charId, valAttr[0].get('current').replace(/_/g, ' '));
});
}
break;
case 'tenebres':
iterTokensOfAttribute(charId, options.pageId, efComplet, attrName, function(token) {
//Puis on regarde si il y a une valeur à afficher
let perso = {
token: token,
charId: charId
};
let valAttr = tokenAttribute(perso, efComplet + 'Valeur');
let tokenTenebres = getObj('graphic', valAttr[0].get('current'));
if (tokenTenebres) tokenTenebres.remove();
});
break;
case 'brumes':
iterTokensOfAttribute(charId, options.pageId, efComplet, attrName, function(token) {
//Puis on regarde si il y a une valeur à afficher
let perso = {
token: token,
charId: charId
};
let valAttr = tokenAttribute(perso, efComplet + 'Valeur');
let tokenTenebres = getObj('graphic', valAttr[0].get('current'));
if (tokenTenebres) tokenTenebres.remove();
});
break;
case 'armeeDesMorts':
{
iterTokensOfAttribute(charId, options.pageId, efComplet, attrName, function(token) {
token.set("aura2_radius", 0);
if (combat.armeesDesMorts && combat.armeesDesMorts[token.id]) {
if (!evt.combat) evt.combat = {...combat
};
evt.combat.armeesDesMorts = {...combat.armeesDesMorts
};
combat.armeesDesMorts[token.id] = undefined;
}
});
break;
}
case 'lienDeSang':
iterTokensOfAttribute(charId, options.pageId, efComplet, attrName, function(token) {
let perso = {
token: token,
charId: charId
};
let attrsLienDeSangDe = tokenAttribute(perso, "lienDeSangDe");
if (attrsLienDeSangDe.length > 0) {
let tokenLie = persoOfId(attrsLienDeSangDe[0].get("current"));
if (tokenLie) {
tokenAttribute(tokenLie, "lienDeSangVers").forEach(function(attr) {
attr.remove();
});
}
}
attrsLienDeSangDe.forEach(function(attr) {
attr.remove();
});
});
break;
default:
}
if (options.attrSave === undefined && charId) {
let estMort = true;
iterTokensOfAttribute(charId, options.pageId, efComplet, attrName, function(token) {
estMort = estMort && getState({
charId: charId,
token: token
}, 'mort');
});
if (!estMort && mEffet) {
let msgFin = messageFin({
charId
}, mEffet);
if (options.print) options.print(msgFin);
else {
if (attrName == efComplet)
sendChar(charId, msgFin, true);
else {
let tokenName = attrName.substring(attrName.indexOf('_') + 1);
sendChat('', tokenName + ' ' + msgFin);
}
}
}
}
if (options.gardeAutresAttributs === undefined && charId) {
enleverEffetAttribut(charId, efComplet, attrName, 'Puissant', evt);
enleverEffetAttribut(charId, efComplet, attrName, 'Valeur', evt);
enleverEffetAttribut(charId, efComplet, attrName, 'TempeteDeManaIntense', evt);
enleverEffetAttribut(charId, efComplet, attrName, 'DureeAccumulee', evt);
enleverEffetAttribut(charId, efComplet, attrName, 'Options', evt);
}
//On remet la face du token
let attrTS = attributeExtending(charId, attrName, efComplet, 'TokenSide');
if (attrTS.length > 0) {
attrTS = attrTS[0];
let side = attrTS.get('current');
iterTokensOfAttribute(charId, options.pageId, efComplet, attrName, function(token) {
changeTokenSide({
token
}, side, evt);
}, {
tousLesTokens: true
});
evt.deletedAttributes.push(attrTS);
attrTS.remove();
}
evt.deletedAttributes.push(attr);
attr.remove();
//Débloque les tokens si l'effet les immobilisait
switch (effet) {
case 'bloqueManoeuvre':
case 'prisonVegetale':
case 'toiles':
case 'statueDeBois':
case 'petrifie':
case 'paralysieRoublard':
iterTokensOfAttribute(charId, options.pageId, efComplet, attrName, function(token) {
let perso = {
token: token,
charId: charId
};
unlockToken(perso, evt);
});
}
if (newInit.length > 0) initiative(newInit, evt, true);
return res;
}
function lockToken(perso, evt) {
let lock = perso.token.get('lockMovement');
if (!lock) {
affectToken(perso.token, 'lockMovement', false, evt);
perso.token.set('lockMovement', true);
}
}
//retourne le personnage du compagnon s'il est présent et actif
function compagnonPresent(personnage, nomCompagnon) {
let compagnon = predicateAsBool(personnage, nomCompagnon);
if (compagnon) {
let compToken = findObjs({
_type: 'graphic',
_subtype: 'token',
_pageid: personnage.token.get('pageid'),
layer: 'objects',
name: compagnon
});
let res;
compToken.forEach(function(tok) {
if (res) return;
let compCharId = tok.get('represents');
if (compCharId === '') return;
compagnon = {
token: tok,
charId: compCharId
};
if (isActive(compagnon)) res = compagnon;
});
return res;
}
return;
}
function personnageAmeLiee(perso, pageId, allToks) {
let lie = predicateAsBool(perso, 'ameLieeAvec');
if (!lie) return;
let tokenLie;
if (allToks) {
tokenLie = allToks.find(function(tok) {
return tok.get('name') == lie;
});
if (tokenLie) return persoOfToken(tokenLie);
} else {
pageId = pageId || perso.token.get('pageid');
tokenLie = findObjs({
_type: 'graphic',
_pageid: pageId,
_subtype: 'token',
layer: 'objects',
name: lie
});
if (tokenLie.length > 0) {
let p;
let t = tokenLie.find(function(tok) {
if (tok.id == perso.token.id) return false;
p = persoOfToken(tok);
if (p === undefined) return false;
if (getState(p, 'mort')) return false;
return true;
});
if (t) return p;
} else {
return;
}
}
}
function estControlleParJoueur(charId) {
let character = getObj('character', charId);
if (character === undefined) return false;
return character.get('controlledby').length > 0;
}
//Pour savoir si un personnage est un personnage joueur
// -> fiche de PJ + dé de vie + token lié + controllé par au moins un joueur.
function estPJ(perso) {
if (persoEstPNJ(perso)) return false;
let dv = ficheAttributeAsInt(perso, 'DV', 0);
if (dv === 0) return false;
if (perso.token.get('bar1_link') === '') return false;
return estControlleParJoueur(perso.charId);
}
/*
function logEvents() {
let l = eventHistory.length;
log("Historique de taille " + l);
eventHistory.forEach(function(evt, i) {
log("evt " + i);
log(evt);
});
}*/
/* Événements, utilisés pour les undo, en particulier undo pour refaire
* une action quand une règle le permet (utilisation de points de chance, etc..)
* Champ d'un événement (variables evt en général dans le code):
* id : identificateur unique (int)
* type : description de l'événement (string)
* affectes : liste de tokens affectés par l'événement
* tokens : liste des tokens créés
* deletedTokens : liste de tokens effacés
* !!!!! -> ne garde pas les tokens effacés si on n'est pas sûr que son image est au bon endroit. Typiquement, on ne va le faire que pour les tokens crées dans le script
* attributes : liste de attributs créés ou modifiés
* deletesAttributes: lites des attributs effacés
* characters : liste des personnages créés
* characterNames : liste de character * name
* defaultTokens : liste de tokens par défaut (objet)
* (character, defaultToken)
* deletedCharacters: liste des personnages effacés
* combat : valeur de la variable d'état combat
* updateNextInitSet: valeur de l'ensemble des tokens dont il faut recalculer l'init
* turnorder : le turnorder (si il a changé)
* initiativepage : true si le turnorder est actif
* personnage : le perso qui 'fait' l'événement
* succes : stoque si l'attaque était un succès (bool)
* action : sauvegarde des paramètres de l'evt, pour la rejouer
* - caracteristique : carac testée (pour un jet)
* - titre : titre du jet
* - playerId : id du joueur qui a lancé l'action
* - selected : cibles sélectionnés des l'action
* - attaquant: personnage attaquant (TODO: voir si doublon avec personnage)
* - cibles: liste des cibles d'attaque, avec leurs tags
* - weaponStats: stats de l'arme (ou attaque) utilisée
* - rolls: les jets de l'action, pour les avoir à l'identique
* les dégâts sont stoqués dans chaque cible, dans cible.rollsDmg
* - attack: les jets de l'attaque
* - etat_e_index_targetid: save pour entrer dans l'état e
* - effet_e_index_targetid: save pour l'effet e
* - attaquant_pietinement_targetid: jet de l'attaquant pour le piétinement
* - defenseur_pietinement_targetid: jet de du défenseur pour le piétinement
* - options : options de l'action
* attenteResultat : permet de savoir que le jet est en attente de décision pour savoir si c'est un succès ou non (quand il n'y a pas de difficulté donnée et que le personnage est sous l'emprise d'une malédiction)
*/
function addEvent(evt) {
if (evt.id) {
error("Tentative d'ajouter un événement déjà dans l'historique", evt);
return;
}
evt.id = stateCOF.eventId++;
eventHistory.push(evt);
if (eventHistory.length > HISTORY_SIZE) {
eventHistory.shift();
}
}
function findEvent(id) {
return eventHistory.find(function(evt) {
return (evt.id == id);
});
}
function lastEvent() {
let l = eventHistory.length;
if (l === 0) return undefined;
return eventHistory[l - 1];
}
function setDefaultTokenFromSpec(character, spec, token) {
let oldTokenFields = {};
for (const field in spec) {
if (field.startsWith('_')) continue;
if (field == 'imgsrc' || field == 'represents' || field == 'top' ||
field == 'left' || field == 'page_id' || field == 'layer' ||
field == 'lastmove') continue;
let oldValue = token.get(field);
if (oldValue == spec[field]) continue;
oldTokenFields[field] = oldValue;
token.set(field, spec[field]);
}
setDefaultTokenForCharacter(character, token);
for (const otf in oldTokenFields) {
token.set(otf, oldTokenFields[otf]);
}
}
//Si evt n'est pas défini, annule le dernier evt
function undoEvent(evt) {
if (evt === undefined) {
if (eventHistory.length === 0) {
sendChat('COF', "/w GM Historique d'évènements vide");
return;
}
evt = eventHistory.pop();
} else {
eventHistory = eventHistory.filter(function(e) {
return (e.id != evt.id);
});
}
if (evt === undefined) {
error("No event to undo", eventHistory);
return;
}
sendChat("COF", "/w GM undo " + evt.type);
if (evt.affectes) undoTokenEffect(evt);
if (evt.attributes) {
// some attributes where modified too
evt.attributes.forEach(function(attr) {
if (attr.current === undefined) attr.attribute.remove();
else {
let aset = {
current: attr.current
};
if (attr.max !== undefined) aset.max = attr.max;
if (attr.name !== undefined) aset.name = attr.name;
if (attr.withWorker) attr.attribute.setWithWorker(aset);
else attr.attribute.set(aset);
}
});
}
if (evt.characterNames) {
evt.characterNames.forEach(function(cn) {
if (cn.name && cn.character)
cn.character.set('name', cn.name);
});
}
if (evt.defaultTokens) {
evt.defaultTokens.forEach(function(dt) {
//On cherche d'abord un token qui représente dt.character
let tokens = findObjs({
_type: 'graphic',
represents: dt.character.id
});
if (tokens.length === 0) return;
setDefaultTokenFromSpec(dt.character, dt.defaultToken, tokens[0]);
});
}
if (evt.deletedTokens) {
evt.deletedTokens.forEach(function(token) {
log("Restoring token " + token.name);
let t = createObj('graphic', token);
if (token.layer == 'map') toFront(t);
});
}
if (evt.deletedCharacters) {
evt.deletedCharacters.forEach(function(character) {
log("Restoring character " + character.name);
let newCharacter =
createObj('character', {
name: character.name,
avatar: character.avatar
});
let charId = newCharacter.id;
let tokens = findObjs({
_type: 'graphic',
represents: character.id
});
tokens.forEach(function(tok) {
tok.set('represents', charId);
});
eventHistory.forEach(function(evt2) {
if (evt2.characters) {
evt2.characters = evt2.characters.map(function(oldCharac) {
if (oldCharac.id == character.id) return newCharacter;
return oldCharac;
});
}
if (evt2.deletedAttributes) {
evt2.deletedAttributes.forEach(function(attr) {
if (attr.get('characterid') == character.id) attr.newCharId = charId;
});
}
});
if (evt.deletedAttributes) {
evt.deletedAttributes.forEach(function(attr) {
if (attr.get('characterid') == character.id) {
attr.newCharId = charId;
}
});
}
//Maintenant on remet les attributs
if (character.attributes) {
character.attributes.forEach(function(attr) {
let oldId = attr.id;
let newAttr = createObj('attribute', {
characterid: charId,
name: attr.get('name'),
current: attr.get('current'),
max: attr.get('max')
});
eventHistory.forEach(function(evt) {
if (evt.attributes) {
evt.attributes.forEach(function(attr) {
if (attr.attribute.id == oldId) attr.attribute = newAttr;
});
}
});
tokens.forEach(function(tok) {
if (tok.get('bar1_link') == oldId)
tok.set('bar1_link', newAttr.id);
});
});
}
if (character.abilities) {
character.abilities.forEach(function(ab) {
createObj('ability', {
characterid: charId,
name: ab.get('name'),
action: ab.get('action'),
istokenaction: ab.get('istokenaction')
});
});
}
// On le remet chez ses alliés
if (character.allies.length > 0) {
Object.values(character.allies).forEach(function(allie) {
let alliesPerso = alliesParPerso[allie] || new Set();
alliesPerso.add(charId);
alliesParPerso[allie] = alliesPerso;
});
}
});
}
// deletedAttributes have a quadratic cost in the size of the history
if (evt.deletedAttributes) {
evt.deletedAttributes.forEach(function(attr) {
let oldId = attr.id;
let nameDel = attr.get('name');
log("Restoring attribute " + nameDel);
let newAttr =
createObj('attribute', {
characterid: attr.newCharId || attr.get('characterid'),
name: nameDel,
current: attr.get('current'),
max: attr.get('max')
});
eventHistory.forEach(function(evt) {
if (evt.attributes !== undefined) {
evt.attributes.forEach(function(attr2) {
if (attr2.attribute && attr2.attribute.id == oldId) attr2.attribute = newAttr;
});
}
});
});
}
if (evt.characters) {
evt.characters.forEach(function(character) {
let charId = character.id;
findObjs({
_type: 'attribute',
_characterid: charId
}).forEach(function(attr) {
attr.remove();
});
findObjs({
_type: 'ability',
_characterid: charId
}).forEach(function(ab) {
ab.remove();
});
character.remove();
});
}
if (evt.tokens) {
evt.tokens.forEach(function(token) {
if (stateCOF.tokensTemps) {
stateCOF.tokensTemps = stateCOF.tokensTemps.filter(function(tt) {
return tt.tid != token.id;
});
}
token.remove();
});
}
if (evt.movedTokens) {
evt.movedTokens.forEach(function(movedToken) {
movedToken.token.set('left', movedToken.oldPosition.left);
movedToken.token.set('top', movedToken.oldPosition.top);
});
}
if (_.has(evt, 'combat')) {
let combat = stateCOF.combat;
//regarde si le token actif a changé
if (evt.combat &&
(!combat || evt.combat.activeTokenId != combat.activeTokenId) &&
stateCOF.options.affichage.val.init_dynamique.val) {
let activeToken = getObj('graphic', evt.combat.activeTokenId);
if (activeToken) {
threadSync++;
activateRoundMarker(threadSync, activeToken);
}
}
stateCOF.combat = evt.combat;
}
if (_.has(evt, 'updateNextInitSet'))
updateNextInitSet = evt.updateNextInitSet;
if (_.has(evt, 'turnorder')) Campaign().set('turnorder', evt.turnorder);
if (_.has(evt, 'initiativepage'))
Campaign().set('initiativepage', evt.initiativepage);
if (evt.chargeFantastique)
stateCOF.chargeFantastique = evt.chargeFantastique;
if (evt.deletedTokensTemps && evt.deletedTokensTemps.length > 0) {
stateCOF.tokensTemps = stateCOF.tokensTemps || [];
evt.deletedTokensTemps.forEach(function(tt) {
log("Restoring temp token " + tt.deletedToken.name);
let t = createObj('graphic', tt.deletedToken);
if (tt.deletedToken.layer == 'map') toFront(t);
delete tt.deletedToken;
tt.tid = t.id;
stateCOF.tokensTemps.push(tt);
});
}
if (evt.tokensTemps) { //ceux pour lesquels on a diminué la durée
evt.tokensTemps.forEach(function(tt) {
if (tt.tt) tt.tt.duree = tt.ancienneDuree;
});
}
}
//origin peut être un message ou un nom de joueur
function sendPlayer(origin, msg, playerId) {
let dest = origin;
if (origin.who !== undefined) {
if (origin.who === '') dest = 'GM';
else {
playerId = playerId || getPlayerIdFromMsg(origin);
if (playerId == 'API' || playerIsGM(playerId)) dest = 'GM';
else dest = origin.who;
}
}
if (dest.includes('"')) {
sendChat('COF', msg);
log("Impossible d'envoyer des messages privés à " + dest + " car le nom contient des guillemets");
}
sendChat('COF', '/w "' + dest + '" ' + msg);
}
function sendPlayerAndGM(origin, playerId, msg) {
sendPlayer(origin, msg, playerId);
if (playerIsGM(playerId)) return;
sendChat('COF', '/w GM ' + msg);
}
function isCarac(x) {
switch (x) {
case 'FOR':
case 'DEX':
case 'CON':
case 'SAG':
case 'INT':
case 'CHA':
return true;
default:
return false;
}
}
function deCarac(x) {
switch (x) {
case 'FOR':
return "de force";
case 'DEX':
return "de dextérité";
case 'CON':
return "de constitution";
case 'SAG':
return "de sagesse";
case 'INT':
return "d'intelligence";
case 'CHA':
return "de charisme";
default:
return "de " + x;
}
}
//msg peut être un message ou un playerId
function peutController(msg, perso) {
if (msg === undefined) return true;
let playerId = getPlayerIdFromMsg(msg);
if (playerIsGM(playerId)) return true;
if (msg.selected && msg.selected.length > 0) {
if (perso.token.id == msg.selected[0]._id) return true;
let selectedPerso = persoOfId(msg.selected[0]._id);
if (selectedPerso !== undefined && selectedPerso.charId == perso.charId) return true;
}
let character = getObj('character', perso.charId);
if (character === undefined) return false;
let cb = character.get('controlledby');
let res = cb.split(',').find(function(pid) {
if (pid == 'all') return true;
return (pid == playerId);
});
return (res !== undefined);
}
// !cof-confirmer-attaque evtid
function confirmerAttaque(msg) {
if (!stateCOF.combat) {
sendPlayer(msg, "Trop tard pour continuer l'attaque, on est hors combat");
return;
}
let cmd = msg.content.split(' ');
if (cmd.length < 2) {
error("Pas assez d'arguments pour !cof-confirmer-attaque", cmd);
return;
}
let evt = findEvent(cmd[1]);
if (evt === undefined) {
error("L'action est trop ancienne ou a été annulée", cmd);
return;
}
let action = evt.action;
if (action === undefined) {
error("Erreur interne du bouton de confirmation: l'évènement n'a pas d'action", cmd);
return;
}
let options = action.currentOptions || {};
let playerId = getPlayerIdFromMsg(msg);
let ctrl = playerIsGM(playerId);
if (!ctrl && options.preDmg) {
let tokens = _.allKeys(options.preDmg);
ctrl = tokens.every(function(tid) {
let perso = persoOfId(tid);
if (perso === undefined) return true;
let character = getObj('character', perso.charId);
if (character === undefined) return true;
let cb = character.get('controlledby');
let res = cb.split(',').find(function(pid) {
return (pid == 'all' || pid == playerId);
});
return (res !== undefined);
});
}
if (!ctrl) {
sendPlayer(msg, "pas le droit d'utiliser ce bouton", playerId);
return;
}
options.rolls = action.rolls;
options.choices = action.choices || {};
options.choices.Continuer = true;
resolvePreDmgOptions(action.attaquant, action.ciblesTouchees, action.echecCritique, action.attackLabel, action.weaponStats, action.attackd20roll, action.display, options, evt, action.explications, action.pageId, action.cibles);
}
function undoTokenEffect(evt) {
let HTdeclared;
try {
HTdeclared = HealthColors;
} catch (e) {
if (e.name != "ReferenceError") throw (e);
}
_.each(evt.affectes, function(aff) {
let prev = aff.prev;
let tok = aff.affecte;
if (prev === undefined || tok === undefined) {
error("Pas d'état précédant", aff);
return;
}
let prevTok;
if (HTdeclared) prevTok = JSON.parse(JSON.stringify(tok));
_.each(prev, function(val, key) {
tok.set(key, val);
});
if (HTdeclared) HealthColors.Update(tok, prevTok);
sendChat("COF", "État de " + tok.get("name") + " restauré.");
});
}
function boutonSimple(action, texte, style) {
action = action.replace(/%/g, '%').replace(/\)/g, ')').replace(/\?/g, '?').replace(/@/g, '@').replace(/\[/g, '[').replace(/]/g, ']').replace(/"/g, '"').replace(/{/g, '{').replace(/}/g, '}').replace(/\|/g, '|').replace(/\*/g, '*');
action = action.replace(/\'/g, '''); // escape quotes
action = action.replace(/:/g, ':'); // double escape colon
style = style || '';
return '' + texte + '';
}
// on, remplace tous les selected par @{character name|attr}
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
}
//Remplace une macro ou ability par sa définition (récursivement)
function replaceAction(action, perso, macros, abilities) {
let remplacement = false;
if (action.indexOf('#') >= 0) {
macros = macros || findObjs({
_type: 'macro'
});
macros.forEach(function(m, i) {
let mName = m.get('name');
if (mName === '') return;
mName = '#' + mName;
if (action.indexOf(mName) >= 0) {
mName = new RegExp(mName + "\\b", "g");
action = action.replace(mName, m.get('action'));
if (!remplacement)
macros = macros.filter(function(m, k) {
return (k != i);
}); //Pour éviter la récursion
remplacement = true;
}
});
}
if (action.indexOf('%') >= 0) {
abilities = abilities || findObjs({
_type: 'ability',
_characterid: perso.charId
});
abilities.forEach(function(a, i) {
let aName = a.get('name');
if (aName === '') return;
let daName = '%' + aName;
if (action.indexOf(daName) >= 0) {
action = action.replace(daName, a.get('action'));
if (!remplacement) abilities = abilities.splice(i); //Pour éviter la récursion
remplacement = true;
}
daName = '%{selected|' + aName + '}';
if (action.indexOf(daName) >= 0) {
action = action.replace(daName, a.get('action'));
if (!remplacement)
abilities = abilities.filter(function(m, k) {
return (k != i);
}); //Pour éviter la récursion
remplacement = true;
}
});
}
if (remplacement) return replaceAction(action, perso, macros, abilities);
return action;
}
function identifierArme(weaponStats, pred, nom, pattern) {
let p = pred[nom] ||
(weaponStats.name.search(pattern) > -1) ||
(weaponStats.modificateurs.search(pattern) > -1);
if (p) {
weaponStats[nom] = true;
}
}
function fieldAsString(obj, field, def) {
let res = obj[field];
if (res === undefined) return def;
return res;
}
function fieldAsInt(obj, field, def) {
let res = obj[field];
if (res === undefined) return def;
return toInt(res, def);
}
function armeDeCreatureFeerique(perso, weaponStats, dice) {
if (!predicateAsBool(perso, 'tropPetit')) return;
if (attributeAsBool(perso, 'grandeTaille')) return;
weaponStats.addNbDice = 1;
weaponStats.attDice = dice;
if (!predicateAsBool(perso, 'lutinGrosBill') || weaponStats.attCarBonus == '@{FOR}')
weaponStats.attCarBonus = '';
}
function weaponStatsOfAttack(perso, label, att) {
let weaponStats = {
label,
name: att.armenom,
attNbDices: fieldAsInt(att, 'armedmnbde', 1),
attDice: fieldAsInt(att, 'armedmde', 4),
crit: fieldAsInt(att, 'armecrit', 20),
divers: fieldAsString(att, 'armespec', ''),
portee: fieldAsInt(att, 'armeportee', 0),
typeAttaque: fieldAsString(att, 'armetypeattaque', 'Naturel'),
modificateurs: fieldAsString(att, 'armemodificateurs', ''),
typeDegats: fieldAsString(att, 'armetypedegats', 'tranchant'),
options: fieldAsString(att, 'armeoptions', ''),
predicats: fieldAsString(att, 'armepredicats', ''),
};
if (persoEstPNJ(perso)) {
weaponStats.attSkill = fieldAsInt(att, 'armeatk', 0);
weaponStats.attDMBonusCommun = fieldAsInt(att, 'armedm', 0);
} else {
weaponStats.attSkill = fieldAsString(att, 'armeatk', '@{ATKCAC}');
weaponStats.attSkillDiv = fieldAsInt(att, 'armeatkdiv', 0);
weaponStats.attCarBonus = fieldAsString(att, 'armedmcar', '@{FOR}');
weaponStats.attDMBonusCommun = fieldAsInt(att, 'armedmdiv', 0);
}
//On remplace les \n par des blancs pour l'affichage, sinon ça bug
weaponStats.options = weaponStats.options.replace(/\n/g, ' ').trim();
switch (weaponStats.typeAttaque) {
case 'Naturel':
weaponStats.armeNaturelle = true;
break;
case 'Arme 1 main':
weaponStats.arme = true;
armeDeCreatureFeerique(perso, weaponStats, 3);
break;
case 'Arme 2 mains':
weaponStats.arme = true;
weaponStats.deuxMains = true;
armeDeCreatureFeerique(perso, weaponStats, 4);
break;
case 'Sortilege':
weaponStats.sortilege = true;
break;
case 'Arme gauche':
weaponStats.armeGauche = true;
armeDeCreatureFeerique(perso, weaponStats, 3);
break;
case 'Arme de jet':
weaponStats.armeDeJet = true;
weaponStats.tauxDePerte = fieldAsInt(att, 'armejettaux', 0);
weaponStats.nbArmesDeJet = fieldAsInt(att, 'armejetqte', 1);
weaponStats.nbArmesDeJetMax = fieldAsInt(att, 'armejetqte_max', 1);
weaponStats.prefixe = att.prefixe; //pour trouver l'attribut
armeDeCreatureFeerique(perso, weaponStats, 3);
break;
default:
//On cherche si c'est une arme à 2 mains
//Ne devrait pas servir, on a toujours un type, maintenant
let t = weaponStats.name.toLowerCase();
if (t.includes('2 mains') || t.includes('deux mains')) {
weaponStats.deuxMains = true;
} else {
t = weaponStats.divers;
if (t) {
t = t.toLowerCase();
if (t.includes('2 mains') || t.includes('deux mains')) {
weaponStats.deuxMains = true;
}
}
}
}
//Informations dans le champ spécial
let champDivers = weaponStats.divers;
if (champDivers === '') champDivers = weaponStats.predicats;
else if (weaponStats.predicats !== '')
champDivers += '\n' + weaponStats.predicats;
let pred = predicateOfRaw(champDivers);
//On transfert les prédicats connus dans weaponStats
if (pred.charge) weaponStats.charge = toInt(pred.charge, 1);
if (pred.legere || (weaponStats.attNbDices <= 1 && weaponStats.attDice <= 6))
weaponStats.armeLegere = true;
weaponStats.eclaire = toInt(pred.eclaire);
weaponStats.eclaireFaible = toInt(pred.eclaireFaible);
weaponStats.batarde = pred.batarde;
if (weaponStats.batarde && weaponStats.deuxMains) {
error("L'arme " + weaponStats.name + " est déclarée comme batârde, il faudrait en faire une arme à une main par défaut", weaponStats);
weaponStats.deuxMains = undefined;
}
if (pred.armeDeGrand) {
weaponStats.armeDeGrand = true;
let taille = taillePersonnage(perso, 4);
if (taille < 5) weaponStats.deuxMains = true;
else weaponStats.deuxMains = false;
}
//Identification des catégories d'armes utilisées en jeu
identifierArme(weaponStats, pred, 'arc', /\barc\b/i);
identifierArme(weaponStats, pred, 'arbalete', /\barbal[eè]te\b/i);
identifierArme(weaponStats, pred, 'baton', /\bb[aâ]ton\b/i);
identifierArme(weaponStats, pred, 'hache', /\bhache\b/i);
identifierArme(weaponStats, pred, 'epee', /\b[eé]p[eé]e\b/i);
identifierArme(weaponStats, pred, 'epieu', /\b[eé]pieu\b/i);
identifierArme(weaponStats, pred, 'fronde', /\bfronde\b/i);
identifierArme(weaponStats, pred, 'marteau', /\bmarteau\b/i);
identifierArme(weaponStats, pred, 'masse', /\bmasse\b/i);
identifierArme(weaponStats, pred, 'rapiere', /\brapi[eè]re\b/i);
identifierArme(weaponStats, pred, 'poudre', /\bpoudre\b/i);
identifierArme(weaponStats, pred, 'sabre', /\b(katana|wakizachi|boken|demi-lame|vivelame|sabre)\b/i);
if (weaponStats.arc && predicateAsBool(perso, 'arcDeMaitre')) {
weaponStats.portee += 20;
}
if (weaponStats.poudre && predicateAsBool(perso, 'poudrePuissante')) {
weaponStats.portee += 10;
weaponStats.attDMBonusCommun += 2;
}
return weaponStats;
}
function depenseManaPossible(personnage, cout, msg) {
if (isNaN(cout) || cout === 0) return {
cout_nul: true
};
let token = personnage.token;
let charId = personnage.charId;
let manaAttr = findObjs({
_type: 'attribute',
_characterid: charId,
name: 'PM'
}, {
caseInsensitive: true
});
let hasMana = false;
if (manaAttr.length > 0) {
let manaMax = parseInt(manaAttr[0].get('max'));
hasMana = !isNaN(manaMax) && manaMax > 0;
}
if (hasMana) {
let bar2;
if (token) {
bar2 = parseInt(token.get('bar2_value'));
if (isNaN(bar2)) {
if (token.get('bar1_link') === '') bar2 = 0;
else { //devrait être lié à la mana courante
sendPerso(personnage, "*** Attention, la barre de mana du token n'est pas liée à la mana de la fiche ***");
bar2 = parseInt(manaAttr[0].get('current'));
}
}
} else bar2 = parseInt(manaAttr[0].get('current'));
msg = msg || '';
if ((reglesOptionelles.mana.val.contrecoup.val && bar2 <= 0) ||
(!reglesOptionelles.mana.val.contrecoup.val && !reglesOptionelles.mana.val.brulure_de_magie.val && bar2 < cout)) {
if (msg) sendPerso(personnage, "n'a pas assez de points de mana pour " + msg);
return false;
}
if (bar2 < cout && (reglesOptionelles.mana.val.contrecoup.val ||
reglesOptionelles.mana.val.brulure_de_magie.val)) {
let degats = cout - bar2;
if (reglesOptionelles.mana.val.brulure_de_magie.val) {
let famille = ficheAttribute(personnage, 'famille', 'aventurier').trim();
if (famille == "combattant") degats *= 2;
degats = Math.ceil(degats / predicateAsInt(personnage, 'coefPVMana', 1));
}
let bar1;
if (token) bar1 = parseInt(token.get('bar1_value'));
else bar1 = ficheAttributeAsInt(personnage, 'PV', 0);
if (bar1 < degats) {
if (msg) sendPerso(personnage, "n'a pas assez de points de mana et de PV pour " + msg);
return false;
}
return {
pm: 0,
depense_pm: bar2,
pv: bar1 - degats,
depense_pv: degats
};
}
return {
pm: bar2 - cout,
depense_pm: cout
};
}
sendPerso(personnage, "n'a pas de points de mana, action impossible");
return false;
}
//depasse est un string qui commence par --depasseLimite
//args doit être défini et contient des valeur à modifier :
// - mana
// - text
function peutDepasserLimite(depasse, perso, attrName, args) {
let depArg = depasse.split(' ', 3);
let step = 1;
if (depArg.length > 1) {
step = parseInt(depArg[1]);
if (isNaN(step) || step < 0) step = 1;
}
let d = attributeAsInt(perso, 'depasse' + attrName, 0) + step;
if (d < 1) return true;
let mana = d;
if (args.mana !== undefined) {
args.mana += d;
mana = args.mana;
}
if (depenseManaPossible(perso, mana)) {
if (args.text) args.text += " (+" + d + "PM)";
return true;
}
return false;
}
//options est un tableaux d'options obtenues par split(' --')
// peut retourner une struct avec champ extraText
function actionImpossible(perso, options, defResource, tref) {
let coutMana = 0;
let ai = options.some(function(opt) {
opt = opt.trim();
if (opt === '') return false;
const cmd = opt.split(' ');
switch (cmd[0]) {
case 'si':
let condition = parseCondition(cmd.slice(1));
switch (condition.type) {
case 'etat':
return !getState(perso, condition.etat);
case 'attribut':
if (condition.valeur === undefined)
return !attributeAsBool(perso, condition.attribute);
return testAttribut(perso, condition.attrbute, condition.valeur, condition);
case 'premiereAttaque':
return attributeAsBool(perso, 'attributDeCombat_premiereAttaque');
}
return false;
case 'mana':
if (cmd.length < 2) return false;
let mana = parseInt(cmd[1]);
if (isNaN(mana) || mana < 0) return false;
coutMana += mana;
return !depenseManaPossible(perso, coutMana) ||
attributeAsBool(perso, 'frappeDesArcanes');
case 'limiteParJour':
if (cmd.length < 2) return false;
let limiteParJour = parseInt(cmd[1]);
if (isNaN(limiteParJour)) {
limiteParJour = predicateAsInt(perso, cmd[1], 0, 1);
}
if (limiteParJour < 1) return false;
let ressourceParJour = defResource;
if (cmd.length > 2) {
cmd.splice(0, 2);
ressourceParJour = cmd.join('_');
}
ressourceParJour = 'limiteParJour_' + ressourceParJour;
if (attributeAsInt(perso, ressourceParJour, limiteParJour) > 0) {
return false;
}
//Reste le cas où on peut dépasser cette limite par jour
let depasse = options.find(function(o) {
return o.startsWith('depasseLimite ');
});
if (depasse) {
let da = {
mana: coutMana
};
if (tref) da.text = tref.text;
let dp = peutDepasserLimite(depasse, perso, ressourceParJour, da);
if (dp) {
coutMana = da.mana;
if (tref) tref.text = da.text;
return false;
}
return true;
}
return true;
case 'limiteParCombat':
case 'limiteParTour':
{
if (cmd.length < 2) return false;
let limite = parseInt(cmd[1]);
if (isNaN(limite)) {
limite = predicateAsInt(perso, cmd[1], 0, 1);
}
if (limite < 1) return false;
let lp = cmd[0];
let ressource = defResource;
if (cmd.length > 2) {
cmd.splice(0, 2);
ressource = cmd.join('_');
}
ressource = lp + '_' + ressource;
return attributeAsInt(perso, ressource, limite) === 0;
}
case 'tempsRecharge':
if (cmd.length < 2) return false;
let effet = cmd[1];
if (!estEffetTemp(effet)) return false;
return attributeAsBool(perso, effet);
case 'aussiArmeDeJet':
if (cmd.length < 2) return false;
let armeAssociee = getWeaponStats(perso, cmd[1]);
return armeAssociee && armeAssociee.armeDeJet && armeAssociee.nbArmesDeJet < 1;
case 'traquenard':
return attributeAsBool(perso, 'attributDeCombat_premiereAttaque');
}
return false;
});
return ai;
}
//Enlève les chaînes de type ?{..} pour être sûr que l'action est impossible
function removeUserInputs(act) {
let m = act.match(/\?\{[^\}]*\}/g);
if (!m) return act;
m.forEach(function(ma) {
act = act.replace(ma, '');
});
return act;
}
//Remplis les champs arme et armeGauche de perso
//renvoie undefined si aucune arme en main principale
//sinon renvoie l'arme principale
function armesEnMain(perso) {
if (perso.armesEnMain) return perso.arme;
let labelArmePrincipale = getLabelArme(perso, 'droite');
if (labelArmePrincipale)
perso.arme = getWeaponStats(perso, labelArmePrincipale);
let labelGauche = getLabelArme(perso, 'gauche');
if (typeof labelGauche == 'string' && labelGauche.startsWith('b')) {
if (perso.arme) perso.arme.deuxMains = undefined;
let attaqueBouclier = predicateAsBool(perso, 'attaqueAuBouclier');
if (attaqueBouclier)
perso.armeGauche = getWeaponStats(perso, attaqueBouclier);
} else if (labelGauche == '2m') {
if (perso.arme) perso.arme.deuxMains = true;
} else {
let labelArmeGauche = toInt(labelGauche, 0);
if (labelArmeGauche) {
perso.armeGauche = getWeaponStats(perso, labelArmeGauche);
}
}
perso.armesEnMain = 'calculee';
return perso.arme;
}
function listAllMunitions(perso) {
if (perso.munitions) return perso.munitions;
let rawList = extractRepeating(perso, 'munitions');
let liste = {}; //liste triée par label de munition
for (let pref in rawList) {
let ra = rawList[pref];
if (ra.labelmunition === undefined) ra.labelmunition = 0;
if (liste[ra.labelmunition]) {
error("Plusieurs munitions de label " + ra.labelmunitions, ra);
continue;
}
ra.prefixe = pref;
liste[ra.labelmunition] = ra;
}
perso.munitions = liste;
return liste;
}
//Les options de l'arme doivent déjà être dans act
function demandeMunition(perso, weaponStats, options, act) {
if (act.includes('--munition')) return act;
let typeMunition;
if (weaponStats.arc) typeMunition = 'Flèche';
else if (weaponStats.arbalete) typeMunition = 'Carreau';
else if (weaponStats.poudre) typeMunition = 'Balle';
else if (weaponStats.fronde) typeMunition = 'Autre'; //TODO: ajouter le type bille sur la fiche
if (!typeMunition) return act;
let munitions = listAllMunitions(perso);
let munitionsDeType = [];
for (let label in munitions) {
let munition = munitions[label];
let tm = fieldAsString(munition, 'typemunition', 'Flèche');
if (tm == typeMunition) {
let nb = fieldAsInt(munition, 'qtemunition', 1);
if (nb > 0) munitionsDeType.push(munition);
}
}
if (munitionsDeType.length === 0) return act;
let demande = ' ?{Munition|Normale, ';
munitionsDeType.forEach(function(m) {
demande += '|' +
fieldAsString(m, 'nommunition', typeMunition + ' ' + m.labelmunition) +
', --munition ' + m.labelmunition;
});
return act + demande + '}';
}
//arm doit être le résultat de getWeaponStats
function armeDechargee(perso, arme) {
if (!arme.charge) return false;
let currentCharge = attributeAsInt(perso, 'charge_' + arme.label, arme.charge);
return currentCharge === 0;
}
function armeChargeeDeGrenaille(perso, arme) {
if (!arme.charge || !arme.poudre) return false;
let currentCharge = attributeAsInt(perso, 'chargeGrenaille_' + arme.label, 0);
return currentCharge > 0;
}
//options peut avoir les champs:
// - ressource, un attribut
// - overlay
// - buttonStyle
// - attackStats
// et la fonction peut écrire actionImpossible = true dans options.
function bouton(action, text, perso, options) {
if (action === undefined || action.trim() === '') return text;
else action = action.trim();
options = options || {};
//Expansion des macros et abilities
action = replaceAction(action, perso);
const tid = perso.token.id;
if (perso.name === undefined) {
const character = getObj('character', perso.charId);
if (character) perso.name = character.get('name');
else perso.name = nomPerso(perso);
}
//Cas de plusieurs actions après expansion
let actions = action.split('\n');
//Cherche le picto et le style
let style = '';
let picto = '';
let groupe; //Pour générer un bouton d'attaque de groupe. À revoir
actions = actions.map(function(act) {
act = act.trim();
if (act.startsWith("/as ")) {
act = "!cof-as" + act.substring(3);
}
if (act.charAt(0) == '!') {
if (act.startsWith('!cof-')) {
let actSansChoix = removeUserInputs(act);
const args = actSansChoix.split(' --');
let defRessource = '';
if (act.startsWith('!cof-guerison ')) defRessource = 'guérison';
let dai = {
text
};
if (actionImpossible(perso, args, defRessource, dai))
options.actionImpossible = true;
else text = dai.text;
if (!options.actionImpossible) {
if (act.startsWith('!cof-soin ') && !actSansChoix.includes('--limitePar') && !actSansChoix.includes('--dose')) { //Limitations spéficiques
let rangSoin = predicateAsInt(perso, 'voieDesSoins', 0);
let cmd = args[0].split(' ');
if (cmd.includes('leger')) {
let soinsLegers = attributeAsInt(perso, 'soinsLegers', 0);
if (soinsLegers >= rangSoin) {
//Peut-être qu'on peut encore dépasser la limite
let depasse = actSansChoix.indexOf('--depasseLimite ');
if (depasse > 0) {
let dp = {
text
};
let pdl =
peutDepasserLimite(actSansChoix.substring(depasse), perso, 'soinsLegers', dp);
if (pdl) text = dp.text;
else options.actionImpossible = true;
} else options.actionImpossible = true;
}
} else if (cmd.includes('modere')) {
let soinsModeres = attributeAsInt(perso, 'soinsModeres', 0);
if (soinsModeres >= rangSoin) {
//Peut-être qu'on peut encore dépasser la limite
let depasse = actSansChoix.indexOf('--depasseLimite ');
if (depasse > 0) {
let dp = {
text
};
let pdl =
peutDepasserLimite(actSansChoix.substring(depasse), perso, 'soinsModeres', dp);
if (pdl) text = dp.text;
else options.actionImpossible = true;
} else options.actionImpossible = true;
}
}
} else if (act.startsWith('!cof-recharger ')) {
let cmd = act.split(' ');
if (cmd.length > 1) {
let attackLabel = cmd[1];
let arme = getWeaponStats(perso, attackLabel);
if (arme !== undefined && arme.charge) {
let currentCharge = attributeAsInt(perso, 'charge_' + arme.label, arme.charge);
if (currentCharge >= arme.charge)
options.actionImpossible = true;
}
}
} else if (act.startsWith('!cof-attack ')) {
if (paralyseParRoublard(perso, false)) {
options.actionImpossible = true;
} else {
let cmd = act.split(' ');
if (cmd.length > 3 && cmd[3] == '-1') {
//Selon l'arme en main, une action peut être possible ou non
let weaponStats = armesEnMain(perso);
if (weaponStats) {
options.attackStats = options.attackStats || weaponStats;
options.actionImpossible =
(weaponStats.deuxMains && attributeAsBool(perso, 'espaceExigu')) ||
(weaponStats.portee &&
(cmd.includes('--attaqueFlamboyante') || cmd.includes('--seulementContact'))) ||
(!cmd.includes('--semonce') && !cmd.includes('--tirDeBarrage') && armeDechargee(perso, weaponStats)) ||
(cmd.includes('--semonce') && attributeAsInt(perso, 'attaqueADistanceRatee', 0) != 1) ||
(cmd.includes('--ricochets') && !(weaponStats.armeDeJet || weaponStats.options.includes('--aussiArmeDeJet')));
}
}
options.actionImpossible = options.actionImpossible ||
(actSansChoix.includes(' --frappeDesArcanes') && attributeAsBool(perso, 'frappeDesArcanes'));
}
}
}
if (options.ressource) act += " --decrAttribute " + options.ressource.id;
if (picto === '') {
// Pictos : https://wiki.roll20.net/CSS_Wizardry#Pictos
let typeAction = act.split(' ', 1)[0].substring(5);
switch (typeAction) {
case 'attack':
case 'attaque':
case 'confirmer-attaque':
case 'explosion':
let portee = 0;
let sortilege;
let cmd = args.shift().split(' ');
let attackStats = options.attackStats;
if (attackStats === undefined) {
let attackLabel;
if (typeAction == 'explosion' && cmd.length > 1) {
attackLabel = cmd[1].trim();
} else if (cmd.length > 3) {
attackLabel = cmd[3].trim();
}
if (attackLabel && !attackLabel.startsWith('?{')) {
attackStats = getWeaponStats(perso, attackLabel);
}
}
if (attackStats) {
portee = attackStats.portee;
sortilege = attackStats.sortilege;
if (attackStats.options) {
let firstOptionIndex = act.indexOf(' --');
if (firstOptionIndex > 0) {
act = act.substring(0, firstOptionIndex) + ' --attaqueOptions ' + attackStats.options + act.substring(firstOptionIndex);
} else {
act += ' --attaqueOptions ' + attackStats.options;
}
}
act = demandeMunition(perso, attackStats, options, act);
}
//On cherche la portée dans les options (ça a la priorité)
args.forEach(function(o) {
if (o.startsWith('portee ')) {
let p = parseInt(o.substring(7));
if (!isNaN(p) && p >= 0) portee = p;
}
});
if (sortilege || act.indexOf(' --sortilege') !== -1) {
// attaque magique
picto = 'g ';
style = 'background-color:#9900ff';
} else if (portee > 0) {
// attaque distance
picto = '[ ';
style = 'background-color:#48b92c';
} else {
// attaque contact
picto = 't ';
style = 'background-color:#cc0000';
}
break;
case 'lancer-sort':
case 'injonction':
case 'injonction-mortelle':
case 'attaque-magique':
case 'tueur-fantasmagorique':
case 'mot-de-pouvoir-immobilise':
case 'animation-des-objets':
case 'sphere-de-feu':
case 'immunite-guerisseur':
case 'enkystement-lointain':
picto = 'g ';
style = 'background-color:#9900ff';
break;
case 'soin':
case 'transe-guerison':
case 'delivrance':
case 'guerir':
case 'guerison':
case 'consommer-baie':
case 'zone-de-vie':
picto = 'k ';
style = 'background-color:#ffe599;color:#333';
break;
case 'effet':
case 'effet-temp':
case 'effet-combat':
case 'set-state':
case 'lumiere':
if (act.includes(' --mana')) {
picto = 'g ';
style = 'background-color:#9900ff';
} else {
picto = 'S ';
style = 'background-color:#4a86e8';
}
break;
case 'fortifiant':
picto = 'S ';
style = 'background-color:#4a86e8';
break;
case 'enduire-poison':
picto = 'i ';
style = 'background-color:#05461c';
break;
case 'desarmer':
picto = 't ';
style = 'background-color:#cc0000';
break;
case 'surprise':
picto = 'e ';
style = 'background-color:#4a86e8';
break;
case 'recharger':
picto = '0 ';
style = 'background-color:#e69138';
break;
case 'action-defensive':
picto = 'b ';
style = 'background-color:#cc0000';
break;
case 'manoeuvre':
picto = 'd ';
style = 'background-color:#cc0000';
break;
case 'attendre':
case 'tour-suivant':
picto = 't ';
style = 'background-color:#999999';
break;
case 'dmg':
case 'bouton-echec-total':
picto = '\' ';
style = 'background-color:#cc0000';
break;
case 'peur':
picto = '` ';
style = 'background-color:#B445FE';
break;
case 'consommables':
picto = 'b ';
style = 'background-color:#ce0f69';
break;
case 'liste-actions':
picto = 'l ';
style = 'background-color:#272751';
break;
}
}
} else if (!act.startsWith('!
')) return act; //On ne touche pas aux commandes des autres scripts
} else {
if (options.ressource) {
act = "!cof-utilise-consommable " + tid + ' ' + options.ressource.id + ' ' + act;
picto = 'b ';
style = 'background-color:#ce0f69';
} else {
act = "!cof-lancer-sort " + act;
picto = 'g ';
style = 'background-color:#9900ff';
}
}
if (act.indexOf('@{selected') !== -1) {
// cas spécial pour @{selected|token_id} où l'on remplace toutes les occurences par token.id
act = act.replace(new RegExp(escapeRegExp('@{selected|token_id}'), 'g'), tid);
act = act.replace(new RegExp(escapeRegExp('@{selected|token_name}'), 'g'), nomPerso(perso));
let tmp = act.split('@{selected');
tmp.forEach(function(elem) {
if (elem.startsWith('|')) {
// attribut demandé
let attribute_name = elem.substring(0, elem.indexOf("}")).substr(1);
let carac = caracOfMod(attribute_name);
let replacement;
if (carac) {
replacement = modCarac(perso, carac);
} else {
let attrs = findObjs({
_type: 'attribute',
_characterid: perso.charId,
name: attribute_name
});
if (attrs.length === 0)
replacement = '@{' + perso.name + '|' + attribute_name + '}';
else
replacement = attrs[0].get('current');
}
act = act.replace(new RegExp(escapeRegExp('@{selected|' + attribute_name + '}'), 'g'), replacement);
}
});
}
if ((act.startsWith('!cof-lancer-sort') || act.startsWith('!cof-immunite-guerisseur')) &&
act.indexOf('--lanceur') == -1) {
act += " --lanceur " + tid;
}
if (act.indexOf('@{target|') == -1 &&
act.indexOf('cof-lancer-sort') == -1 &&
act.indexOf('cof-surprise') == -1 &&
act.indexOf('cof-attack') == -1 &&
act.indexOf('cof-soin') == -1 &&
act.indexOf('cof-guerison') == -1 &&
act.indexOf('cof-as ') == -1 &&
act.indexOf('cof-jouer-son ') == -1 &&
act.indexOf('cof-utilise-consommable ') == -1 &&
act.indexOf('--equipe') == -1 &&
act.indexOf('--enVue') == -1 &&
act.indexOf('--disque') == -1 &&
act.indexOf('--target ' + tid) == -1) {
//Si on n'a pas de cible, on fait comme si le token était sélectionné.
let add_token = " --target " + tid;
if (act.indexOf(' --allie') >= 0) {
if (act.indexOf('--lanceur') == -1)
add_token = " --lanceur " + tid;
else add_token = ""; //La cible sont les alliés de --lanceur.
}
if (act.indexOf(' --message ') != -1) act = act.replace(' --message ', add_token + ' --message ');
else act += add_token;
}
return act;
});
text = picto + text;
let buttonStyle = '';
if (options.buttonStyle) buttonStyle = ' style="' + options.buttonStyle + '"';
else if (style !== '') buttonStyle = ' style="' + style + '"';
let overlay = '';
if (options.overlay) overlay = ' title="' + options.overlay + '"';
if (actions.length == 1) {
action = actions[0];
let toReturn = boutonSimple(action, text, buttonStyle + overlay);
if (groupe) {
toReturn += "
" + boutonSimple(action + " --attaqueDeGroupe ?{Attaque en groupe ?}", text + " (groupe)", buttonStyle + overlay);
}
return toReturn;
} else {
action = "!cof-multi-command " + actions.join(' --cof-multi-command ');
return boutonSimple(action, text, buttonStyle + overlay);
}
}
function improve_image(image_url) {
if (image_url) {
image_url = image_url.replace('/med.png', '/thumb.png');
image_url = image_url.replace('/max.png', '/thumb.png');
let index = image_url.indexOf('?');
if (index > 0) image_url = image_url.substring(0, index);
return image_url;
}
}
//Fonction séparée pour pouvoir envoyer un frame à plusieurs joueurs
// playerId peut être undefined (en particulier pour envoyer au mj)
function addFramedHeader(display, playerId, chuchote) {
let perso1 = display.perso1;
let perso2 = display.perso2;
let action = display.action;
let playerBGColor = '#333';
let playerTXColor = '#FFF';
let displayname;
let player;
if (playerId) player = getObj('player', playerId);
if (player !== undefined) {
playerBGColor = player.get("color");
playerTXColor = (getBrightness(playerBGColor) < 50) ? "#FFF" : "#000";
displayname = player.get('displayname');
}
let res = '/direct ';
if (chuchote) {
let who;
if (chuchote !== true) who = chuchote;
else who = displayname;
if (who) res = '/w "' + who + '" ';
else chuchote = false;
}
let name1, name2 = '';
let avatar1, avatar2;
if (perso2) {
let img2;
if (stateCOF.options.affichage.val.avatar_dans_cadres.val || !perso2.token) {
let character2 = getObj('character', perso2.charId);
if (character2) img2 = improve_image(character2.get('avatar'));
} else img2 = improve_image(perso2.token.get('imgsrc'));
if (img2) {
avatar2 = '';
name2 = '' + nomPerso(perso2) + '';
}
}
if (perso1) {
let img1;
if (stateCOF.options.affichage.val.avatar_dans_cadres.val || !perso1.token) {
let character1 = getObj('character', perso1.charId);
if (character1) img1 = improve_image(character1.get('avatar'));
} else img1 = improve_image(perso1.token.get('imgsrc'));
if (img1) {
avatar1 = '';
name1 = '' + nomPerso(perso1) + '';
}
}
res +=
'
' + name1 + ' | ' + '' + 'VS' + ' | ' + '' + name2 + ' | ' + '
' + avatar1 + ' | ' + '' + ' | ' + avatar2 + ' | ' + '
' + avatar1 + ' | ' + '' +
' ' + name1 + ' ' +
'' +
' ' +
'' + bar1_info + ' ' +
'' + bar2_info + ' ' +
'' + bar3_info + ' ' +
' | ' +
'
' + action + ' | ' + display.action_right + ' |
')) note = note.substring(3);
note = note.trim().replace(/]*>|<\/span>/g, '');
note = note.replace(/ /g, ' ')) note = note.substring(3);
note = note.trim().replace(/]*>|<\/span>/g, '');
note = note.replace(/ /g, '
');
note = note.replace(/<\/p>/g, '');
return note.trim().split('
');
}
function charactersInHandout(note, nomEquipe) {
let names = linesOfNote(note);
let persos = new Set();
let characters = findObjs({
_type: 'character',
});
names.forEach(function(name) {
name = name.replace(/<(?:.|\s)*?>/g, ''); //Pour enlever les , etc
name = name.trim();
if (name.length === 0) return;
let charsWithName = characters.filter(function(c) {
return c.get('name').trim() == name;
});
if (charsWithName.length === 0) {
log(name + " dans l'équipe " + nomEquipe + " est inconnu");
return;
}
if (charsWithName.length > 1) {
let nonArch = charsWithName.filter(function(c) {
return !(c.get('archived'));
});
if (nonArch.length > 0) charsWithName = nonArch;
if (charsWithName.length > 1) {
log(name + " dans l'équipe " + nomEquipe + " est en double");
}
}
charsWithName.forEach(function(character) {
persos.add(character.id);
});
});
return persos;
}
function parseHandout(hand) {
const handName = hand.get('name').trim();
if (handName.startsWith("Equipe ")) {
hand.get('notes', function(note) { // asynchronous
const persos = charactersInHandout(note, handName);
let attaqueEnMeute = false;
persos.forEach(function(charId) {
attaqueEnMeute = attaqueEnMeute || charPredicateAsBool(charId, 'attaqueEnMeute');
let ancien = alliesParPerso[charId];
if (ancien === undefined) {
ancien = new Set();
alliesParPerso[charId] = ancien;
}
persos.forEach(function(aci) {
if (aci == charId) return;
ancien.add(aci);
});
//On ajoute les familiers
});
if (attaqueEnMeute) {
persos.forEach(function(charId) {
alliesDAttaqueEnMeute.add(charId);
});
}
}); //end hand.get('notes')
} else if (handName == 'Compétences' || handName == 'Competences') {
listeCompetences = {
FOR: {
list: [],
elts: new Set()
},
DEX: {
list: [],
elts: new Set()
},
CON: {
list: [],
elts: new Set()
},
SAG: {
list: [],
elts: new Set()
},
INT: {
list: [],
elts: new Set()
},
CHA: {
list: [],
elts: new Set()
},
nombre: 0
};
hand.get('notes', function(note) { // asynchronous
let carac; //La carac dont on spécifie les compétences actuellement
let lignes = linesOfNote(note);
lignes.forEach(function(ligne) {
ligne = ligne.trim();
let header = ligne.split(':');
if (header.length > 1) {
let c = header.shift().trim().toUpperCase();
if (!isCarac(c)) return;
carac = c;
ligne = header.join(':').trim();
}
if (ligne.length === 0) return;
if (carac === undefined) {
error("Compétences sans caractéristique associée", note);
return;
}
let comps = ligne.split(/, |\/| /);
comps.forEach(function(comp) {
if (comp.length === 0) return;
comp = comp.replace(/_/g, ' ');
listeCompetences[carac].list.push(comp);
listeCompetences.nombre++;
listeCompetences[carac].elts.add(comp.toLowerCase());
});
});
}); //end hand.get(notes)
}
}
function estAllieJoueur(perso) {
if (estControlleParJoueur(perso.charId)) return true;
let allies = alliesParPerso[perso.charId];
if (allies === undefined) return false;
let res = false;
allies.forEach(function(p) {
res = res || estControlleParJoueur(p);
});
return res;
}
function estArme(attaque) {
let t = fieldAsString(attaque, 'armetypeattaque', 'Naturel');
if (t.startsWith('Arme')) {
return t != 'Arme de jet';
}
return false;
}
//options peut contenir:
// - ligneOptions : une chaîne de caractères à ajouter aux attaques
// - target : l'id de la cible des attaques
// - nePasAfficherArmes : quand on affiche plus tard l'arme en main
function listeAttaquesVisibles(perso, options) {
options = options || {};
let ligneOptions = options.ligneOptions || '';
let target = options.target || '@{target|token_id}';
let ligne = '';
//Cherche toutes les attaques à afficher
let attaques = listAllAttacks(perso);
let attaquesTriees = [];
let attaquesNonTriees = {};
for (let attLabel in attaques) {
let att = attaques[attLabel];
if (fieldAsInt(att, 'armeactionvisible', 1) === 0) continue;
if (options.nePasAfficherArmes && estArme(att)) continue;
//Vérification que des options n'empêchent pas l'utilisation de l'attaque
let attackOptions = ' ' + fieldAsString(att, 'armeoptions', '');
if (actionImpossible(perso, attackOptions.split(' --'), attLabel)) continue;
//On regarde aussi si c'est une arme de jet
if (att.armetypeattaque == 'Arme de jet' && fieldAsInt(att, 'armejetqte', 1) === 0) continue;
let command = "!cof-attack @{selected|token_id} " + target + " " + attLabel + " " + ligneOptions;
let index = +attLabel;
if (isNaN(index) || parseInt(index) != index || index < 0)
attaquesNonTriees[attLabel] = bouton(command, att.armenom, perso);
else
attaquesTriees[attLabel] = bouton(command, att.armenom, perso);
}
attaquesTriees.forEach(function(b) {
if (b === undefined) return;
ligne += b + '
';
if (er.actif) {
ligne = ' portee) {
sendPlayer(msg, nomPerso(perso) + " est trop loin.", playerId);
return;
}
let message = messageEffetTemp.immuniteAmaladie;
let ef = {
effet: 'immuniteAmaladie',
duree: true,
message,
whisper: true,
};
setEffetTemporaire(perso, ef, duree, evt, options);
ef.effet = 'immuniteApoison';
ef.message = messageEffetTemp.immuniteApoison;
setEffetTemporaire(perso, ef, duree, evt, options);
sendPerso(perso, "est maintenant immunisé" + eForFemale(perso) + " au poison et aux maladies");
});
});
}
//!cof-zone-de-vie duree
function lancerZoneDeVie(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Problème de parse options", msg.content);
return;
}
if (cmd.length < 2) {
error("Il manque un argument à !cof-zone-de-vie", cmd);
return;
}
let duree = parseInt(cmd[1]);
if (isNaN(duree) || duree <= 0) {
error("La zone de vie a une durée incorrecte", cmd);
return;
}
let pageId = options.pageId;
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
sendPlayer(msg, "Utilisation de !cof-zone-de-vie sans sélection de token", playerId);
return;
}
const evt = {
type: 'zone de vie'
};
addEvent(evt);
if (options.lanceur &&
limiteRessources(options.lanceur, options, 'zoneDeVie', "lancer une zone de vie", evt)) return;
initiative(selected, evt);
iterSelected(selected, function(perso) {
let n = 1; //Le numéro de zone de vie, s'il y en a plusieurs en même temps
let attrName = 'zoneDeVie(' + n + ')';
while (attributeAsBool(perso, attrName)) {
n++;
attrName = 'zoneDeVie(' + n + ')';
}
let scale = computeScale(pageId);
let diametre = 20;
if (options.portee) diametre = 2 * options.portee;
diametre = diametre / scale;
let diamPix = PIX_PER_UNIT * diametre;
let imageFields = {
_pageid: pageId,
imgsrc: stateCOF.options.images.val.zone_de_vie.val,
represents: '',
left: perso.token.get('left'),
top: perso.token.get('top'),
width: diamPix,
height: diamPix,
layer: 'map',
name: "Zone de vie " + n,
isdrawing: true,
emits_low_light: true,
low_light_distance: diametre / 2,
};
let newImage = createObj('graphic', imageFields);
if (newImage) {
evt.tokens = evt.tokens || [];
evt.tokens.push(newImage);
toFront(newImage);
setTokenAttr(perso, attrName + 'Id', newImage.id, evt);
setAttrDuree(perso, attrName, duree, evt);
} else {
error("Impossible de créer l'image " + options.image, imageFields);
}
});
}, options);
}
//Pour effacer tout ce qui pourrait faire planter et stoqué dans stateCOF
function cleanGlobalState(msg) {
stateCOF = stateCOF || state.COFantasy;
if (!stateCOF) {
sendPlayer(msg, "État global de COF déjà vide");
return;
}
try {
removeRoundMarker();
} catch (e) {
stateCOF.roundMarkerId = undefined;
}
stateCOF.roundMarkerId = undefined;
if (stateCOF.combat) {
try {
sortirDuCombat();
} catch (e) {
stateCOF.combat = undefined;
}
}
stateCOF.combat = undefined;
stateCOF.chargeFantastique = undefined;
stateCOF.tokensTemps = undefined;
stateCOF.effetAuD20 = undefined;
stateCOF.tenebresMagiques = undefined;
stateCOF.jetsEnCours = undefined;
stateCOF.currentAttackDisplay = undefined;
stateCOF.prescience = undefined;
stateCOF.nextPrescience = undefined;
stateCOF.afterDisplay = undefined;
stateCOF.statistiquesEnPause = undefined;
stateCOF.statistiques = undefined;
log("stateCOf purgé");
log(stateCOF);
sendPlayer(msg, "État global de COFantasy purgé.");
}
//!cof-poser-bombe token_id [demolition n | piege dm [retardateur|intrusion]]
function poserBombe(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Problème de parse options", msg.content);
return;
}
if (cmd.length < 3) {
error("Il manque des arguments pour !cof-poser-bombe", cmd);
return;
}
let pageId = options.pageId;
let arquebusier = persoOfId(cmd[1], cmd[1], pageId);
if (!arquebusier) {
error("Le premier argument de !cof-poser-bombe n'est pas un token valide", cmd);
return;
}
pageId = pageId || arquebusier.token.get('pageid');
let typeBombe;
let portee; //la portée de l'explosion
let message = "La bombe explose";
let dm; //les dégâts infligés, sous forme de nbDe, dice, bonus, id
let tempsDePose;
let duree = 1; //temps au bout duquel ça explose après la pose
let intrusion; //si positif, se déclenche s'il y a quelqu'un qui arrive à moins de cette distance de la bombe.
switch (cmd[2]) {
case 'demolition':
case 'démolition':
{
typeBombe = 'demolition';
let rang = 2;
if (cmd.length > 3) {
rang = parseInt(cmd[3]);
if (isNaN(rang))
rang = predicateAsInt(arquebusier, cmd[3], 0, 2);
if (rang < 1) {
error("Rang de démolition " + cmd[3] + " incorrect", cmd);
return;
}
} else rang = predicateAsInt(arquebusier, 'voieDesExplosifs', 2, 2);
portee = 6;
dm = rang + 'd6';
let dmStruct = rollDePlus(6, {
nbDes: 2 * rang
});
message += " et inflige " + dmStruct.roll + " DM à la structure (ignore la moitié de la RD)";
tempsDePose = 3;
break;
}
case 'piege':
case 'piège':
{
typeBombe = 'piege';
if (cmd.length < 5) {
error("Il manque des arguments pour !cof-poser-bombe", cmd);
return;
}
dm = cmd[3];
portee = 3;
switch (cmd[4]) {
case 'retard':
case 'retardé':
tempsDePose = 1;
if (cmd.length > 5) {
duree = parseInt(cmd[5]);
if (isNaN(duree) || duree < 1) {
error("Durée de retardement " + cmd[5] + " incorrecte");
return;
}
}
break;
case 'intrusion':
case 'detection':
case 'détection':
tempsDePose = 2;
intrusion = 1;
duree = 100;
if (cmd.length > 5) {
intrusion = parseInt(cmd[5]);
if (isNaN(intrusion) || intrusion < 1) {
error("Distance de détection " + cmd[5] + " incorrecte");
return;
}
}
//On converti la distance d'intrusion en pixels
let scale = computeScale(pageId);
intrusion = (intrusion / scale) * PIX_PER_UNIT;
break;
default:
error("Type de piège explosif " + cmd[4] + " non reconnu", cmd);
return;
}
break;
}
default:
error("Type de bombe " + cmd[2] + " non reconnu", cmd);
return;
}
let gmnotes = JSON.stringify({
typeBombe,
portee,
message,
dm,
tempsDePose,
duree,
intrusion
});
let name = "Bombe " + generateUUID();
const evt = {
type: "Pose de bombe"
};
addEvent(evt);
if (limiteRessources(arquebusier, options, 'poseBombe', "poser un explosif", evt)) return;
//create the token
let t = createObj('graphic', {
_pageid: pageId,
imgsrc: IMG_BOMB,
represents: '',
left: arquebusier.token.get('left'),
top: arquebusier.token.get('top'),
width: PIX_PER_UNIT,
height: PIX_PER_UNIT * 0.8,
layer: 'map',
isDrawing: true,
name,
gmnotes
});
if (!t) {
error("Impossible de créer le token de la bombe", IMG_BOMB);
return;
}
toFront(t);
evt.tokens = evt.tokens || [];
evt.tokens.push(t);
stateCOF.tokensTemps = stateCOF.tokensTemps || [];
stateCOF.tokensTemps.push({
tid: t.id,
duree: duree + tempsDePose,
name,
init: getInit(),
intrusion
});
//TODO: ajouter un effet temporaire "occupé"
let msgPose = "pose un explosif. ";
if (tempsDePose < 2) msgPose += "Cela lui prend tout le tour";
else {
msgPose += onGenre(arquebusier, 'Il', 'Elle') + " y passe le tour et encore le";
if (tempsDePose < 3) msgPose += " suivant.";
else msgPose += "s " + (tempsDePose - 1) + " suivants.";
}
sendPerso(arquebusier, msgPose);
}
//!cof-mettre-casque label, label -1 pour remettre le casque par défaut, 0 pour enlever
function mettreCasque(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Problème de parse options", msg.content);
return;
}
let label = -1;
if (cmd.length > 1) label = toInt(cmd[1], -1);
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
sendPlayer(msg, "Personne pour mettre son casque", playerId);
return;
}
const evt = {
type: 'mettre casque'
};
addEvent(evt);
iterSelected(selected, function(perso) {
if (label === 0) {
setFicheAttr(perso, 'teteequipe', '0', evt);
sendPerso(perso, "enlève son casque", options.secret);
purgeCachePredicats(perso);
return;
}
let labelPerso = label;
if (labelPerso == -1)
labelPerso = toInt(ficheAttributeMax(perso, 'teteequipe', -1), -1);
//Si le label est >0 on vérifie qu'il existe
if (labelPerso > 0) {
let armures = listAllArmors(perso);
let casque = armures[labelPerso];
if (!casque) {
sendPlayer(msg, nomPerso(perso) + " n'a pas d'armure de label " + labelPerso, playerId);
return;
}
if (casque.typearmure != 'Casque') {
sendPlayer(msg, "L'armure de label " + labelPerso + " de " + nomPerso(perso) + " n'est pas un casque.", playerId);
return;
}
sendPerso(perso, "met " + casque.nomarmure, options.secret);
} else {
sendPerso(perso, 'met son casque', options.secret);
setFicheAttr(perso, 'casque_on', '1', evt);
}
purgeCachePredicats(perso);
let opt = {};
let current = ficheAttributeAsInt(perso, 'teteequipe', 0);
if (current) opt.maxVal = current;
setFicheAttr(perso, 'teteequipe', labelPerso, evt, opt);
});
});
}
function apiCommand(msg) {
msg.content = msg.content.replace(/\s+/g, ' '); //remove duplicate whites
const command = msg.content.split(' ', 1);
// First replace inline rolls by their values
replaceInline(msg);
switch (command[0]) {
case '!cof-affaiblir-carac':
parseAffaiblirCarac(msg);
return;
case '!cof-agrandir-page':
agrandirPage(msg);
return;
case '!cof-animation-des-objets':
animationDesObjets(msg);
return;
case '!cof-armure-magique':
armureMagique(msg);
return;
case '!cof-attack':
parseAttack(msg);
return;
case '!cof-attack-line':
attaqueLigneBouger(msg);
return;
case '!cof-attack-line-from': //seulement utilisé en interne
attaqueLigne(msg);
return;
case '!cof-attendre':
attendreInit(msg);
return;
case '!cof-bouger':
decoincer(msg);
return;
case '!cof-bouton-chance':
boutonChance(msg);
return;
case '!cof-bouton-pousser-kiai':
kiai(msg);
return;
case '!cof-buf-def':
bufDef(msg);
return;
case '!cof-canaliser':
canaliser(msg);
return;
case '!cof-clean-global-state':
cleanGlobalState(msg);
return;
case '!cof-confirmer-attaque':
confirmerAttaque(msg);
return;
case '!cof-creer-baies':
creerBaies(msg);
return;
case '!cof-degainer':
parseDegainer(msg);
return;
case '!cof-dmg':
parseDmgDirects(msg);
return;
case '!cof-effet-chaque-d20':
setEffetChaqueD20(msg);
return;
case '!cof-expert-combat':
case '!cof-expert-combat-touche':
case '!cof-expert-combat-dm':
expertDuCombat(msg);
return;
case '!cof-expert-combat-def':
expertDuCombatDEF(msg);
return;
case '!cof-expert-combat-bousculer':
expertDuCombatBousculer(msg);
return;
case '!cof-explosion':
attaqueExplosion(msg);
return;
case '!cof-hors-combat': //ancienne syntaxe, plus documentée
case '!cof-fin-combat':
sortirDuCombat();
return;
case '!cof-fin-reaction-violente':
finReactionViolente(msg);
return;
case '!cof-foudre-du-temps': //ancienne syntaxe, plus documentée
if (!msg.content) return;
let i = msg.content.indexOf(' ');
if (i < 0) return;
msg.content = msg.content.substr(0, i) + ' foudresDuTemps' + msg.content.substr(i);
setEffetChaqueD20(msg);
return;
case '!cof-gerer-runes-mortes':
gererRunesMortes(msg);
return;
case '!cof-huile-instable':
huileInstable(msg);
return;
case '!cof-immunite-guerisseur':
immuniteDuGuerisseur(msg);
return;
case '!cof-init':
initiativeInterface(msg);
return;
case '!cof-jet':
jet(msg);
return;
case '!cof-liste-actions':
apiTurnAction(msg);
return;
case '!cof-mettre-a-zero-pv':
interfaceMettreAZeroPV(msg);
return;
case '!cof-mettre-casque':
mettreCasque(msg);
return;
case '!cof-montrer-resultats-attaque':
montrerResultatsAttaque(msg);
return;
case '!cof-montrer-resultats-jet':
montrerResultatJet(msg);
return;
case '!cof-nouveau-jour':
parseNouveauJour(msg);
return;
case '!cof-open-door':
openDoor(msg);
return;
case '!cof-options':
setCofOptions(msg);
return;
case '!cof-pathfinder1':
translateFromPathfinder1(msg);
return;
case '!cof-pause':
pauseGame();
return;
case '!cof-poser-bombe':
poserBombe(msg);
return;
case '!cof-recharger':
recharger(msg);
return;
case '!cof-recuperation':
parseRecuperer(msg);
return;
case '!cof-remove-buf-def':
removeBufDef(msg);
return;
case '!cof-resultat-jet':
resultatJet(msg);
return;
case '!cof-retour-boomerang':
retourBoomerang(msg);
return;
case "!cof-rune-protection":
runeProtection(msg);
return;
case "!cof-rune-puissance":
case "!cof-bouton-rune-puissance":
runePuissance(msg);
return;
case "!cof-rune-energie":
case "!cof-bouton-rune-energie":
runeEnergie(msg);
return;
case '!cof-sentir-la-corruption':
parseSentirLaCorruption(msg);
return;
case '!cof-set-state':
parseSetState(msg);
return;
case '!cof-skip-attack':
skipAttack(msg);
return;
case '!cof-soigner-affaiblissement':
soignerAffaiblissement(msg);
return;
case '!cof-sphere-de-feu':
sphereDeFeu(msg);
return;
case '!cof-save-effet':
parseSaveEffet(msg);
return;
case '!cof-save-state':
parseSaveState(msg);
return;
case '!cof-statut':
statut(msg);
return;
case '!cof-surprise':
parseSurprise(msg);
return;
case '!cof-tenebres-magiques':
tenebresMagiques(msg);
return;
case '!cof-undo':
undoEvent();
return;
case '!cof-vision-nocturne':
ajouterVisionNocturne(msg);
return;
case '!cof-zone-de-vie':
lancerZoneDeVie(msg);
return;
case "!cof-echange-init":
echangeInit(msg);
return;
case "!cof-a-couvert":
aCouvert(msg);
return;
case "!cof-bonus-couvert":
bonusCouvert(msg);
return;
case "!cof-effet-temp":
parseEffetTemporaire(msg);
return;
case "!cof-effet-combat":
effetCombat(msg);
return;
case "!cof-effet":
parseEffetIndetermine(msg);
return;
case "!cof-fin-classe-effet":
finClasseDEffet(msg);
return;
case '!cof-attaque-magique':
parseAttaqueMagique(msg);
return;
case "!cof-injonction":
parseAttaqueMagique(msg, 'injonction');
return;
case "!cof-sommeil":
parseSommeil(msg);
return;
case "!cof-attaque-magique-contre-pv": // deprecated
attaqueMagiqueContrePV(msg);
return;
case "!cof-transe-guerison":
transeGuerison(msg);
return;
case "!cof-soin":
case "!cof-soins":
soigner(msg);
return;
case "!cof-nature-nourriciere":
parseNatureNourriciere(msg);
return;
case "!cof-ignorer-la-douleur":
ignorerLaDouleur(msg);
return;
case "!cof-fortifiant":
fortifiant(msg);
return;
case "!cof-intercepter":
intercepter(msg);
return;
case "!cof-interposer":
interposer(msg);
return;
case "!cof-esquive-fatale":
doEsquiveFatale(msg);
return;
case "!cof-exemplaire":
exemplaire(msg);
return;
case "!cof-intervention-divine":
interventionDivine(msg);
return;
case "!cof-lancer-sort":
lancerSort(msg);
return;
case "!cof-as":
emulerAs(msg);
return;
case "!cof-peur":
parsePeur(msg);
return;
case "!cof-distribuer-baies":
distribuerBaies(msg);
return;
case "!cof-consommer-baie":
consommerBaie(msg);
return;
case "!cof-proteger-un-allie":
protegerUnAllie(msg);
return;
case "!cof-action-defensive":
actionDefensive(msg);
return;
case "!cof-strangulation":
strangulation(msg);
return;
case "!cof-ombre-mortelle":
ombreMortelle(msg);
return;
case "!cof-escalier":
escalier(msg);
return;
case "!cof-defaut-dans-la-cuirasse":
defautDansLaCuirasse(msg);
return;
case '!cof-posture-de-combat':
postureDeCombat(msg);
return;
case '!cof-attaque-a-outrance':
attaqueAOutrance(msg);
return;
case '!cof-mur-de-force':
murDeForce(msg);
return;
case '!cof-capitaine':
devientCapitaine(msg);
return;
case '!cof-tueur-fantasmagorique':
parseAttaqueMagique(msg, 'tueurFantasmagorique');
return;
case '!cof-enkystement-lointain':
parseAttaqueMagique(msg, 'enkystementLointain');
return;
case '!cof-injonction-mortelle':
parseInjonctionMortelle(msg);
return;
case '!cof-tour-de-force': // Deprecrated
parseTourDeForce(msg);
return;
case '!cof-prouesse':
boutonProuesse(msg);
return;
case '!cof-tour-force':
boutonTourDeForce(msg);
return;
case '!cof-pacte-sanglant':
boutonPacteSanglant(msg);
return;
case '!cof-pacte-sanglant-def':
boutonPacteSanglantDef(msg);
return;
case '!cof-encaisser-un-coup':
doEncaisserUnCoup(msg);
return;
case '!cof-devier-les-coups':
doDevierLesCoups(msg);
return;
case '!cof-parade-projectiles':
doParadeProjectiles(msg);
return;
case "!cof-parade-au-bouclier":
doParadeAuBouclier(msg);
return;
case "!cof-esquive-acrobatique":
doEsquiveAcrobatique(msg);
return;
case "!cof-esquive-de-la-magie":
doEsquiveDeLaMagie(msg);
return;
case "!cof-resister-a-la-magie":
resisterALaMagie(msg);
return;
case "!cof-cercle-protection":
cercleDeProtection(msg);
return;
case "!cof-parade-magistrale":
doParadeMagistrale(msg);
return;
case "!cof-esquive-magistrale":
doEsquiveMagistrale(msg);
return;
case "!cof-absorber-au-bouclier":
case "!cof-absorber-coup-au-bouclier":
absorberCoupAuBouclier(msg);
return;
case "!cof-absorber-sort-au-bouclier":
absorberSortAuBouclier(msg);
return;
case "!cof-chair-a-canon":
doChairACanon(msg);
return;
case "!cof-demarrer-statistiques":
if (stateCOF.statistiquesEnPause) {
stateCOF.statistiques = stateCOF.statistiquesEnPause;
delete stateCOF.statistiquesEnPause;
} else {
stateCOF.statistiques = {}; //remet aussi les statistiques à 0
}
return;
case "!cof-arreter-statistiques":
delete stateCOF.statistiques;
return;
case "!cof-pause-statistiques":
if (stateCOF.statistiques) {
stateCOF.statistiquesEnPause = stateCOF.statistiques;
delete stateCOF.statistiques;
} // sinon, ne pas écraser les statistiques déjà en pause
return;
case '!cof-statistiques':
displayStatistics(msg);
return;
case '!cof-destruction-des-morts-vivants':
parseDestructionDesMortsVivants(msg);
return;
case '!cof-enduire-poison':
parseEnduireDePoison(msg);
return;
case '!cof-consommables':
listeConsommables(msg);
return;
case '!cof-utilise-consommable': //Usage interne seulement
utiliseConsommable(msg);
return;
case '!cof-echange-consommable': //Usage interne seulement
echangeConsommable(msg);
return;
case '!cof-provocation':
parseProvocation(msg);
return;
case '!cof-en-selle':
enSelle(msg);
return;
case '!cof-creer-elixir': //usage interne seulement
creerElixir(msg);
return;
case '!cof-elixirs':
gestionElixir(msg);
return;
case '!cof-runes':
gestionRunes(msg);
return;
case '!cof-creer-rune': // usage interne seulement
creerRune(msg);
return;
case '!cof-rage-du-berserk':
parseRageDuBerserk(msg);
return;
case '!cof-arme-secrete':
parseArmeSecrete(msg);
return;
case '!cof-animer-arbre':
animerUnArbre(msg);
return;
case '!cof-delivrance':
case '!cof-guerir':
delivrance(msg);
return;
case '!cof-guerison':
guerison(msg);
return;
case '!cof-test-attaque-opposee':
testAttaqueOpposee(msg);
return;
case '!cof-manoeuvre':
manoeuvreRisquee(msg);
return;
case '!cof-appliquer-manoeuvre':
appliquerManoeuvre(msg);
return;
case '!cof-desarmer':
desarmer(msg);
return;
case '!cof-tempete-de-mana':
optionsDeTempeteDeMana(msg);
return;
case '!cof-tour-suivant':
tourSuivant(msg);
return;
case '!cof-multi-command':
multiCommand(msg);
return;
case '!cof-conjuration-de-predateur':
conjurationPredateur(msg);
return;
case '!cof-conjuration-armee':
conjurationArmee(msg);
return;
case '!cof-set-macros':
setGameMacros(msg);
return;
case '!cof-lumiere':
ajouteLumiere(msg);
return;
case '!cof-eteindre-lumiere':
eteindreLumieres(msg);
return;
case '!cof-torche':
switchTorche(msg);
return;
case '!cof-defi-samourai':
lancerDefiSamourai(msg);
return;
case '!cof-enveloppement':
parseEnveloppement(msg);
return;
case '!cof-echapper-enveloppement':
parseEchapperEnveloppement(msg);
return;
case '!cof-liberer-agrippe':
parseLibererAgrippe(msg);
return;
case '!cof-liberer-ecrase':
parseLibererEcrase(msg);
return;
case '!cof-animer-cadavre':
animerCadavre(msg);
return;
case '!cof-vapeurs-ethyliques':
parseVapeursEthyliques(msg);
return;
case '!cof-desaouler':
desaouler(msg);
return;
case '!cof-boire-alcool':
parseBoireAlcool(msg);
return;
case '!cof-jouer-son':
jouerSon(msg);
return;
case '!cof-bouton-echec-total':
echecTotal(msg);
return;
case '!cof-usure-off':
let combat = stateCOF.combat;
if (combat) {
combat.usureOff = true;
sendChat('COF', "/w GM Pas d'usure de la DEF sur ce combat");
}
return;
case '!cof-set-attribute':
setAttributeInterface(msg);
return;
case '!cof-set-predicate':
setPredicateInterface(msg);
return;
case '!cof-options-d-attaque':
optionsDAttaque(msg);
return;
case '!cof-petit-veinard':
case '!cof-bouton-petit-veinard':
petitVeinard(msg);
return;
case '!cof-suivre':
suivre(msg);
return;
case '!cof-centrer-sur-token':
centrerSurToken(msg);
return;
case '!cof-bourse':
gestionBourse(msg);
return;
case '!cof-mot-de-pouvoir-immobilise':
motDePouvoirImmobilise(msg);
return;
case '!cof-charge-fantastique':
chargeFantastque(msg);
return;
case '!cof-next-charge-fantastique':
nextTurnChargeFantastique(msg);
return;
case '!cof-tenebres':
tenebres(msg);
return;
case '!cof-defense-armee-des-morts':
defenseArmeeDesMorts(msg);
return;
case '!cof-invoquer-demon':
invocationDemon(msg);
return;
case '!cof-animer-mort':
animerMort(msg);
return;
case '!cof-prescience':
utiliserPrescience(msg);
return;
case '!cof-multi-cartes':
multiCartes(msg);
return;
case '!cof-ombre-mouvante':
ombreMouvante(msg);
return;
case '!cof-reveler-nom':
revelerNom(msg);
return;
case '!cof-fiole-de-lumiere':
fioleDeLumiere(msg);
return;
case '!cof-agripper-de-demon':
agripperDeDemon(msg);
return;
default:
let errMsg = "Commande " + command[0] + " non reconnue.";
log(errMsg);
log(command);
try {
sendPlayer(msg, errMsg);
} catch (e) {
errMsg = errMsg.replace('[', '[ ');
sendPlayer(msg, "Message sans jet : " + errMsg);
}
return;
}
}
//Attributs possibles :
// activation : message à l'activation
// activationF : message à l'activation si la cible est féminine
// actif : message de statut
// actifF : message de statut si la cible est féminine
// fin : message à la fin de l'effet
// dm : permet d'infliger des dm
// soins : soigne
// prejudiciable: est un effet préjudiciable, qui peut être enlevé par délivrance
// generic: admet un argument entre parenthèses
// seulementVivant: ne peut s'appliquer qu'aux créatures vivantes
// visible : l'effet est visible
// msgSave: message à afficher quand on résiste à l'effet. Sera précédé de "pour "
// entrave: effet qui immobilise, paralyse ou ralentit
// statusMarker: marker par défaut pour l'effet
const messageEffetTemp = {
aCouvert: {
activation: "reste à couvert",
actif: "est à couvert",
fin: "n'est pas à couvert"
},
affecteParAura: {
activation: "entre dans une aura",
actif: "est entré dans une aura ce tour",
actifF: "est entrée dans une aura ce tour",
fin: "fin d'aura",
generic: true,
},
apeureTemp: {
activation: "prend peur",
actif: "est dominé par sa peur",
actifF: "est dominée par sa peur",
fin: "retrouve du courage",
msgSave: "retrouver du courage",
prejudiciable: true,
visible: true
},
attaqueADistanceRatee: {
activation: "rate une attaque à distance",
actif: "a raté une attaque à distance",
fin: '',
visible: false
},
aveugleTemp: {
activation: "n'y voit plus rien !",
actif: "est aveuglé",
actifF: "est aveuglée",
fin: "retrouve la vue",
msgSave: "retrouver la vue",
prejudiciable: true,
visible: true
},
benediction: {
activation: "est touché par la bénédiction",
activationF: "est touchée par la bénédiction",
actif: "est béni",
actifF: "est bénie",
fin: "l'effet de la bénédiction s'estompe"
},
blessureSanglante: {
activation: "saigne abondamment",
actif: "saigne abondamment",
fin: "le saignement cesse enfin",
prejudiciable: true,
seulementVivant: true,
visible: true,
dm: true,
},
carquoisMagique: {
activation: "enchante son carquois",
actif: "a un carquois enchannté",
fin: "le carquois redevient normal",
visible: true,
},
chantDesHeros: {
activation: "écoute le chant du barde",
actif: "est inspiré par le Chant des Héros",
actifF: "est inspirée par le Chant des Héros",
fin: "n'est plus inspiré par le Chant des Héros",
finF: "n'est plus inspirée par le Chant des Héros"
},
drainDeForce: {
activation: "se sent drainé de sa force",
activationF: "se sent drainée de sa force",
actif: "est sous l'effet d'un drain de force",
msgSave: "ne plus être affaibli",
fin: "n'est plus affaibli",
finF: "n'est plus affaiblie",
prejudiciable: true,
seulementVivant: true,
},
drainDeForceSup: {
activation: "se sent drainé de sa force",
activationF: "se sent drainée de sa force",
actif: "est sous l'effet d'un drain de force supérieur",
msgSave: "ne plus être affaibli",
fin: "n'est plus affaibli",
finF: "n'est plus affaiblie",
prejudiciable: true,
seulementVivant: true,
},
etourdiTemp: {
activation: "est étourdi : aucune action et -5 en DEF",
activationF: "est étourdie : aucune action et -5 en DEF",
actif: "est étourdi",
actifF: "est étourdie",
fin: "n'est plus étourdi",
finF: "n'est plus étourdie",
msgSave: "se reprendre",
prejudiciable: true,
visible: true
},
formeDAnge: {
activation: "prend la forme d'un ange ailé",
actif: "est en forme d'ange et peut jeter des sorts en vol stationnaire",
fin: "retrouve sa forme normale. Espérons qu'il était au sol...",
finF: "retrouve sa forme normale. Espérons qu'elle était au sol...",
visible: true,
},
formeHybride: {
activation: "prend la forme d'une bête mi-homme mi-loup",
actif: "est en forme hybride",
fin: "retrouve forme humaine",
visible: true,
dm: true,
},
frappeDesArcanes: {
activation: "insuffle sa puissance magique dans son attaque",
actif: "ne peut plus lancer de sorts pour l'instant",
fin: "retrouve assez de puissance magique pour lancer des sorts",
},
frenesieMinotaure: {
activation: "entre en frénésie",
actif: "est en frénésie",
fin: "retrouve son calme",
msgSave: "retrouver son calme",
},
inspiration: {
activation: "est inspiré",
activationF: "est inspirée",
actif: "est sous l'effet d'une inspiration",
fin: "n'est plus inspiré",
finF: "n'est plus inspirée"
},
imageDecalee: {
activation: "décale légèrement son image",
actif: "a décalé son image",
fin: "apparaît à nouveau là où il se trouve",
finF: "apparaît à nouveau là où elle se trouve",
visible: false
},
immobiliseTemp: {
activation: "est immobilisé : aucun déplacement possible",
activationF: "est immobilisée : aucun déplacement possible",
actif: "est immobilisé",
actifF: "est immobilisée",
fin: "n'est plus immobilisé",
finF: "n'est plus immobilisée",
msgSave: "pouvoir bouger",
prejudiciable: true,
visible: true,
entrave: true
},
immuniteAmaladie: {
activation: "devient immunisé aux maladies",
activationF: "devient immunisée aux maladies",
actif: "est temporairement immunisé aux maladies",
actifF: "est temporairement immunisée aux maladies",
fin: "redevient sensible aux maladies",
visible: false
},
immuniteApoison: {
activation: "devient immunisé au poison",
activationF: "devient immunisée au poison",
actif: "est temporairement immunisé au poison",
actifF: "est temporairement immunisée au poison",
fin: "redevient sensible au poison",
visible: false
},
lycanthropie: {
activation: "fusionne avec son compagnon",
actif: "est en forme hybride",
fin: "retrouve sa forme normale",
visible: true,
dm: true
},
paradeCroisee: {
activation: "se met en position pour parer des deux armes",
actif: "est en position de parade croisée",
fin: "ne bénéficie plus de sa parade croisée",
visible: true,
},
paralyseTemp: {
activation: "est paralysé : aucune action ni déplacement possible",
activationF: "est paralysée : aucune action ni déplacement possible",
actif: "est paralysé",
actifF: "est paralysée",
fin: "n'est plus paralysé",
finF: "n'est plus paralysée",
msgSave: "ne plus être paralysé",
prejudiciable: true,
visible: true,
entrave: true
},
paralyseGoule: {
activation: "est paralysé : aucune action ni déplacement possible",
activationF: "est paralysée : aucune action ni déplacement possible",
actif: "est paralysé",
actifF: "est paralysée",
fin: "n'est plus paralysé",
finF: "n'est plus paralysée",
msgSave: "ne plus être paralysé",
prejudiciable: true,
visible: true
},
peauDEcorce: {
activation: "donne à sa peau la consistance de l'écorce",
actif: "a la peau dure comme l'écorce",
fin: "retrouve une peau normale",
visible: true
},
peauDEcorceAmeLiee: {
activation: "voit sa peau se durcir",
actif: "a la peau un peu rugueuse",
fin: "retrouve une peau normale",
visible: true
},
penombreTemp: {
activation: "ne voit plus très loin",
actif: "est dans la pénombre",
fin: "retrouve une vue normale",
msgSave: "retrouver la vue",
prejudiciable: true,
},
peurEtourdi: {
activation: "prend peur: il peut fuir ou rester recroquevillé",
activationF: "prend peur: elle peut fuir ou rester recroquevillé",
actif: "est paralysé par la peur",
actifF: "est paralysée par la peur",
fin: "retrouve du courage et peut à nouveau agir",
msgSave: "retrouver du courage",
prejudiciable: true,
visible: true
},
protectionContreLesProjectiles: {
activation: "gagne une protection contre les projectiles",
actif: "est protégé contre les projectiles",
actifF: "est protégée contre les projectiles",
fin: "n'est plus protégé contre les projectiles",
finF: "n'est plus protégée contre les projectiles",
visible: false,
},
ralentiTemp: {
activation: "est ralenti : une seule action, pas d'action limitée",
activationF: "est ralentie : une seule action, pas d'action limitée",
actif: "est ralenti",
actifF: "est ralentie",
msgSave: "ne plus être ralenti",
fin: "n'est plus ralenti",
finF: "n'est plus ralentie",
prejudiciable: true,
visible: true,
entrave: true
},
rayonAffaiblissant: {
activation: "est touché par un rayon affaiblissant",
activationF: "est touchée par un rayon affaiblissant",
actif: "est sous l'effet d'un rayon affaiblissant",
msgSave: "ne plus être affaibli",
fin: "n'est plus affaibli",
finF: "n'est plus affaiblie",
prejudiciable: true
},
rituelAssure: {
activation: "passe un tour complet à préparer un sort",
actif: "a préparé un rituel assuré",
fin: "",
visible: false
},
secoue: {
activation: "hésite un peu, il est secoué",
activationF: "hésite un peu, elle est secouée",
actif: "a un peu peur",
fin: "retrouve du courage",
msgSave: "retrouver du courage",
prejudiciable: true,
visible: true
},
secretsDeLAuDela: {
activation: "contacte des esprits",
actif: "des esprits lui murmurent des secrets oubliés",
fin: "les esprits repartent"
},
souffleDeMort: {
activation: "est terrifié par la mort de son allié",
activationF: "est terrifiée par la mort de son allié",
actif: "ne peut agir suite au souffle de mort",
fin: "retrouve ses esprits",
msgSave: "pouvoir agir malgré la mort de son allié",
prejudiciable: true,
visible: true,
},
tremblementMineur: {
activation: "Tremblement mineur",
actif: "est déséquilibré par le tremblement",
actifF: "est déséquilibrée par le tremblement",
fin: '',
visible: false
},
zoneDeVie: {
activation: "enchante une zone autour de lui",
activationF: "enchante une zone autour d'elle",
actif: "a créé une zone de vie",
fin: "la zone de vie se termine",
visible: true,
generic: true, //pour pouvoir avoir plusieurs zones de vie
},
affaibliTemp: {
activation: "se sent faible",
actif: "est affaibli",
actifF: "est affaiblie",
fin: "se sent moins faible",
msgSave: "retrouver des forces",
prejudiciable: true
},
assommeTemp: {
activation: "est assommé",
activationF: "est assommée",
actif: "est assommé",
actifF: "est assommée",
fin: "reprend conscience",
msgSave: "reprendre conscience",
prejudiciable: true,
visible: true
},
nauseeuxTemp: {
activation: "souffre de violentes douleurs au ventre",
actif: "est nauséeux, seul le mouvement est possible",
actifF: "est nauséeuse, seul le mouvement est possible",
fin: "se sent mieux",
msgSave: "ne plus être nauséeux",
prejudiciable: true,
visible: true
},
invisibleTemp: {
activation: "disparaît",
actif: "est invisible",
fin: "réapparaît",
msgSave: "ne pas devenir invisible",
visible: true
},
aveugleManoeuvre: {
activation: "est aveuglé par la manoeuvre",
activationF: "est aveuglée par la manoeuvre",
actif: "a du mal à voir où sont ses adversaires",
fin: "retrouve une vision normale",
msgSave: "voir à nouveau",
prejudiciable: true,
visible: true
},
bloqueManoeuvre: {
activation: "est bloqué par la manoeuvre",
activationF: "est bloquée par la manoeuvre",
actif: "est bloqué et ne peut pas se déplacer",
actifF: "est bloquée et ne peut pas se déplacer",
fin: "peut à nouveau se déplacer",
msgSave: "pouvoir se déplacer",
prejudiciable: true,
entrave: true
},
diversionManoeuvre: {
activation: "est déconcentré",
activationF: "est déconcentrée",
actif: "a été perturbé par une diversion",
actifF: "a été perturbée par une diversion",
fin: "se reconcentre sur le combat",
msgSave: "se reconcentrer sur le combat",
prejudiciable: true,
visible: true
},
menaceManoeuvre: {
activation: "est menacé",
activationF: "est menacée",
actif: "a été menacé, risque de plus de DM",
actifF: "a été menacée, risque de plus de DM",
fin: "n'est plus sous la menace",
msgSave: "ne plus être sous la menace",
prejudiciable: true,
generic: true
},
tenuADistanceManoeuvre: {
activation: "est tenu à distance",
activationF: "est tenue à distance",
actif: "est tenu à distance de son adversaire, il ne peut pas l'attaquer",
actifF: "est tenue à distance de son adversaire, elle ne peut pas l'attaquer",
fin: "peut à nouveau attaquer son adversaire",
msgSave: "pouvoir à nouveau attaquer son adversaire",
prejudiciable: true,
generic: true,
visible: true,
entrave: true
},
epeeDansante: {
activation: "fait apparaître une lame d'énergie lumineuse",
actif: "contrôle une lame d'énergie lumineuse",
fin: "La lame d'énergie lumineuse disparaît",
dm: true,
visible: true
},
rapiereDansante: {
activation: "fait apparaître une rapière d'énergie lumineuse",
actif: "contrôle une rapière d'énergie lumineuse",
fin: "La rapière d'énergie lumineuse disparaît",
dm: true,
visible: true
},
putrefaction: {
activation: "vient de contracter une sorte de lèpre fulgurante",
actif: "est en pleine putréfaction",
fin: "La putréfaction s'arrête.",
msgSave: "résister à la putréfaction",
prejudiciable: true,
dm: true,
visible: true
},
forgeron: {
activation: "enflamme son arme",
actif: "a une arme en feu",
fin: "L'arme n'est plus enflammée.",
dm: true,
generic: true,
visible: true
},
armeEnflammee: {
activation: "voit son arme prendre feu",
actif: "a une arme enflammée",
fin: "L'arme n'est plus enflammée.",
dm: true,
generic: true,
visible: true
},
armeGlacee: {
activation: "voit son arme se couvrir de givre",
actif: "a une arme glacée",
fin: "L'arme n'est plus gelée.",
dm: true,
generic: true,
visible: true
},
armesEnflammees: {
activation: "voit ses armes prendre feu",
actif: "a des armes enflammées",
fin: "Les armes ne sont plus enflammées.",
dm: true,
visible: true
},
dotGen: {
activation: "subit un effet",
actif: "subit régulièrement des dégâts",
fin: "ne subit plus ces effets de dégâts",
dm: true,
prejudiciable: true,
generic: true
},
rechargeGen: {
activation: "doit maintenant attendre un peu avant de pouvoir le refaire",
actif: "attend avant de pouvoir refaire une action",
fin: "a récupéré",
generic: true
},
dmgArme: {
activation: "enduit son arme d'une huile magique",
actif: "a une arme plus puissante",
fin: "L'arme retrouve sa puissance normale",
dm: true,
generic: true
},
flou: {
activation: "devient flou",
activationF: "devient floue",
actif: "apparaît flou",
actifF: "apparaît floue",
fin: "redevient net",
visible: true
},
agrandissement: {
activation: "se met à grandir",
actif: "est vraiment très grand",
actifF: "est vraiment très grande",
fin: "retrouve sa taille normale",
visible: true
},
formeGazeuse: {
activation: "semble perdre de la consistance",
actif: "est en forme gazeuse",
fin: "retrouve sa consistance normale",
visible: true
},
intangible: {
activation: "devient translucide",
actif: "est intangible",
fin: "redevient solide",
visible: true
},
intangibleInvisible: {
activation: "disparaît",
actif: "est intangible et invisible",
fin: "réapparaît",
visible: true
},
sousTension: {
activation: "se charge d'énergie électrique",
actif: "est chargé d'énergie électrique",
actifF: "est chargée d'énergie électrique",
fin: "n'est plus chargé d'énergie électrique",
finF: "n'est plus chargée d'énergie électrique",
dm: true,
visible: false
},
strangulation: {
activation: "commence à étouffer",
actif: "est étranglé",
actifF: "est étranglée",
fin: "respire enfin",
msgSave: "pouvoir respirer",
prejudiciable: true,
seulementVivant: true,
dm: true,
visible: true
},
ombreMortelle: {
activation: "voit son ombre s'animer et l'attaquer !",
actif: "est une ombre animée",
fin: "retrouve une ombre normale",
dm: true,
visible: true
},
dedoublement: {
activation: "voit un double translucide sortir de lui",
activationF: "voit un double translucide sortir d'elle",
actif: "est un double translucide",
fin: "le double disparaît",
dm: true,
visible: true
},
zoneDeSilence: {
activation: "n'entend plus rien",
actif: "est totalement sourd",
actifF: "est totalement sourde",
fin: "peut à nouveau entendre"
},
danseIrresistible: {
activation: "se met à danser",
actif: "danse malgré lui",
actifF: "danse malgré elle",
fin: "s'arrête de danser",
msgSave: "s'arrêter de danser",
prejudiciable: true,
visible: true
},
confusion: {
activation: "ne sait plus très bien ce qu'il fait là",
activationF: "ne sait plus très bien ce qu'elle fait là",
actif: "est en pleine confusion",
fin: "retrouve ses esprits",
msgSave: "retrouver ses esprits",
prejudiciable: true,
visible: true
},
murDeForce: {
activation: "fait apparaître un mur de force",
actif: "en entouré d'un mur de force",
actifF: "en entourée d'un mur de force",
fin: "voit son mur de force disparaître",
visible: true
},
asphyxie: {
activation: "commence à manquer d'air",
actif: "étouffe",
fin: "peut à nouveau respirer",
msgSave: "pouvoir respirer normalement",
prejudiciable: true,
seulementVivant: true,
statusMarker: 'blue',
dm: true,
visible: true
},
forceDeGeant: {
activation: "devient plus fort",
activationE: "devient plus forte",
actif: "a une force de géant",
fin: "retrouve sa force normale"
},
saignementsSang: {
activation: "commence à saigner du nez, des oreilles et des yeux",
actif: "saigne de tous les orifices du visage",
fin: "ne saigne plus",
msgSave: "ne plus saigner",
prejudiciable: true,
statusMarker: 'red',
dm: true,
visible: true
},
encaisserUnCoup: {
activation: "se place de façon à dévier un coup sur son armure",
actif: "est placé de façon à dévier un coup",
actifF: "est placée de façon à dévier un coup",
fin: "n'est plus en position pour encaisser un coup"
},
seulContreTous: {
activation: "se place de façon à faire face à tous ses adversaires",
actif: "riposte",
fin: "ne fait plus face à tous ses adversaires"
},
absorberUnCoup: {
activation: "se prépare à absorber un coup avec son bouclier",
actif: "est prêt à absorber un coup avec son bouclier",
actifF: "est prête à absorber un coup avec son bouclier",
fin: "n'est plus en position de prendre le prochain coup sur son bouclier"
},
absorberUnSort: {
activation: "se prépare à absorber un sort avec son bouclier",
actif: "est prêt à absorber un sort avec son bouclier",
actifF: "est prête à absorber un sort avec son bouclier",
fin: "n'est plus en position de se protéger d'un sort avec son bouclier"
},
nueeDInsectes: {
activation: "est attaqué par une nuée d'insectes",
activationF: "est attaquée par une nuée d'insectes",
actif: "est entouré d'une nuée d'insectes",
actifF: "est entourée d'une nuée d'insectes",
fin: "est enfin débarassé des insectes",
finF: "est enfin débarassée des insectes",
msgSave: "se débarasser des insectes",
prejudiciable: true,
dm: true,
visible: true
},
nueeDeCriquets: {
activation: "est attaqué par une nuée de criquets",
activationF: "est attaquée par une nuée de criquets",
actif: "est entouré d'une nuée de criquets",
actifF: "est entourée d'une nuée de criquets",
fin: "est enfin débarassé des criquets",
finF: "est enfin débarassée des criquets",
msgSave: "se débarasser des criquets",
prejudiciable: true,
dm: true,
visible: true
},
nueeDeScorpions: {
activation: "est attaqué par une nuée de scorpions",
activationF: "est attaquée par une nuée de scorpions",
actif: "est entouré d'une nuée de scorpions",
actifF: "est entourée d'une nuée de scorpions",
fin: "est enfin débarassé des scorpions",
finF: "est enfin débarassée des scorpions",
msgSave: "se débarasser des scorpions",
prejudiciable: true,
dm: true,
visible: true
},
toiles: {
activation: "voit des toiles d'araignées apparaître tout autour",
actif: "est bloqué par des toiles d'araignées",
actifF: "est bloquée par des toiles d'araignées",
fin: "se libère des toiles",
msgSave: "se libérer des toiles",
prejudiciable: true,
statusMarker: 'cobweb',
visible: true,
entrave: true
},
prisonVegetale: {
activation: "voit des plantes pousser et s'enrouler autour de ses jambes",
actif: "est bloqué par des plantes",
actifF: "est bloquée par des plantes",
fin: "se libère des plantes",
msgSave: "se libérer des plantes",
prejudiciable: true,
statusMarker: 'green',
visible: true,
entrave: true
},
protectionContreLesElements: {
activation: "lance un sort de protection contre les éléments",
actif: "est protégé contre les éléments",
actifF: "est protégée contre les éléments",
fin: "n'est plus protégé contre les éléments",
finF: "n'est plus protégée contre les éléments"
},
masqueMortuaire: {
activation: "prend l'apparence de la mort",
actif: "semble mort et animé",
actifF: "semble morte et animée",
fin: "retrouve une apparence de vivant",
visible: true
},
masqueMortuaireAmeLiee: {
activation: "est lié à une apparence de la mort",
activationF: "est liée à une apparence de la mort",
actif: "semble en lien avec la mort",
fin: "n'est plus en lien avec la mort"
},
armeBrulante: {
activation: "sent son arme lui chauffer la main",
actif: "se brûle la main sur son arme",
fin: "sent son arme refroidir",
dm: true
},
armureBrulante: {
activation: "sent son armure chauffer",
actif: "brûle dans son armure",
fin: "sent son armure refroidir",
dm: true
},
masqueDuPredateur: {
activation: "prend les traits d'un prédateur",
actif: "a les traits d'un prédateur",
fin: "redevient normal",
finF: "redevient normale",
visible: true
},
masqueDuPredateurAmeLiee: {
activation: "est lié à une âme de prédateur",
activationF: "est liée à une âme de prédateur",
actif: "bénéficie d'un lien avec un prédateur",
fin: "le lien disparaît",
visible: false
},
aspectDeLaSuccube: {
activation: "acquiert une beauté fascinante",
actif: "est d'une beauté fascinante",
fin: "retrouve sa beauté habituelle",
visible: true
},
aspectDuDemon: {
activation: "prend l’apparence d’un démon",
actif: "a l’apparence d’un démon",
fin: "retrouve son apparence habituelle",
visible: true
},
sangMordant: {
activation: "transforme son sang",
actif: "a du sang acide",
fin: "retrouve un sang normal"
},
armeSecreteBarde: {
activation: "est déstabilisé",
activationF: "est déstabilisée",
actif: "est déstabilisé par une action de charme",
actifF: "est déstabilisée par une action de charme",
fin: "retrouve ses esprits",
msgSave: "retrouver ses esprits",
prejudiciable: true,
visible: true
},
regeneration: {
activation: "commence à se régénérer",
actif: "se régénère",
fin: "a fini de se régénérer",
soins: true,
visible: true
},
arbreAnime: {
activation: "commence à bouger",
actif: "est un arbre animé",
fin: "redevient un arbre ordinaire",
visible: true
},
objetAnime: {
activation: "commence à bouger",
actif: "est un objet animé",
fin: "redevient un objet ordinaire",
visible: true
},
magnetisme: {
activation: "contrôle le magnétisme",
actif: "contrôle le magnétisme",
fin: "relache son contrôle du magnétisme"
},
hate: {
activation: "voit son métabolisme s'accélérer",
actif: "peut faire une action de plus par tour",
fin: "retrouve un métabolisme normal (plus d'action supplémentaire)"
},
ailesCelestes: {
activation: "sent des ailes célestes lui pousser dans le dos",
actif: "possède des ailes célestes",
fin: "n'a plus d'aile céleste. Espérons qu'il était au sol...",
finF: "n'a plus d'aile céleste. Espérons qu'elle était au sol...",
visible: true
},
sanctuaire: {
activation: "est protégé par un sanctuaire",
activationF: "est protégée par un sanctuaire",
actif: "est protégé par un sanctuaire",
actifF: "est protégée par un sanctuaire",
fin: "n'est plus protégé par le sanctuaire",
finF: "n'est plus protégée par le sanctuaire"
},
rechargeSouffle: {
activation: "doit maintenant attendre un peu avant de pouvoir le refaire",
actif: "attend avant de pouvoir refaire un souffle",
fin: "a récupéré"
},
paralysieRoublard: {
activation: "est paralysé par la douleur",
activationF: "est paralysée par la douleur",
actif: "ne peut pas attaquer ni se déplacer",
fin: "peut à nouveau attaquer et se déplacer",
msgSave: "résister à la douleur",
prejudiciable: true,
seulementVivant: true,
visible: true,
entrave: true
},
mutationOffensive: {
activation: "échange une partie de son corps avec celle d'une créature monstrueuse",
actif: "possède un appendice monstrueux",
fin: "retrouve un corps normal",
visible: true
},
formeDArbre: {
activation: "se transorme en arbre",
actif: "est transformé en arbre",
actifF: "est transformée en arbre",
fin: "retrouve sa forme normale",
visible: true
},
statueDeBois: {
activation: "se transforme en statue de bois",
actif: "est transformé en statue de bois",
actifF: "est transformée en statue de bois",
fin: "retrouve sa forme normale",
msgSave: "ne plus être une statue de bois",
prejudiciable: true,
visible: true
},
clignotement: {
activation: "disparaît, puis réapparaît",
actif: "clignote",
fin: "ne disparaît plus",
visible: true
},
agitAZeroPV: {
activation: "continue à agir malgré les blessures",
actif: "devrait être à terre",
fin: "subit l'effet de ses blessures",
visible: true
},
predateurConjure: {
activation: "apparaît depuis un autre plan",
actif: "est un prédateur conjuré",
fin: "disparaît",
dm: true
},
champDeProtection: {
activation: "devient protégé par un champ télékinétique",
actif: "est protégé par un champ télékinétique",
actifF: "est protégée par un champ télékinétique",
fin: "n'est plus sous l'effet d'un champ de protection",
},
attaqueArmeeConjuree: {
activation: "se bat contre une armée conjurée",
actif: "se bat contre une armée conjurée",
fin: "ne se bat plus contre l'armée conjurée"
},
rechargeDuKiai: {
activation: "pousse un kiai",
actif: "ne peut pas encore pousser un autre kiai",
fin: "peut pousser un autre kiai"
},
memePasMalBonus: {
activation: "enrage suite au coup critique",
actif: "a subit un coup critique",
fin: "ne bénéficie plus des effets de même pas mal"
},
attaqueRisquee: {
activation: "fait une attaque risquée",
actif: "s'est mis en danger par une attaque risquée",
fin: "retrouve une position moins risquée",
},
peauDePierreMag: {
activation: "transforme sa peau en pierre",
actif: "voit ses dégâts réduits par sa Peau de pierre",
fin: "retrouve sa peau normale",
visible: true
},
expose: {
activation: "s'expose aux attaques de sa cible",
actif: "est exposé aux attaques de son adversaire",
actifF: "est exposée aux attaques de son adversaire",
fin: "n'est plus exposé",
finF: "n'est plus exposée",
msgSave: "ne plus s'exposer",
prejudiciable: true
},
effetRetarde: {
activation: "il va bientôt se produire quelque chose",
actif: "s'attend à un effet",
fin: "effet retardé activé",
generic: true,
prejudiciable: true
},
messageRetarde: {
activation: "il va bientôt se produire quelque chose",
actif: "s'attend à un effet",
fin: "effet activé",
generic: true
},
detectionDeLInvisible: {
activation: "voit les choses invibles et cachées",
actif: "détecte l'invisible",
fin: "ne voit plus les choses invisibles",
},
bonusAttaqueTemp: {
activation: "affecte son attaque",
actif: "a son attaque affectée",
fin: "retrouve son attaque normale",
},
enerve: {
activation: "est énervé par ces railleries",
activationF: "est énervée par ces railleries",
actif: "est énervé",
actifF: "est énervée",
fin: "retrouve son calme",
msgSave: "retrouver son calme",
prejudiciable: true,
visible: true
},
cercleDeProtection: {
activation: "est protégé par le Cercle de protection",
activationF: "est protégée par le Cercle de protection",
actif: "est dans le Cercle de protection",
fin: "sort du Cercle de protection",
},
tenebres: {
activation: "lance un sort de Ténèbres",
actif: "maintient un sort de Ténèbres",
fin: "les ténèbres se dissipent",
visible: true
},
brumes: {
activation: "lance un sort de brumes",
actif: "maintient un sort de brumes",
fin: "les brumes se dissipent",
visible: true
},
progresserACouvert: {
activation: "est à couvert de bouclier",
actif: "est à couvert de bouclier",
fin: "n'est plus à couvert de bouclier"
},
cyclone: {
activation: "se transforme en tourbillon de matière élémentaire",
actif: "est en cyclone",
fin: "retrouve sa forme habituelle",
visible: true
},
momentDePerfection: {
activation: "atteint un instant de perfection",
actif: "semble tout voir au ralenti autour de lui",
fin: "voit le temps reprendre son cours normal",
},
armeeDesMorts: {
activation: "invoque d'innombrables squelettes émergeant du sol",
actif: "invoque d'innombrables squelettes",
fin: "laisse les morts en paix",
visible: true
},
demonInvoque: {
activation: "apparaît depuis un autre plan",
actif: "est un démon invoqué",
fin: "disparaît",
dm: true
},
degradationZombie: {
activation: "se relève d'entre les morts",
actif: "est un zombie animé",
fin: "tombe en poussière",
dm: true
},
hemorragie: {
activation: "saigne à la moindre blessure",
actif: "saigne à la moindre blessure",
fin: "soigne son hémorragie",
msgSave: "guérir de son hémorragie",
prejudiciable: true
},
lienDeSang: {
activation: "est lié par le sang",
activationF: "est liée par le sang",
actif: "a un lien de sang",
fin: "perd son lien de sang",
msgSave: "ne plus être lié par le sang",
prejudiciable: true
},
tenirADistance: {
activation: "utilise son allonge pour tenir ses ennemis à distance",
actif: "utilise son allonge pour tenir ses ennemis à distance",
fin: "ne tient plus ses ennemis à distance"
},
reactionViolente: {
activation: "est pris de folie et attaque la personne qui s'est moquée de lui",
activationF: "est prise de folie et attaque la personne qui s'est moquée d'elle",
actif: "a une réaction violente et doit attaquer",
fin: "reprend son calme",
msgSave: "résister à la provocation",
prejudiciable: true,
},
poisonAffaiblissantLatent: {
activation: "sent qu'un poison commence à se répandre dans ses veines",
actif: "est empoisonné, mais l'effet du poison ne se fait pas encore sentir",
actifF: "est empoisonnée, mais l'effet du poison ne se fait pas encore sentir",
fin: "se sent faible",
msgSave: "résister au poison",
prejudiciable: true,
seulementVivant: true,
},
};
function buildPatternEffets(listeEffets, postfix) {
if (postfix && postfix.length === 0) postfix = undefined;
let expression = "(";
expression = _.reduce(listeEffets, function(reg, msg, effet) {
let res = reg;
if (res !== "(") res += "|";
res += "^" + effet;
if (msg.generic) res += "\\([^)_]*\\)";
res += "(";
if (postfix) {
postfix.forEach(function(p, i) {
if (i) res += "|";
res += p + "$|" + p + "_";
});
} else res += "$|_";
res += ")";
return res;
}, expression);
expression += ")";
return new RegExp(expression);
}
const patternEffetsTemp = buildPatternEffets(messageEffetTemp);
function estEffetTemp(name) {
return (patternEffetsTemp.test(name));
}
const patternAttributEffetsTemp =
buildPatternEffets(messageEffetTemp, ['Puissant', 'Valeur', 'SaveParTour', 'SaveActifParTour', 'SaveParTourType', 'TempeteDeManaIntense', 'Options', 'TokenSide']);
function estAttributEffetTemp(name) {
return (patternAttributEffetsTemp.test(name));
}
//On sait déjà que le nom a passé le test estEffetTemp
function effetTempOfAttribute(attr) {
let ef = attr.get('name');
if (ef === undefined || messageEffetTemp[ef]) return ef;
//D'abord on enlève le nom du token
let pu = ef.indexOf('_');
if (pu > 0) {
ef = ef.substring(0, pu);
if (messageEffetTemp[ef]) return ef;
}
//Ensuite on enlève les parties entre parenthèse pour les effets génériques
pu = ef.indexOf('(');
if (pu > 0) {
ef = ef.substring(0, pu);
if (messageEffetTemp[ef]) return ef;
}
error("Impossible de déterminer l'effet correspondant à " + ef, attr);
}
function messageOfEffetTemp(effetC) {
let res = messageEffetTemp[effetC];
if (res) return res;
let p = effetC.indexOf('(');
if (p > 0) {
let ef = effetC.substring(0, p);
res = messageEffetTemp[ef];
return res;
}
error("Effet temporaire non trouvé", effetC);
}
const messageEffetCombat = {
a0PVDepuis: {
activation: "est à 0 PV",
actif: "est à 0 PV",
fin: ""
},
armureMagique: {
activation: "est entouré d'un halo magique",
activationF: "est entourée d'un halo magique",
actif: "est protégé par une armure magique",
actifF: "est protégée par une armure magique",
fin: "n'est plus entouré d'un halo magique",
finF: "n'est plus entourée d'un halo magique"
},
armureDuMage: {
activation: "fait apparaître un nuage magique argenté qui le protège",
activationF: "fait apparaître un nuage magique argenté qui la protège",
actif: "est entouré d'une armure du mage",
actifF: "est entourée d'une armure du mage",
fin: "n'a plus son armure du mage"
},
armureDEau: {
activation: "fait apparaître une couche d'eau de quelques centimètres qui le protège",
activationF: "fait apparaître une couche d'eau de quelques centimètres qui la protège",
actif: "est entouré d'une armure d'eau'",
actifF: "est entourée d'une armure d'eau'",
fin: "n'a plus son armure d'eau'"
},
armeDArgent: {
activation: "crée une arme d'argent et de lumière",
actif: "possède une arme d'argent et de lumière",
fin: "ne possède plus d'arme d'argent et de lumière",
dm: true
},
attaqueAOutrance: {
activation: "attaque à outrance",
actif: "attaque à outrance",
fin: "attaque normalement",
},
bonusInitEmbuscade: { //Effet interne pour la capacité Surveillance
activation: "a un temps d'avance en cas d'embuscade",
actif: "a un temps d'avance",
fin: ""
},
criDeGuerre: {
activation: "pousse son cri de guerre",
actif: "a effrayé ses adversaires",
fin: ""
},
criDuPredateur: {
activation: "pousse un hurlement effrayant",
actif: "a libéré son âme de prédateur",
fin: ""
},
danseDesLames: {
activation: "entre en transe",
actif: "danse la danse des lames",
fin: "termine sa danse des lames"
},
detournerLeRegard: {
activation: "détourne le regard",
actif: "détourne le regard",
fin: "regarde normalement",
},
enflamme: {
activation: "prend feu !",
actif: "est en feu",
fin: "les flammes s'éteignent",
dm: true,
statusMarker: 'red',
},
enrage: {
activation: "devient enragé",
activationF: "devient enragée",
actif: "est enragé",
actifF: "est enragée",
fin: "retrouve son calme",
msgSave: "retrouver son calme",
},
fureurDrakonideCritique: {
activation: "est rendu furieux par le coup critique",
activationF: "est rendue furieuse par le coup critique",
actif: "est en furie draconide",
fin: "retrouve son calme"
},
protectionContreLeMal: {
activation: "reçoit une bénédiction de protection contre le mal",
actif: "est protégé contre le mal",
actifF: "est protégée contre le mal",
fin: "n'est plus protégé contre le mal",
finF: "n'est plus protégée contre le mal",
},
putrefactionOutreTombe: {
activation: "sent ses chairs pourrir",
actif: "subit le contrecoup d'une putréfaction",
fin: "se remet de la putréfaction",
msgSave: "ne plus être putréfié",
prejudiciable: true,
dm: true
},
rage: {
activation: "entre en rage",
actif: "est en rage",
fin: "retrouve son calme",
msgSave: "retrouver son calme",
},
rageDuBerserk: {
activation: "entre dans une rage berserk",
actif: "est dans une rage berserk",
fin: "retrouve son calme",
msgSave: "retrouver son calme",
},
bonusInitVariable: {
activation: "entre en combat",
actif: "est en combat",
fin: ''
},
defiSamourai: {
activation: "lance un défi",
actif: "a lancé un défi",
fin: ''
},
agrippe: {
activation: "agrippe sa cible",
actif: "agrippe sa cible",
fin: "lache sa cible"
},
estAgrippePar: {
activation: "est agrippé",
activationF: "est agrippée",
actif: "est agrippé",
actifF: "est agrippée",
fin: "se libère",
msgSave: "se libérer",
entrave: true
},
devore: {
activation: "saisit sa cible",
actif: "saisit sa cible",
fin: "lache sa cible",
},
estDevorePar: {
activation: "est saisi",
activationF: "est saisie",
actif: "est saisi",
actifF: "est saisie",
fin: "se libère",
msgSave: "se libérer",
entrave: true
},
ecrase: {
activation: "saisit sa cible entre ses bras",
actif: "saisit sa cible entre ses bras",
fin: "lache sa cible",
},
estEcrasePar: {
activation: "est saisi",
activationF: "est saisie",
actif: "est saisi",
actifF: "est saisie",
fin: "se libère",
msgSave: "se libérer",
entrave: true
},
aGobe: {
activation: "avale sa cible",
actif: "a avalé une créature vivante",
fin: "digère sa cible"
},
estGobePar: {
activation: "est avalé",
activationF: "est avalée",
actif: "est dans le ventre d'une créature",
fin: "fin de la digestion, sort du ventre",
msgSave: "sortir du ventre",
entrave: true
},
inconfort: {
activation: "commence à être gêné par son armure",
activationF: "commence à être gênée par son armure",
actif: "est gêné par son armure",
actifF: "est gênée par son armure",
fin: "réajuste son armure",
},
noyade: {
activation: "commence à se noyer",
actif: "se noie",
fin: "peut à nouveau respirer",
msgSave: "surnager",
prejudiciable: true,
seulementVivant: true,
dm: true,
visible: true
},
blessureQuiSaigne: {
activation: "reçoit une blessure qui saigne",
actif: "saigne à cause d'une blessure",
fin: "saigne beaucoup moins",
msgSave: "ne plus saigner",
prejudiciable: true,
dm: true
},
poisonAffaiblissant: {
activation: "sent le poison ralentir ses mouvements",
actif: "est empoisonné",
actifF: "est empoisonnée",
fin: "le poison n'agit plus",
msgSave: "résister au poison",
prejudiciable: true,
seulementVivant: true,
},
};
const patternEffetsCombat = buildPatternEffets(messageEffetCombat);
function estEffetCombat(name) {
return (patternEffetsCombat.test(name));
}
const patternAttributEffetsCombat =
buildPatternEffets(messageEffetCombat, ['Puissant', 'Valeur', 'SaveParTour', 'SaveActifParTour', 'SaveParTourType', 'TempeteDeManaIntense', 'Options', 'TokenSide']);
function estAttributEffetCombat(name) {
return (patternAttributEffetsCombat.test(name));
}
function effetCombatOfAttribute(attr) {
let ef = attr.get('name');
if (ef === undefined || messageEffetCombat[ef]) return ef;
//D'abord on enlève le nom du token
let pu = ef.indexOf('_');
if (pu > 0) {
ef = ef.substring(0, pu);
if (messageEffetCombat[ef]) return ef;
}
error("Impossible de déterminer l'effet correspondant à " + ef, attr);
}
// Si un effet est prejudiciable, enlevé par délivrance
const messageEffetIndetermine = {
armesNaturelles: {
activation: "se fait pousser griffes et crocs",
actif: "a des griffes et des crocs",
fin: "n'a plus de griffes et crocs visibles"
},
charme: {
activation: "devient un ami de longue date",
activationF: "devient une amie de longue date",
actif: "est sous le charme de quelqu'un",
fin: "retrouve ses esprits",
prejudiciable: true
},
conditionsHostiles: {
activation: "se trouve dans des conditions hostiles",
actif: "est en conditions hostiles",
fin: "retrouve des conditions normales",
},
constructionTailleHumaine: {
activation: "rentre dans une construction de taille humaine.",
actif: "est un peu à l'étroit, le bâtiment est trop petit",
fin: "sort de la construction de taille humains."
},
dominationPsy: {
activation: "est maintenant sous le contrôle de quelqu'un",
actif: "est sous l'effet d'une domination psy",
fin: "retrouve le contrôle de son corps",
prejudiciable: true,
},
fievreux: {
activation: "se sent fiévreux",
activationF: "se sent fiévreuse",
actif: "est fiévreux",
actifF: "est fiévreuse",
fin: "se sent mieux",
prejudiciable: true
},
foretVivanteEnnemie: {
activation: "est gêné par la forêt",
activationF: "est gênée par la forêt",
actif: "est désorienté par la forêt",
actifF: "est désorientée par la forêt",
fin: "se retrouve dans une forêt normale",
entrave: true
},
grandeTaille: {
activation: "devient aussi grand qu'un humain",
activationF: "devient aussi grande qu'une humaine",
actif: "est grand comme un humain",
actifF: "est grande comme une humaine",
fin: "retrouve sa taille normale"
},
lameDeLigneePerdue: {
activation: "perd sa lame de lignée",
actif: "a perdu sa lame de lignée",
fin: "retrouve sa lame de lignée",
},
marcheSylvestre: {
activation: "se deplace maintenant en terrain difficile",
actif: "profite du terrain difficile",
fin: "est maintenant en terrain normal"
},
mutationCuirasse: {
activation: "endurcit sa peau",
actif: "a la peau recouverte d'une cuirasse",
fin: "retrouve une peau normale"
},
mutationEcaillesRouges: {
activation: "recouvre sa peau d'écailles rouges",
actif: "a la peau recouverte d'écailles rouges",
fin: "retrouve une peau normale"
},
mutationFourrureViolette: {
activation: "se fait pousser une fourrure violette",
actif: "a la peau recouverte d'une fourrure violette",
fin: "retrouve une peau normale"
},
mutationMusclesHypertrophies: {
activation: "devient plus musclé",
activationF: "devient plus musclée",
actif: "a les muscles hypertrophiés",
fin: "retrouve des muscles normaux",
},
mutationOuies: {
activation: "se fait pousser des ouïes",
actif: "possède des ouïes",
fin: "n'a plus d'ouïes"
},
mutationSangNoir: {
activation: "prend un teint plus sombre",
actif: "a le sang noir",
fin: "retrouve un sang normal"
},
mutationSilhouetteFiliforme: {
activation: "devient plus fin",
activationF: "devient plus fine",
actif: "a une silhouette filiforme",
fin: "retrouve une silhouette normale",
},
mutationSilhouetteMassive: {
activation: "devient plus massif",
activationF: "devient plus massive",
actif: "a une silhouette massive",
fin: "retrouve une silhouette normale",
},
presenceGlaciale: {
activation: "transforme son corps en glace vivante",
actif: "est formé de glace",
actifF: "est formée de glace",
fin: "retrouve un corps normal",
},
sensDuDevoir: {
activation: "escorte ou transporte une missive",
actif: "a une mission",
fin: "a fini sa mission",
},
sixiemeSens: {
activation: "fait un rituel de divination",
actif: "sait un peu à l'avance ce qu'il va se passer",
fin: "l'effet du rituel de divination prend fin",
},
espaceExigu: {
activation: "entre dans un espace exigu.",
actif: "est à l'étroit.",
fin: "sort de l'espace exigu."
},
sangDeLArbreCoeur: {
activation: "boit une potion de Sang de l'Arbre-Coeur",
actif: "a bu une potion de Sang de l'Arbre-Coeur",
fin: "les effets de la potion de Sang de l'Arbre-Coeur diminuent un peu"
},
ondesCorruptrices: { //nombre, à mettre avec !cof-effet ondesCorruptrices 2
activation: "se sent nauséeux",
activationF: "se sent nauséeuse",
actif: "se sent nauséeux",
actifF: "se sent nauséeuse",
fin: "se sent un peu mieux",
prejudiciable: true
},
petrifie: {
activation: "se change en pierre",
actif: "est transformé en pierre",
actifF: "est transformée en pierre",
fin: "n'est plus pétrifié",
finF: "n'est plus pétrifiée",
prejudiciable: true,
msgSave: "résister à la pétrification",
},
poisonAffaiblissantLong: {
activation: "sent le poison ralentir ses mouvements",
actif: "est empoisonné",
actifF: "est empoisonnée",
fin: "le poison n'agit plus",
msgSave: "résister au poison",
prejudiciable: true,
seulementVivant: true,
},
reactionAllergique: {
activation: "ressent de fortes démangeaisons",
actif: "est victime d'une réaction allergique",
fin: "les démangeaisons cessent",
prejudiciable: true
},
};
const patternEffetsIndetermine = buildPatternEffets(messageEffetIndetermine);
function estEffetIndetermine(name) {
return (patternEffetsIndetermine.test(name));
}
function effetIndetermineOfAttribute(attr) {
let ef = attr.get('name');
if (ef === undefined || messageEffetIndetermine[ef]) return ef;
//D'abord on enlève le nom du token
let pu = ef.indexOf('_');
if (pu > 0) {
ef = ef.substring(0, pu);
if (messageEffetIndetermine[ef]) return ef;
}
error("Impossible de déterminer l'effet correspondant à " + ef, attr);
}
function attributeExtending(charId, attrName, effetC, extension) {
let nameWithExtension = effetC + extension + attrName.substr(effetC.length);
return findObjs({
_type: 'attribute',
_characterid: charId,
name: nameWithExtension
});
}
//Nom de l'effet, avec la partie générique, mais sans le nom de token
function effetComplet(effet, attrName) {
if (effet == attrName) return effet;
//On a un effet lié à un token ou bien un effet générique
if (attrName.charAt(effet.length) == '(') {
let p = attrName.indexOf(')', effet.length);
if (p > 0) return attrName.substring(0, p + 1);
}
return effet;
}
function rollAndDealDmg(perso, dmg, type, effet, attrName, msg, count, evt, options, callback, display) {
if (options.valeur) {
let attrsVal = tokenAttribute(perso, options.valeur);
if (attrsVal.length > 0) {
dmg = attrsVal[0].get('current');
let dmgDice = parseDice(dmg);
if (dmgDice) {
if (dmgDice.nbDe === 0) dmg = {
cst: dmgDice.bonus
};
else if (dmgDice.bonus === 0) dmg = {
de: dmgDice.dice,
nbDe: dmgDice.nbDe
};
}
}
}
let dmgExpr = dmg;
let tdmi = attributeAsInt(perso, effet + 'TempeteDeManaIntense', 0);
if (dmg.de) {
if (tdmi) {
dmgExpr = (tdmi + dmg.nbDe) + 'd' + dmg.de;
removeTokenAttr(perso, effet + 'TempeteDeManaIntense', evt);
} else dmgExpr = dmg.nbDe + 'd' + dmg.de;
} else if (dmg.cst) {
if (tdmi) {
dmgExpr = dmg.cst * (1 + tdmi);
removeTokenAttr(perso, effet + 'TempeteDeManaIntense', evt);
} else dmgExpr = dmg.cst;
} else if (options.dotGen) {
//alors dmg = '' et type = ''
let valAttr = tokenAttribute(perso, effet + 'Valeur');
if (valAttr.length === 0) {
//Par défaut, 1d6 DM normaux
dmgExpr = "1d6";
type = 'normal';
} else {
dmgExpr = valAttr[0].get('current');
type = valAttr[0].get('max');
if (type === '') type = 'normal';
}
}
getEffectOptions(perso, effet, options);
sendChat('', "[[" + dmgExpr + "]]", function(res) {
let rolls = res[0];
let dmgRoll = rolls.inlinerolls[0];
let r = {
total: dmgRoll.results.total,
type: type,
display: buildinline(dmgRoll, type)
};
let explications;
if (display) explications = [];
if (options.saignement) {
let sourceDrain = saignementAvecDrain(perso, effet);
r.drainDeSang = sourceDrain;
}
dealDamage(perso, r, [], evt, false, options, explications,
function(dmgDisplay, dmg) {
if (dmg > 0) {
let msgDm;
if (msg) msgDm = msg + '. ' + onGenre(perso, 'Il', 'Elle');
else msgDm = '';
if (display) {
explications.forEach(function(m) {
addLineToFramedDisplay(display, m);
});
addLineToFramedDisplay(display, nomPerso(perso) + ' ' + msgDm + " subit " + dmgDisplay + " DM");
sendFramedDisplay(display);
} else if (effet == attrName) {
sendPerso(perso, msgDm + " subit " + dmgDisplay + " DM");
} else {
let tokenName = attrName.substring(attrName.indexOf('_') + 1);
sendChat('', tokenName + ' ' + msgDm + " subit " + dmgDisplay + " DM");
}
}
count.v--;
if (count.v === 0) callback();
});
}); //fin sendChat du jet de dé
}
//asynchrone
// effet est le nom complet de l'effet
function degatsParTour(charId, pageId, effet, attrName, dmg, type, msg, evt, options, callback) {
options = options || {};
let count;
iterTokensOfAttribute(charId, pageId, effet, attrName,
function(token, total) {
if (count === undefined) count = {
v: total
};
let perso = {
token,
charId
};
if (getState(perso, 'mort')) {
if (callback) callback();
return;
}
if (options.save) {
let playerId = getPlayerIds(perso);
let nameEffet = effet;
if (effet.startsWith('dotGen('))
nameEffet = effet.substring(7, effet.indexOf(')'));
let display = startFramedDisplay(playerId, "Effet de " + nameEffet, perso);
let saveId = "degatsParTour_" + effet + "_" + token.id;
let expliquer = function(m) {
addLineToFramedDisplay(display, m);
};
let msgPour = " pour ne pas prendre de dégâts de " + nameEffet;
let sujet = onGenre(perso, 'il', 'elle');
let msgReussite = ", " + sujet + " ne perd pas de PV ce tour";
let saveOpts = {
msgPour: msgPour,
msgReussite: msgReussite,
rolls: options.rolls,
chanceRollId: options.chanceRollId,
type: type
};
save(options.save, perso, saveId, expliquer, saveOpts, evt, function(reussite, texte) {
if (reussite) {
sendFramedDisplay(display);
return;
}
rollAndDealDmg(perso, dmg, type, effet, attrName, msg, count, evt, options, callback, display);
});
return;
}
rollAndDealDmg(perso, dmg, type, effet, attrName, msg, count, evt, options, callback);
}); //fin iterTokensOfAttribute
}
//asynchrone
function soigneParTour(charId, pageId, effet, attrName, soinsExpr, msg, evt, options, callback) {
options = options || {};
msg = msg || '';
let count = -1;
iterTokensOfAttribute(charId, pageId, effet, attrName,
function(token, total) {
if (count < 0) count = total;
const perso = {
token: token,
charId: charId
};
let tdmi = attributeAsInt(perso, effet + "TempeteDeManaIntense", 0);
if (tdmi) {
soinsExpr = "(" + soinsExpr + ")*" + (1 + tdmi);
removeTokenAttr(perso, effet + "TempeteDeManaIntense", evt);
}
let localSoinsExpr = soinsExpr;
if (options.valeur) {
let attrsVal = tokenAttribute(perso, options.valeur);
if (attrsVal.length > 0) localSoinsExpr = attrsVal[0].get('current');
}
sendChat('', "[[" + localSoinsExpr + "]]", function(res) {
let rolls = res[0];
let soinRoll = rolls.inlinerolls[0];
let soins = soinRoll.results.total;
let displaySoins = buildinline(soinRoll, 'normal', true);
soigneToken(perso, soins, evt,
function(s) {
if (s < soins) sendPerso(perso, "récupère tous ses PV.");
else sendPerso(perso, "récupère " + displaySoins + " PV.");
count--;
if (count === 0) callback();
},
function() {
count--;
if (count === 0) callback();
});
}); //fin sendChat du jet de dé
}); //fin iterTokensOfAttribute
}
function getEffectOptions(perso, effet, options) {
options = options || {};
let optionsAttr = tokenAttribute(perso, effet + 'Options');
optionsAttr.forEach(function(oAttr) {
parseDmgOptions(oAttr.get('current'), options);
});
copyDmgOptionsToTarget(perso, options);
return options;
}
// gestion des effets qui se déclenchent à la fin de chaque tour
// N'ajoute pas evt à l'historique
// Asynchrone (à cause des saves par tour)
function changementDeTour(tour, attrs, evt, combat, pageId, options) {
// Enlever les bonus d'un tour
attrs = removeAllAttributes('limiteParTour', evt, attrs);
attrs = removeAllAttributes('actionConcertee', evt, attrs);
attrs = removeAllAttributes('interposer', evt, attrs);
attrs = removeAllAttributes('attaqueMalgreMenace', evt, attrs);
attrs = removeAllAttributes('prescienceUtilisee', evt, attrs);
attrs = removeAllAttributes('increvableHumainUtilise', evt, attrs);
resetAttr(attrs, 'cercleDeProtectionActif', evt);
// Pour défaut dans la cuirasse, on diminue si la valeur est 2, et on supprime si c'est 1
let defautsDansLaCuirasse = allAttributesNamed(attrs, 'defautDansLaCuirasse');
defautsDansLaCuirasse.forEach(function(attr) {
if (attr.get('current') < 2) {
if (evt.deletedAttributes) evt.deletedAttributes.push(attr);
else evt.deletedAttributes = [attr];
attr.remove();
} else {
let prevAttr = {
attribute: attr,
current: 2
};
evt.attributes.push(prevAttr);
attr.set('current', 1);
}
});
// Pour la feinte, on augmente la valeur, et on supprime si la valeur est 2
let feinte = allAttributesNamed(attrs, 'feinte');
feinte.forEach(function(attr) {
let valFeinte = parseInt(attr.get('current'));
if (isNaN(valFeinte) || valFeinte > 0) {
evt.deletedAttributes.push(attr);
attr.remove();
} else {
let prevAttr = {
attribute: attr,
current: 0
};
evt.attributes.push(prevAttr);
attr.set('current', 1);
}
});
//Les tests ratés
let trTour = allAttributesNamed(attrs, 'testsRatesDuTour');
trTour.forEach(function(tr) {
let curTr = tr.get('current');
if (curTr === '') {
evt.deletedAttributes.push(tr);
tr.remove();
} else {
let maxTr = tr.get('max');
evt.attributes.push({
attribute: tr,
current: curTr,
max: maxTr
});
tr.set('max', curTr);
tr.set('current', '');
}
});
let vapeth = allAttributesNamed(attrs, 'vapeursEthyliques');
vapeth.forEach(function(attr) {
let ve = parseInt(attr.get('current'));
if (isNaN(ve)) ve = 0;
evt.attributes.push({
attribute: attr,
current: 0
});
attr.set('current', ve + 1);
let veCharId = attr.get('characterid');
if (veCharId === undefined || veCharId === '') {
error("Attribut sans personnage associé", attr);
return;
}
let veSeuil = parseInt(attr.get('max'));
if (isNaN(veSeuil)) veSeuil = 0;
veSeuil -= Math.floor(ve / 2);
iterTokensOfAttribute(veCharId, combat.pageId,
'vapeursEthyliques', attr.get('name'),
function(tok) {
let perso = {
charId: veCharId,
token: tok
};
let testId = 'vapeursEthyliques_' + perso.token.id;
testCaracteristique(perso, 'CON', veSeuil, testId, options, evt, function(testRes) {
let res = "tente un jet de CON " + veSeuil + " pour combattre les vapeurs éthyliques " + testRes.texte;
if (testRes.reussite) {
res += " => réussi." + testRes.modifiers;
let expliquer;
if (attr.get('name') == 'vapeursEthyliques') {
expliquer = function(s) {
sendChar(veCharId, s, true);
};
} else {
expliquer = function(s) {
sendChat('', nomPerso(perso) + ' ' + s);
};
}
expliquer(res);
diminueEbriete(perso, evt, expliquer);
} else {
res += " => raté" + testRes.rerolls + testRes.modifiers;
sendPerso(perso, res);
}
});
});
});
// nouveau tour : enlever le statut surpris
// et faire les actions de début de tour
let selected = [];
updateNextInitSet.forEach(function(id) {
selected.push({
_id: id
});
});
let allTokens = findObjs({
_type: 'graphic',
_subtype: 'token',
_pageid: pageId,
layer: 'objects'
});
let allPersos = [];
allTokens.forEach(function(token) {
let charId = token.get('represents');
if (charId === '') return;
let c = getObj('character', charId);
if (c === undefined) {
token.remove();
return;
}
allPersos.push({
token,
charId
});
});
allPersos.forEach(function(perso) {
if (getState(perso, 'surpris')) { //surprise
setState(perso, 'surpris', false, {});
selected.push({
_id: perso.token.id
});
}
if (getState(perso, 'enseveli')) {
let degats = rollDePlus(6, {
nbDes: 2
});
let dmg = {
type: 'magique',
total: degats.val,
display: degats.roll
};
dealDamage(perso, dmg, [], evt, false, {}, undefined,
function(dmgDisplay, dmgFinal) {
sendPerso(perso, " est écrasé ! " +
onGenre(perso, 'Il', 'Elle') + " subit " + dmgDisplay + " DM");
});
}
let enflammeAttr = tokenAttribute(perso, 'enflamme');
if (enflammeAttr.length > 0) {
let enflamme = parseInt(enflammeAttr[0].get('current'));
// Pour ne pas faire les dégâts plusieurs fois (plusieurs tokens pour un même personnage), on utilise la valeur max de l'attribut
let dernierTourEnflamme = parseInt(enflammeAttr[0].get('max'));
if ((isNaN(dernierTourEnflamme) || dernierTourEnflamme < tour) &&
!isNaN(enflamme) && enflamme > 0) {
let optFlammes = {
resultatDesSeuls: true,
bonus: enflamme - 1
};
let {
val,
roll
} = rollDePlus(6, optFlammes);
let d6Enflamme = optFlammes.resultatDesSeuls;
let dmgEnflamme = {
type: 'feu',
total: val,
display: roll
};
if (getState(perso, 'mort')) {
sendChat('', "Le cadavre de " + nomPerso(perso) + " continue de brûler");
} else {
dealDamage(perso, dmgEnflamme, [], evt, false, {}, undefined,
function(dmgDisplay, dmgFinal) {
sendPerso(perso, " est en flamme ! " +
onGenre(perso, 'Il', 'Elle') + " subit " + dmgDisplay + " DM");
});
}
if (d6Enflamme < 3) {
sendPerso(perso, ": les flammes s'éteignent");
removeTokenAttr(perso, 'enflamme', evt);
let ms = messageEffetCombat.enflamme.statusMarker;
if (ms) {
affectToken(perso.token, 'statusmarkers', perso.token.get('statusmarkers'), evt);
perso.token.set('status_' + ms, false);
}
} else {
enflammeAttr[0].set('max', tour);
}
}
}
if (attributeAsBool(perso, 'estGobePar') && !getState(perso, 'mort')) {
let jet = rollDePlus(6, {
nbDes: 3
});
let dmg = {
type: 'normal', //correspond à de l'asphyxie
total: jet.val,
display: jet.roll
};
if (immuniseAsphyxie(perso)) dmg.type = 'acide';
dealDamage(perso, dmg, [], evt, false, {}, undefined,
function(dmgDisplay, dmgFinal) {
sendPerso(perso, "est en train d'être digéré. " + onGenre(perso, 'Il', 'Elle') + " perd " + dmgDisplay + " PVs");
});
}
if (attributeAsBool(perso, 'blessureQuiSaigne') &&
!getState(perso, 'mort') &&
!predicateAsBool(perso, 'immuniteSaignement') &&
!predicateAsBool(perso, 'controleSanguin')) {
let jetSaignement = rollDePlus(6);
let dmgSaignement = {
type: 'normal',
total: jetSaignement.val,
display: jetSaignement.roll
};
let optDMSaignements = getEffectOptions(perso, 'blessureQuiSaigne');
dealDamage(perso, dmgSaignement, [], evt, false, optDMSaignements, undefined,
function(dmgDisplay, dmgFinal) {
sendPerso(perso, "saigne. " + onGenre(perso, 'Il', 'Elle') + " perd " + dmgDisplay + " PVs");
});
}
if (attributeAsBool(perso, 'noyade') && !getState(perso, 'mort') && !immuniseAsphyxie(perso)) {
let playerId = getPlayerIds(perso);
let display = startFramedDisplay(playerId, "Noyade", perso);
let saveId = "Noyade_" + perso.token.id;
let expliquer = function(m) {
addLineToFramedDisplay(display, m);
};
let msgPour = " pour ne pas prendre de dégâts de noyade";
let sujet = onGenre(perso, 'il', 'elle');
let msgReussite = ", " + sujet + " ne perd pas de PV ce tour";
let saveOpts = {
msgPour: msgPour,
msgReussite: msgReussite,
rolls: options.rolls,
chanceRollId: options.chanceRollId,
type: 'normal'
};
let saveNoyade = {
carac: 'CON',
seuil: 15
};
save(saveNoyade, perso, saveId, expliquer, saveOpts, evt, function(reussite, texte) {
if (reussite) {
sendFramedDisplay(display);
return;
}
let jetNoyade = rollDePlus(6);
let dmNoyade = {
type: 'normal',
total: jetNoyade.val,
display: jetNoyade.roll
};
let explications = [];
dealDamage(perso, dmNoyade, [], evt, false, options, explications,
function(dmgDisplay, dmg) {
explications.forEach(function(m) {
addLineToFramedDisplay(display, m);
});
addLineToFramedDisplay(display, nomPerso(perso) + "se noie et subit " + dmgDisplay + " DM");
sendFramedDisplay(display);
});
});
}
let vitaliteSurnaturelle = predicateAsBool(perso, 'vitaliteSurnaturelle');
if (vitaliteSurnaturelle) {
let indexType = vitaliteSurnaturelle.indexOf('/');
let vitaliteSurnat = vitaliteSurnaturelle;
if (indexType > 0)
vitaliteSurnat = vitaliteSurnat.substring(0, indexType);
vitaliteSurnat = vitaliteSurnat.trim();
let regenereMemeMort;
if ((vitaliteSurnat + '').endsWith('+')) {
vitaliteSurnat = vitaliteSurnat.substr(0, vitaliteSurnat.length - 1);
regenereMemeMort = true;
}
if (regenereMemeMort || !getState(perso, 'mort')) {
vitaliteSurnat = parseInt(vitaliteSurnat);
if (vitaliteSurnat > 0) {
let saufDMType;
if (indexType > 0 && indexType < vitaliteSurnaturelle.length - 1)
saufDMType = vitaliteSurnaturelle.substring(indexType + 1).split(',');
soigneToken(perso, vitaliteSurnat, evt,
function(s) {
whisperChar(perso.charId, 'récupère ' + s + ' PVs.');
},
function() {}, {
saufDMType
}
);
}
}
}
if (attributeAsBool(perso, 'sangDeLArbreCoeur') && !getState(perso, 'mort')) {
soigneToken(perso, 5, evt,
function(s) {
whisperChar(perso.charId, "régénère " + s + " PVs. (grâce à la potion de sang de l'Arbre Coeur)");
},
function() {}
);
}
let increvableActif = tokenAttribute(perso, 'increvableActif');
if (increvableActif.length > 0) {
increvableActif[0].remove();
let soins = randomInteger(6) + randomInteger(6) + randomInteger(6) + modCarac(perso, 'constitution');
soigneToken(perso, soins, evt, function(soinsEffectifs) {
let msgSoins = "est increvable et récupère ";
if (soinsEffectifs == soins) msgSoins += soins + " points de vie";
else msgSoins += soinsEffectifs + " PV (le jet était " + soins + ")";
whisperChar(perso.charId, msgSoins);
});
}
let degradationZombie = attributeAsInt(perso, 'degradationZombie', -1);
if (degradationZombie % 6 === 0) {
let r = {
total: 1,
type: 'normal',
display: 1
};
perso.ignoreTouteRD = true;
dealDamage(perso, r, [], evt, false, {}, [], function() {
// Vérification si le Zombie est toujours vivant
let token = getObj('graphic', perso.token.id);
if (token) whisperChar(perso.charId, "se dégrade et perd 1 PV");
});
}
});
setActiveToken(combat, undefined, evt);
initiative(selected, evt, true); // met Tour à la fin et retrie
updateNextInitSet = new Set();
// Saves à faire à la fin de chaque tour. Asynchrone, mais pas grave ?
attrs.forEach(function(attr) {
let attrName = attr.get('name');
let indexSave = attrName.indexOf('SaveParTour');
if (indexSave < 0) return;
let indexSaveType = attrName.indexOf('SaveParTourType');
if (indexSaveType > 0) return;
let effetC = attrName.substring(0, indexSave);
let effetTemp = estEffetTemp(effetC);
if (!cof_states[effetC] && !effetTemp && !estEffetCombat(effetC)) return;
let carac = attr.get('current');
if (!isCarac(carac)) {
error("Save par tour " + attrName + " mal formé", carac);
return;
}
let seuil = parseInt(attr.get('max'));
if (isNaN(seuil)) {
error("Save par tour " + attrName + " mal formé", seuil);
return;
}
let charId = attr.get('characterid');
attrName = effetC + attrName.substr(indexSave + 11);
let token;
iterTokensOfAttribute(charId, pageId, effetC, attrName, function(tok) {
if (token === undefined) token = tok;
});
if (token === undefined) {
log("Pas de token pour le save " + attrName);
return;
}
const perso = {
token: token,
charId: charId
};
if (getState(perso, 'mort')) {
return;
}
let expliquer = function(msg) {
sendPerso(perso, msg);
};
let met;
if (effetTemp) met = messageOfEffetTemp(effetC);
else if (cof_states[effetC]) {
let se = stringOfEtat(effetC, perso);
met = {
etat: true,
msgSave: "ne plus être " + se,
fin: "n'est plus " + se,
actif: "est toujours " + se
};
} else met = messageEffetCombat[effetC];
let msgPour = " pour ";
if (met.msgSave) msgPour += met.msgSave;
else {
msgPour += "ne plus être sous l'effet de ";
if (effetC.startsWith('dotGen('))
msgPour += effetC.substring(7, effetC.indexOf(')'));
else msgPour += effetC;
}
let sujet = onGenre(perso, 'il', 'elle');
let msgReussite = ", " + sujet + ' ' + messageFin(perso, met);
let msgRate = ", " + sujet + ' ' + messageActif(perso, met);
let saveOpts = {
msgPour: msgPour,
msgReussite: msgReussite,
msgRate: msgRate,
rolls: options.rolls,
chanceRollId: options.chanceRollId
};
let attrType = findObjs({
_type: 'attribute',
_characterid: charId,
name: attr.get('name').replace('SaveParTour', 'SaveParTourType')
});
if (attrType.length > 0) {
saveOpts.type = attrType[0].get('current');
}
let attrEffet;
if (met.etat) {
attrEffet = {
id: effetC
};
} else {
attrEffet = findObjs({
_type: 'attribute',
_characterid: charId,
name: attrName
});
if (attrEffet === undefined || attrEffet.length === 0) {
if (getObj('attribute', attr.id)) {
error("Save sans effet " + attrName, attr);
findObjs({
_type: 'attribute',
_characterid: charId,
name: attr.get('name').replace('SaveParTour', 'SaveParTourType')
}).forEach(function(a) {
a.remove();
});
attr.remove();
}
return;
}
attrEffet = attrEffet[0];
}
let saveId = 'saveParTour_' + attrEffet.id + '_' + perso.token.id;
let s = {
carac: carac,
seuil: seuil,
entrave: met.entrave
};
save(s, perso, saveId, expliquer, saveOpts, evt,
function(reussite, texte) { //asynchrone
if (reussite) {
if (met.etat) {
setState(perso, effetC, false, evt);
} else {
let eff = effetC;
if (effetTemp) eff = effetTempOfAttribute(attrEffet);
finDEffet(attrEffet, eff, attrName, charId, evt, {
attrSave: attr,
pageId: pageId
});
}
}
});
}); //fin boucle attrSave
let armeesDesMorts = allAttributesNamed(attrs, 'armeeDesMorts');
let degatsArmeeFull = {};
let degatsArmeeDefense = {};
let gmId;
armeesDesMorts.forEach(function(armee) {
let charId = armee.get('characterid');
let boost = 0;
if (charAttribute(charId, "armeeDesMortsPuissant").length > 0) boost = 1;
else boost = attrAsInt(charAttribute(charId, "armeeDesMortsTempeteDeManaIntense"), 0);
let rayon = Math.floor(20 * Math.sqrt(1 + boost));
let allies = alliesParPerso[charId] || new Set();
//Pour chaque token representant ce perso
allPersos.forEach(function(perso) {
if (perso.charId != charId) return;
//On cherche ensuite les tokens à portee
allPersos.forEach(function(target) {
if (target.token.id == perso.token.id) return;
let tokRepresents = target.charId;
if (tokRepresents == charId) return;
if (allies.has(tokRepresents)) return;
if (degatsArmeeDefense[target.token.id] != undefined || degatsArmeeFull[target.token.id] != undefined) return;
if (predicateAsBool(perso, 'volant')) return;
if (distanceCombat(perso.token, target.token, pageId) > rayon) return;
if (attributeAsBool(target, 'defenseArmeeDesMorts')) {
degatsArmeeDefense[target.token.id] = target;
} else {
degatsArmeeFull[target.token.id] = target;
}
});
});
if (!gmId) {
gmId = getGMId();
if (!gmId) {
error("Impossible de trouver un MJ");
return;
}
}
});
let targetLine = '';
Object.keys(degatsArmeeFull).forEach(function(tokId) {
targetLine += " --target " + tokId;
});
if (targetLine != "")
sendChat('player|' + gmId, "!cof-dmg 3d6" + targetLine + " --titre Dégâts des morts-vivants animés");
targetLine = "";
Object.keys(degatsArmeeDefense).forEach(function(tokId) {
targetLine += " --target " + tokId;
});
if (targetLine != "")
sendChat('player|' + gmId, "!cof-dmg 1d6" + targetLine + " --titre Dégâts des morts-vivants animés sur les cibles qui les combattent");
removeAllAttributes("defenseArmeeDesMorts", evt, attrs);
if (stateCOF.prescience) {
//On affiche la prescience aux joueurs concernés
allPersos.forEach(function(perso) {
if (capaciteDisponible(perso, 'prescience', 'combat')) {
whisperChar(perso.charId, "Possibilité d'utiliser la " + boutonSimple('!cof-prescience ' + perso.token.id + ' --mana 2', "Prescience"));
}
});
stateCOF.nextPrescience = {
evt: evt,
dernieresPositions: []
};
allTokens.forEach(function(tok) {
stateCOF.nextPrescience.dernieresPositions.push({
token: tok,
left: tok.get('left'),
top: tok.get('top')
});
});
}
if (stateCOF.tenebresMagiques && stateCOF.tenebresMagiques.attaques) {
stateCOF.tenebresMagiques.attaques = undefined;
}
}
//evt a un champ attributes et un champ deletedAttributes
//evt n'est pas ajouté à l'historique dans cette fonction
function nextTurnOfActive(active, attrs, evt, combat, pageId, options) {
if (active === undefined) return;
if (active.id == "-1" && active.custom == "Tour") { //Nouveau tour
let tour = parseInt(active.pr);
if (isNaN(tour)) {
error("Tour invalide", active);
return;
}
if (!evt.combat) evt.combat = {...combat
};
evt.combat.tour = tour - 1;
evt.updateNextInitSet = updateNextInitSet;
active.pr = tour - 1; // préparation au calcul de l'undo
sendChat("GM", "Début du tour " + tour);
combat.tour = tour;
combat.init = 1000;
changementDeTour(tour, attrs, evt, combat, pageId, options);
} else { // change le token actif
setActiveToken(combat, active.id, evt);
}
}
function actionEffet(attr, effet, attrName, charId, pageId, evt, callBack) {
switch (effet) {
case 'putrefaction': //prend 1d6 DM
degatsParTour(charId, pageId, effet, attrName, {
nbDe: 1,
de: 6
}, 'maladie',
"pourrit", evt, {
magique: true
}, callBack);
return;
case 'asphyxie': //prend 1d6 DM
degatsParTour(charId, pageId, effet, attrName, {
nbDe: 1,
de: 6
}, 'normal',
"ne peut plus respirer", evt, {
asphyxie: true
}, callBack);
return;
case 'saignementsSang': //prend 1d6 DM
if (charPredicateAsBool(charId, 'immuniteSaignement') ||
charPredicateAsBool(charId, 'controleSanguin')) {
callBack();
return;
}
degatsParTour(charId, pageId, effet, attrName, {
nbDe: 1,
de: 6
}, 'normal',
"saigne par tous les orifices du visage", evt, {
magique: true,
saignement: true
}, callBack);
return;
case 'blessureSanglante': //prend 1d6 DM
if (charPredicateAsBool(charId, 'immuniteSaignement') ||
charPredicateAsBool(charId, 'controleSanguin')) {
callBack();
return;
}
degatsParTour(charId, pageId, effet, attrName, {
nbDe: 1,
de: 6
}, 'normal',
"saigne abondamment", evt, {
saignement: true
}, callBack);
return;
case 'armureBrulante': //prend 1d4 DM
degatsParTour(charId, pageId, effet, attrName, {
nbDe: 1,
de: 4
}, 'feu',
"brûle dans son armure", evt, {
valeur: 'armureBrulanteValeur'
}, callBack);
return;
case 'nueeDInsectes': //prend 1 DM
degatsParTour(charId, pageId, effet, attrName, {
cst: 1
}, 'normal',
"est piqué par les insectes", evt, {
valeur: 'nueeDInsectesValeur'
}, callBack);
return;
case 'nueeDeCriquets': //prend 1 DM
degatsParTour(charId, pageId, effet, attrName, {
cst: 2
}, 'normal',
"est piqué par les criquets", evt, {}, callBack);
return;
case 'nueeDeScorpions': //prend 1D6 DM
degatsParTour(charId, pageId, effet, attrName, {
nbDe: 1,
de: 6
}, 'normal',
"est piqué par les scorpions", evt, {}, callBack);
return;
case 'armeBrulante': //prend 1 DM
degatsParTour(charId, pageId, effet, attrName, {
cst: 1
}, 'feu',
"se brûle avec son arme", evt, {
valeur: 'armeBrulanteValeur'
}, callBack);
return;
case 'regeneration': //soigne
soigneParTour(charId, pageId, effet, attrName, 3, "régénère", evt, {
valeur: 'regenerationValeur'
}, callBack);
return;
case 'strangulation':
let nameDureeStrang = 'dureeStrangulation';
if (effet != attrName) { //concerne un token non lié
nameDureeStrang += attrName.substring(attrName.indexOf('_'));
}
let dureeStrang = findObjs({
_type: 'attribute',
_characterid: charId,
name: nameDureeStrang
});
if (dureeStrang.length === 0) {
let attrDuree = createObj('attribute', {
characterid: charId,
name: nameDureeStrang,
current: 0,
max: false
});
evt.attributes.push({
attribute: attrDuree,
});
} else {
let strangUpdate = dureeStrang[0].get('max');
if (strangUpdate) { //a été mis à jour il y a au plus 1 tour
evt.attributes.push({
attribute: dureeStrang[0],
current: dureeStrang[0].get('current'),
max: strangUpdate
});
dureeStrang[0].set('max', false);
} else { //Ça fait trop longtemps, on arrête tout
sendChar(charId, messageFin({
charId
}, messageEffetTemp[effet]), true);
evt.attributes.pop(); //On enlève des attributs modifiés pour mettre dans les attribute supprimés.
evt.deletedAttributes.push(attr);
attr.remove();
evt.deletedAttributes.push(dureeStrang[0]);
dureeStrang[0].remove();
}
}
callBack();
return;
case 'dotGen':
{
let effetC = effetComplet(effet, attrName);
degatsParTour(charId, pageId, effetC, attrName, {}, '', "", evt, {
dotGen: true
}, callBack);
return;
}
case 'zoneDeVie':
{
let effetC = effetComplet(effet, attrName);
let count = -1;
let fin = function() {
count--;
if (count === 0 && callBack) callBack();
};
iterTokensOfAttribute(charId, pageId, effetC, attrName,
function(token, total) {
if (count < 0) count = total;
let perso = {
charId,
token
};
if (getState(perso, 'mort')) {
fin();
return;
}
let attrTok = tokenAttribute(perso, effetC + 'Id');
if (attrTok.length === 0) {
fin();
return;
}
let tokId = attrTok[0].get('current');
let tokZone = getObj('graphic', tokId);
if (!tokZone) {
attrTok[0].remove();
fin();
return;
}
let allies = alliesParPerso[charId] || new Set();
allies.add(charId);
let pageId = tokZone.get('pageid');
let cx = tokZone.get('left');
let cy = tokZone.get('top');
let dx = tokZone.get('width');
let dy = tokZone.get('height');
let minx = cx - dx / 2;
let maxx = cx + dx / 2;
let miny = cy - dy / 2;
let maxy = cy + dy / 2;
let allToks = findObjs({
_type: 'graphic',
_pageid: pageId,
_subtype: 'token',
});
let cibles = [];
allToks.forEach(function(tok) {
let tx = tok.get('left');
if (tx < minx || tx > maxx) return;
let ty = tok.get('top');
if (ty < miny || ty > maxy) return;
let cible = persoOfToken(tok);
if (!cible || !allies.has(cible.charId) || getState(cible, 'mort')) return;
cibles.push(cible);
});
if (cibles.length === 0) {
fin();
return;
}
count--; //On a fini avec perso.
count += cibles.length; //On ajoute les cibles
cibles.forEach(function(cible) {
sendChat('', "[[2d6]]", function(res) {
let rolls = res[0];
let soinRoll = rolls.inlinerolls[0];
let soins = soinRoll.results.total;
let displaySoins = buildinline(soinRoll, 'normal', true);
soigneToken(cible, soins, evt,
function(s) {
if (s < soins) sendPerso(cible, "récupère tous ses PV.");
else if (s == soins)
sendPerso(cible, "récupère " + displaySoins + " PV.");
else
sendPerso(cible, "récupère " + s + " PV. (Le jet était " + displaySoins + ")");
fin();
}, fin);
}); //fin sendChat du jet de dé
});
});
return;
}
default:
callBack();
return;
}
}
//Appelé si le turn order change, mais aussi en interne
//si evt est déjà défini, ne l'ajoute pas au turn order
function nextTurn(cmp, options, evt) {
if (!cmp.get('initiativepage')) return;
let combat = stateCOF.combat;
if (!combat) {
error("Le script n'est pas en mode combat", cmp);
return;
}
let turnOrder = cmp.get('turnorder');
let pageId = combat.pageId;
if (pageId === undefined) {
pageId = cmp.get('playerpageid');
combat.pageId = pageId;
}
if (turnOrder === '') return; // nothing in the turn order
turnOrder = JSON.parse(turnOrder);
if (turnOrder.length < 1) return; // Juste le compteur de tour
if (stateCOF.nextPrescience) {
stateCOF.prescience = stateCOF.nextPrescience;
stateCOF.nextPrescience = undefined;
}
let active = turnOrder[0];
let init = parseInt(active.pr);
if (active.id == "-1" && active.custom == "Tour") {
let tour = init; //= parseInt(active.pr);
init = 0;
if (isNaN(tour)) {
error("Le tour n'est pas un nombre");
return;
}
turnOrder[0] = {...active
};
turnOrder[0].pr = tour - 1;
}
let lastHead = turnOrder.pop();
turnOrder.unshift(lastHead);
if (evt === undefined) {
evt = {
type: 'nextTurn',
attributes: [],
deletedAttributes: [],
turnorder: JSON.stringify(turnOrder)
};
addEvent(evt);
} else {
evt.attributes = evt.attributes || [];
evt.deletedAttributes = evt.deletedAttributes || [];
evt.turnorder = evt.turnorder || JSON.stringify(turnOrder);
}
if (stateCOF.chargeFantastique) {
//cmp.set('turnorder', evt.turnorder);
if (stateCOF.chargeFantastique.attaques) {
nextTurnChargeFantastique(undefined, evt.turnorder);
return;
}
stateCOF.chargeFantastique = undefined;
}
let attrs = findObjs({
_type: 'attribute'
});
// Si on a changé d'initiative, alors diminue les effets temporaires
if (combat.init > init) {
if (stateCOF.tokensTemps && stateCOF.tokensTemps.length > 0) {
stateCOF.tokensTemps = stateCOF.tokensTemps.filter(function(tt) {
if (init < tt.init && tt.init <= combat.init) {
if (tt.duree > 1) {
evt.tokensTemps = evt.tokensTemps || [];
evt.tokensTemps.push({
tt,
ancienneDuree: tt.duree
});
tt.duree--;
return true;
} else {
if (tt.intrusion) tt.pasDExplosion = true;
deleteTokenTemp(tt, evt);
return false;
}
} else {
return true;
}
});
}
//attrsTemp ne contient que les attributs dont la durée doit baisser
let attrsTemp = attrs.filter(function(obj) {
if (!estEffetTemp(obj.get('name'))) return false;
let obji = obj.get('max');
return (init <= obji && obji < combat.init) || (init === 0 && obji == 1000);
});
if (!evt.combat) evt.combat = {...stateCOF.combat
};
combat.init = init;
// Boucle sur les effets temps peut être asynchrone à cause des DM
let count = attrsTemp.length;
if (count === 0) {
nextTurnOfActive(active, attrs, evt, combat, pageId, options);
return;
}
let fin = function() {
count--;
if (count === 0) nextTurnOfActive(active, attrs, evt, combat, pageId, options);
};
attrsTemp.forEach(function(attr) {
let charId = attr.get('characterid');
const effet = effetTempOfAttribute(attr);
if (effet === undefined) {
//erreur, on stoppe tout
log(attr);
fin();
return;
}
let attrName = attr.get('name');
let effetC = effetComplet(effet, attrName);
let v = parseInt(attr.get('current'));
if (isNaN(v)) v = 1;
if (v <= 1) { //L'effet arrive en fin de vie, doit être supprimé
//Sauf si on a accumulé plusieurs fois l'effet
let accumuleAttr = attributeExtending(charId, attrName, effetC, 'DureeAccumulee');
if (accumuleAttr.length > 0) {
accumuleAttr = accumuleAttr[0];
let dureeAccumulee = accumuleAttr.get('current') + '';
let listeDureeAccumulee = dureeAccumulee.split(',');
evt.attributes.push({
attribute: attr,
current: v
});
let nDuree = parseInt(listeDureeAccumulee.pop());
if (isNaN(nDuree)) {
v = 1;
fin();
return;
} else v = nDuree + 1; //car on va le diminuer plus bas.
if (listeDureeAccumulee.length === 0) {
evt.deletedAttributes.push(accumuleAttr);
accumuleAttr.remove();
} else {
evt.attributes.push({
attribute: accumuleAttr,
current: dureeAccumulee
});
accumuleAttr.set('current', listeDureeAccumulee.join(','));
}
} else {
//L'action finale
actionEffet(attr, effet, attrName, charId, pageId, evt, function() {
let effetFinal = finDEffet(attr, effet, attrName, charId, evt, {
pageId
});
if (effetFinal && effetFinal.oldTokenId == active.id) {
active.id = effetFinal.newTokenId;
if (active.id === undefined) {} else if (active.id == '-1') {
active.custom = 'Tour';
}
}
fin();
});
return;
}
}
//Effet encore actif
evt.attributes.push({
attribute: attr,
current: v
});
if (v > 1) attr.set('current', v - 1);
actionEffet(attr, effet, attrName, charId, pageId, evt, fin);
}); //fin de la boucle sur tous les attributs d'effets temporaires
} else { //L'initiative n'a pas bougée
nextTurnOfActive(active, attrs, evt, combat, pageId, options);
}
}
//Fonction appelée par !cof-tour-suivant
function tourSuivant(msg) {
let combat = stateCOF.combat;
if (!combat) {
sendPlayer(msg, "Vous n'êtes pas en combat");
return;
}
let cmp = Campaign();
let turnOrder = cmp.get('turnorder');
if (turnOrder === '') {
error("Personne n'est en combat", turnOrder);
return;
}
turnOrder = JSON.parse(turnOrder);
if (turnOrder.length < 1) {
error("Personne n'est en combat", turnOrder);
return;
}
let active = turnOrder.shift();
let persoActif = persoOfId(active.id);
if (persoActif === undefined) {
error("Impossible de trouver le personnage actif", active);
return;
}
if (!peutController(msg, persoActif)) {
sendPlayer(msg, "Ce n'est pas votre tour (personnage actif : " + nomPerso(persoActif) + ")");
return;
}
turnOrder.push(active);
if (turnOrder[0].id == "-1" && turnOrder[0].custom == "Tour") {
//Il faut aussi augmenter la valeur du tour
let tour = parseInt(turnOrder[0].pr);
if (isNaN(tour)) {
error("Tour invalide", turnOrder);
return;
}
turnOrder[0].pr = tour + 1;
}
cmp.set('turnorder', JSON.stringify(turnOrder));
nextTurn(cmp);
}
//nb à 11 pour ne pas retenter de lire les attributs
function scriptVersionToCharacter(character, nb) {
let charId = character.id;
//On vérifie que les attributs sont peuplés
let attrs = findObjs({
_type: 'attribute',
_characterid: charId,
});
if (attrs.length === 0) {
nb = nb || 1;
if (nb < 9) {
_.delay(function() {
scriptVersionToCharacter(character, nb + 1);
}, 2000);
return;
}
}
attrs = findObjs({
_type: 'attribute',
_characterid: charId,
name: 'scriptVersion',
}, {
caseInsensitive: true
});
if (attrs.length === 0) {
let attr = createObj('attribute', {
characterid: charId,
name: 'scriptVersion',
current: true,
max: state.COFantasy.version
});
attr.setWithWorker({
current: true,
max: state.COFantasy.version
});
} else {
if (attrs.length > 1) {
for (let i = 1; i < attrs.length; i++) {
attrs[i].remove();
}
}
attrs[0].setWithWorker({
current: true,
max: state.COFantasy.version
});
}
}
function destroyToken(token) { //to remove unused local attributes
let charId = token.get('represents');
if (charId === '') return;
let perso = {
charId: charId,
token: token
};
let pageId = token.get('pageid');
let nomToken = token.get('name');
if (nomToken.startsWith('decoince ')) {
let originalToken = findObjs({
_type: 'graphic',
_pageid: pageId,
represents: charId,
name: nomToken.substring(9)
});
if (originalToken.length === 0) return;
perso.token = originalToken[0];
removeTokenAttr(perso, 'bougeGraceA');
return;
}
//On regarde si il existe une copie de ce token, par exemple à cause de l'invisibilité
let otherTokens = findObjs({
_type: 'graphic',
_pageid: pageId,
represents: charId,
name: nomToken
});
if (otherTokens.length > 0) return;
let tokenBougeAttr = tokenAttribute(perso, 'bougeGraceA');
tokenBougeAttr.forEach(function(a) {
let tokenBouge = getObj('graphic', a.get('current'));
if (tokenBouge) {
tokenBouge.remove();
} else {
tokenBouge = findObjs({
_type: 'graphic',
_pageid: pageId,
represents: charId,
name: 'decoince ' + token.get('name')
});
if (tokenBouge.length > 0) {
tokenBouge = tokenBouge[0];
tokenBouge.remove();
}
}
a.remove();
});
nePlusSuivre(perso, pageId);
let deplacementsSynchronises = tokenAttribute(perso, 'tokensSynchronises');
let keepToken;
deplacementsSynchronises.forEach(function(attr) {
let listTokens = attr.get('current').split(',');
listTokens = listTokens.filter(function(tid) {
return tid != token.id;
});
if (listTokens.length < 2) attr.remove();
else keepToken = true;
});
if (keepToken || token.get('bar1_link') !== '') return;
let endName = "_" + token.get('name');
let tokAttr = findObjs({
_type: 'attribute',
_characterid: charId
});
tokAttr = tokAttr.filter(function(obj) {
return obj.get('name').endsWith(endName);
});
if (tokAttr.length > 0) {
log("Removing token local attributes");
log(tokAttr);
tokAttr.forEach(function(attr) {
attr.remove();
});
}
}
function persoImmobilise(perso) {
return (
stateCOF.pause ||
!isActive(perso) ||
getState(perso, 'immobilise') ||
attributeAsBool(perso, 'bloqueManoeuvre') ||
attributeAsBool(perso, 'enveloppePar') ||
attributeAsBool(perso, 'prisonVegetale') ||
attributeAsBool(perso, 'toiles') ||
attributeAsBool(perso, 'estGobePar') ||
attributeAsBool(perso, 'agrippeParUnDemon') ||
attributeAsBool(perso, 'etreinteScorpionPar') ||
attributeAsBool(perso, 'estEcrasePar') ||
paralyseParRoublard(perso, false)
);
}
function unlockToken(perso, evt) {
if (!perso.token.get('lockMovement')) return;
if (persoImmobilise(perso)) return;
if (evt) affectToken(perso.token, 'lockMovement', true, evt);
perso.token.set('lockMovement', false);
enleveDecoince(perso, evt);
}
function getGMId() {
let gm = findObjs({
_type: 'player'
}).find(function(p) {
return playerIsGM(p.id);
});
if (!gm) {
error("Impossible de trouver un MJ");
return;
}
return gm.id;
}
function doAuraDrainDeForce(playerId, origine, cibles, mEffet, options) {
let per;
if (cibles.length < 2) per = cibles[0];
else per = origine;
let display = startFramedDisplay(playerId, "Aura de drain de force", per);
const evtEffet = {
type: 'auraDrainDeForce',
action: {
playerId,
origine,
cibles,
mEffet,
options,
}
};
addEvent(evtEffet);
activerEffetTemporaire(origine, cibles, 'drainDeForce', mEffet, 1, options, evtEffet, '', [], display);
}
function doAuraDrainDeForceSup(playerId, origine, cibles, mEffet, options) {
let per;
if (cibles.length < 2) per = cibles[0];
else per = origine;
let display = startFramedDisplay(playerId, "Aura de drain de force", per);
const evtEffet = {
type: 'auraDrainDeForceSup',
action: {
playerId,
origine,
cibles,
mEffet,
options,
}
};
addEvent(evtEffet);
activerEffetTemporaire(origine, cibles, 'drainDeForceSup', mEffet, 1, options, evtEffet, '', [], display);
}
//retourne le nombre de cibles affectées
function appliquerAura(origine, cibles, pageId, aura, evt, renew) {
let effet;
cibles = cibles.filter(function(perso) {
if (distanceCombat(origine.token, perso.token, pageId) > aura.portee)
return false;
if (aura.seulementVivant && estNonVivant(perso)) return false;
effet = effet || 'affecteParAura(' + aura.id + ')';
if (!renew && attributeAsBool(perso, effet)) return false;
return true;
});
if (cibles.length === 0) return 0;
cibles.forEach(function(perso) {
setAttrDuree(perso, effet, 1, evt);
});
switch (aura.effet) {
case 'drainDeForce':
{
let options = {
save: {
carac: 'CON',
seuil: 12,
msgPour: ' pour ne pas être affaibli' + eForFemale(cibles[0])
}
};
let mEffet = messageEffetTemp.drainDeForce;
let playerId;
if (cibles.length > 1) {
playerId = getGMId();
} else {
let pl = getPlayerIds(cibles[0]);
if (pl === undefined) return;
if (pl.length > 0) playerId = pl[0];
}
doAuraDrainDeForce(playerId, origine, cibles, mEffet, options);
return;
}
case 'drainDeForceSup':
{
let options = {
save: {
carac: 'FOR',
seuil: 15,
msgPour: ' pour ne pas être affaibli' + eForFemale(cibles[0])
}
};
let mEffet = messageEffetTemp.drainDeForce;
let playerId;
if (cibles.length > 1) {
playerId = getGMId();
} else {
let pl = getPlayerIds(cibles[0]);
if (pl === undefined) return;
if (pl.length > 0) playerId = pl[0];
}
doAuraDrainDeForceSup(playerId, origine, cibles, mEffet, options);
return;
}
case 'auraGenerique':
let cmd = aura.effetCmd;
cibles.forEach(function(perso) {
cmd += " --target " + perso.token.id;
});
let gmid = getGMId();
if (!gmid) {
error("Impossible de trouver un MJ");
return;
}
sendChat('player|' + gmid, cmd);
return;
default:
error("Aura inconnue", aura);
}
return cibles.length;
}
//Réagit au déplacement manuel d'un token.
// suivis est l'ensemble des tokens qui a déjà été bougé suite à ce déplacement
function moveToken(token, prev, synchronisation, suivis) {
let charId = token.get('represents');
if (charId === '') return;
let perso = {
token,
charId
};
let pageId = token.get('pageid');
let x = token.get('left');
let y = token.get('top');
let deplacement = prev && (prev.left != x || prev.top != y);
if (!deplacement) return;
//Effet des bombes à intrusion
if (stateCOF.tokensTemps) {
let collisions = [];
let pt_arrivee = {
x,
y
};
let pt_depart = {
x: prev.left,
y: prev.top
};
let rayon = tokenSizeAsCircle(token) / 2;
stateCOF.tokensTemps.forEach(function(tt) {
if (!tt.intrusion) return;
//tt.intrusion est exprimé en pixels
let bombe = getTokenTemp(tt);
if (!bombe) return;
let pb = pointOfToken(bombe);
let distance = distancePoints(pt_depart, pb);
if (distance < tt.intrusion) return; //On est parti de la zone de départ
let distToTrajectory =
distancePixTokenSegment(bombe, pt_depart, pt_arrivee);
if (distToTrajectory > tt.intrusion + rayon) return;
collisions.push({
bombe,
tt,
distance
});
});
if (collisions.length > 0) {
collisions.sort(function(b1, b2) {
let d1 = b1.distance;
let d2 = b2.distance;
if (d1 < d2) return -1;
if (d2 < d1) return 1;
return 0;
});
let bombe = collisions[0].bombe;
x = bombe.get('left');
y = bombe.get('top');
token.set('left', x);
token.set('top', y);
const evt = {
type: "Explosion de bombe"
};
deleteTokenTemp(collisions[0].tt, evt);
stateCOF.tokensTemps = stateCOF.tokensTemps.filter(function(tt) {
return tt.id == collisions[0].tt.id;
});
}
}
//Effets des auras, asynchrone
if (stateCOF.combat && stateCOF.combat.auras) {
const evt = {
type: "Appliquer auras",
};
addEvent(evt);
stateCOF.combat.auras.forEach(function(aura) {
let origine = persoOfId(aura.origineId, aura.origineName, pageId);
if (!origine) return;
let cibles = [perso];
if (origine.token.id == token.id) {
cibles = [];
let allToks = findObjs({
_type: 'graphic',
_pageid: pageId,
_subtype: 'token',
layer: 'objects'
});
allToks.forEach(function(tok) {
if (tok.id == token.id) return;
let ci = tok.get('represents');
if (ci === '') return;
let p = {
charId: ci,
token: tok,
};
cibles.push(p);
});
}
appliquerAura(origine, cibles, pageId, aura, evt);
});
}
if (!synchronisation) {
let deplacementsSynchronises = tokenAttribute(perso, 'tokensSynchronises');
deplacementsSynchronises.forEach(function(attr) {
let listTokens = attr.get('current');
listTokens.split(',').forEach(function(tid) {
if (tid == token.id) return;
let tok = getObj('graphic', tid);
if (tok === undefined) {
error("Impossible de trouver le token d'id " + tid + " synchronisé avec " + token.get('name'), attr);
return;
}
tok.set('left', x);
tok.set('top', y);
moveToken(tok, prev, true);
});
});
}
suivis = suivis || new Set();
let nomToken = token.get('name');
if (nomToken.startsWith('decoince ')) {
let originalToken = findObjs({
_type: 'graphic',
_pageid: pageId,
represents: charId,
name: nomToken.substring(9)
});
if (originalToken.length === 0) return;
originalToken = originalToken[0];
let sprev = {
left: originalToken.get('left'),
top: originalToken.get('top'),
rotation: originalToken.get('rotation'),
};
originalToken.set('left', x);
originalToken.set('top', y);
originalToken.set('rotation', token.get('rotation'));
moveToken(originalToken, sprev, synchronisation, suivis);
return;
}
//On regarde d'abord si perso est sur une monture
let attrMonteSur = tokenAttribute(perso, 'monteSur');
if (attrMonteSur.length > 0) {
let monture = persoOfIdName(attrMonteSur[0].get('current'), pageId, true);
if (monture === undefined) {
sendPerso(perso, "descend de sa monture");
attrMonteSur[0].remove();
} else {
if (monture.token.get('pageid') != pageId || monture.token.get('lockMovement')) {
sendPerso(perso, "descend de " + nomPerso(monture));
removeTokenAttr(monture, 'estMontePar');
removeTokenAttr(monture, 'positionSurMonture');
attrMonteSur[0].remove();
} else if (!suivis.has(monture.token.id)) {
let position = tokenAttribute(monture, 'positionSurMonture');
if (position.length > 0) {
let dx = parseInt(position[0].get('current'));
let dy = parseInt(position[0].get('max'));
if (!(isNaN(dx) || isNaN(dy))) {
let sprev = {
left: monture.token.get('left'),
top: monture.token.get('top'),
};
monture.token.set('left', x - dx);
monture.token.set('top', y - dy);
monture.token.set('rotation', token.get('rotation') - attributeAsInt(monture, 'directionSurMonture', 0));
suivis.add(token.id);
moveToken(monture.token, sprev, synchronisation, suivis);
}
}
}
if (stateCOF.combat) {
const evt = {
type: "initiative"
};
updateInit(monture.token, evt);
// Réadapter l'init_dynamique au token du perso
if (stateCOF.options.affichage.val.init_dynamique.val) {
setTokenInitAura(perso);
}
}
}
}
//Si il est invisible, on bouge aussi l'autre token
let attrInvisible = tokenAttribute(perso, 'tokenInvisible');
if (attrInvisible.length > 0) {
attrInvisible = attrInvisible[0];
let tidInv1 = attrInvisible.get('current'); //Originel, normalement sur le gmlayer
let tidInv2 = attrInvisible.get('max');
let autreInvisible;
if (token.id == tidInv1) {
autreInvisible = getObj('graphic', tidInv2);
if (!autreInvisible) {
autreInvisible =
findObjs({
_type: 'graphic',
_subtype: 'token',
_pageid: token.get('pageid'),
layer: 'objects',
represents: perso.charId,
name: token.get('name')
});
if (autreInvisible.length > 0) autreInvisible = autreInvisible[0];
else autreInvisible = undefined;
}
} else if (token.id == tidInv2) {
autreInvisible = getObj('graphic', tidInv1);
if (!autreInvisible) {
autreInvisible =
findObjs({
_type: 'graphic',
_subtype: 'token',
_pageid: token.get('pageid'),
layer: 'gmlayer',
represents: perso.charId,
name: token.get('name')
});
if (autreInvisible.length > 0) autreInvisible = autreInvisible[0];
else autreInvisible = undefined;
}
}
if (!autreInvisible) {
switch (token.get('layer')) {
case 'objects':
autreInvisible =
findObjs({
_type: 'graphic',
_subtype: 'token',
_pageid: token.get('pageid'),
layer: 'gmlayer',
represents: perso.charId,
name: token.get('name')
});
if (autreInvisible.length > 0) autreInvisible = autreInvisible[0];
else autreInvisible = undefined;
break;
case 'gmlayer':
autreInvisible =
findObjs({
_type: 'graphic',
_subtype: 'token',
_pageid: token.get('pageid'),
layer: 'objects',
represents: perso.charId,
name: token.get('name')
});
if (autreInvisible.length > 0) autreInvisible = autreInvisible[0];
else autreInvisible = undefined;
break;
default:
error("Impossible de trouver la couche du token " + token.get('name'), token);
}
}
if (autreInvisible) {
autreInvisible.set('left', x);
autreInvisible.set('top', y);
}
}
//si non, perso est peut-être une monture
let attrMontePar = tokenAttribute(perso, 'estMontePar');
attrMontePar.forEach(function(a) {
let cavalier = persoOfIdName(a.get('current'), pageId);
if (cavalier === undefined) {
a.remove();
return;
}
if (!suivis.has(cavalier.token.id)) {
let position = tokenAttribute(perso, 'positionSurMonture');
if (position.length > 0) {
let dx = parseInt(position[0].get('current'));
let dy = parseInt(position[0].get('max'));
if (!(isNaN(dx) || isNaN(dy))) {
x += dx;
y += dy;
}
}
cavalier.token.set('left', x);
cavalier.token.set('top', y);
cavalier.token.set('rotation', token.get('rotation') + attributeAsInt(perso, 'directionSurMonture', 0));
}
});
//Si le token suivait quelqu'un, ce n'est plus le cas
if (prev.suit === undefined) nePlusSuivre(perso, pageId);
//On bouge tous les tokens qui suivent le personnage
//sauf si on a déjà été bougé.
if (!suivis.has(token.id)) {
suivis.add(token.id);
let attrSuivi = tokenAttribute(perso, 'estSuiviPar');
let page = getObj('page', pageId);
if (page === undefined) {
error("Impossible de trouver la page du token", perso);
return;
}
if (attrSuivi.length > 0) {
let width = page.get('width') * PIX_PER_UNIT;
let height = page.get('height') * PIX_PER_UNIT;
let pt = {
x: x,
y: y
};
let murs = getWalls(page, pageId, prev.murs);
let distance =
Math.sqrt((x - prev.left) * (x - prev.left) + (y - prev.top) * (y - prev.top));
attrSuivi.forEach(function(as) {
let suivants = as.get('current').split(':::');
let removedSuivant;
suivants = suivants.filter(function(idn) {
let suivant = persoOfIdName(idn, pageId);
if (suivant === undefined) {
removedSuivant = true;
return false;
}
let sw = suivant.token.get('width');
let sh = suivant.token.get('height');
if (sw > width) return false;
if (sh > width) return false;
let sx = suivant.token.get('left');
let sy = suivant.token.get('top');
//On essaie de garder la même position par rapport au token, en supposant qu'on était derrière lui
let attrSuit = tokenAttribute(suivant, 'suit');
let dp;
if (attrSuit.length > 0) {
dp = parseInt(attrSuit[0].get('max'));
}
if (dp === undefined || isNaN(dp) || dp < 1) {
dp = Math.sqrt((prev.left - sx) * (prev.left - sx) + (prev.top - sy) * (prev.top - sy));
}
let nsx = x + (prev.left - x) * dp / distance;
let nsy = y + (prev.top - y) * dp / distance;
if (nsx < 0) nsx = 0;
if (nsy < 0) nsy = 0;
if (nsx + sw / 2 > width) nsx = Math.floor(width - sw / 2);
if (nsy + sh / 2 > height) nsy = Math.floor(height - sh / 2);
//vérifie si de la nouvelle position on peut voir le suivi
if (obstaclePresent(nsx, nsy, pt, murs)) {
//On essaie de suivre le chemin du token, à la place
//D'abord se déplacer vers l'ancienne position de perso, au maximum de distance pixels
let distLoc = distance;
if (distLoc - dp < 5) {
nsx = prev.left;
nsy = prev.top;
} else {
if (dp > distLoc) {
nsx = sx + (prev.left - sx) * distLoc / dp;
nsy = sy + (prev.top - sy) * distLoc / dp;
if (obstaclePresent(nsx, nsy, pt, murs)) {
sendPerso(suivant, "ne peut plus suivre " + nomPerso(perso) + " car " + onGenre(suivant, 'il', 'elle') + " ne " + onGenre(perso, 'le', 'la') + " voit plus");
removeTokenAttr(suivant, 'suit');
removedSuivant = true;
return false;
}
} else {
//On part de l'ancienne position, et on peut encore avancer
distLoc -= dp;
nsx = prev.left + (x - prev.left) * distLoc / distance;
nsy = prev.top + (y - prev.top) * distLoc / distance;
if (obstaclePresent(nsx, nsy, pt, murs)) {
nsx = prev.left;
nsy = prev.top;
}
}
}
}
suivant.token.set('left', nsx);
suivant.token.set('top', nsy);
let sprev = {
left: sx,
top: sy,
suit: true,
murs: murs
};
moveToken(suivant.token, sprev, synchronisation, suivis); //pour faire suivre ceux qui le suivent
return true;
});
if (removedSuivant) {
if (suivants.length === 0) {
as.remove();
} else {
as.set('current', suivants.join(':::'));
}
}
});
}
}
// Update position du token d'initiative dynamique
let combat = stateCOF.combat;
if (stateCOF.options.affichage.val.init_dynamique.val && roundMarker &&
combat) {
if ((!stateCOF.chargeFantastique && combat.activeTokenId == token.id) ||
(stateCOF.chargeFantastique && stateCOF.chargeFantastique.activeTokenId == token.id)) {
roundMarker.set('left', x);
roundMarker.set('top', y);
} else {
// Cas spéciaux du cavaliers : au tour du cavalier, l'init_dynamique suit la monture
let estMontePar = tokenAttribute(perso, 'estMontePar');
if (estMontePar.length > 0) {
let sp = splitIdName(estMontePar[0].get('current'));
if (sp && combat.activeTokenId == sp.id) {
let cavalier = persoOfId(sp.id);
roundMarker.set('left', cavalier.token.get('left'));
roundMarker.set('top', cavalier.token.get('top'));
}
}
}
}
//On déplace les tokens de lumière, si il y en a
let attrLumiere = tokenAttribute(perso, 'lumiere');
attrLumiere.forEach(function(al) {
let lumId = al.get('max');
if (lumId == 'surToken') return;
let lumiereExiste;
let lumiere = getObj('graphic', lumId);
if (lumiere && lumiere.get('pageid') != pageId) {
lumiere = undefined;
lumiereExiste = true;
}
if (lumiere === undefined) {
let tokensLumiere = findObjs({
_type: 'graphic',
_pageid: pageId,
layer: 'walls',
name: al.get('current')
});
if (tokensLumiere.length === 0) {
if (lumiereExiste) return;
log("Pas de token pour la lumière " + al.get('current'));
al.remove();
return;
}
lumiere = tokensLumiere.shift();
if (tokensLumiere.length > 0) {
//On cherche le token le plus proche de la position précédente
let d = distanceTokenPrev(lumiere, prev);
tokensLumiere.forEach(function(tl) {
let d2 = distanceTokenPrev(tl, prev);
if (d2 < d) {
d = d2;
lumiere = tl;
}
});
}
}
if (lumiere === undefined) {
if (lumiereExiste) return;
log("Pas de token pour la lumière " + al.get('current'));
al.remove();
return;
}
lumiere.set('left', x);
lumiere.set('top', y);
});
let attrEnveloppe = tokenAttribute(perso, 'enveloppe');
attrEnveloppe = attrEnveloppe.concat(tokenAttribute(perso, 'aGobe'));
attrEnveloppe = attrEnveloppe.concat(tokenAttribute(perso, 'ecrase'));
attrEnveloppe.forEach(function(a) {
let cible = persoOfIdName(a.get('current'), pageId);
if (cible === undefined) {
a.remove();
return;
}
cible.token.set('left', x);
cible.token.set('top', y);
});
}
function synchronisationDesEtats(perso) {
for (let etat in cof_states) {
// Récupère la valeur de l'état sur la fiche
let valEtat;
if (etat == 'affaibli') { // Cas particulier affaibli sur la fiche perso
valEtat = (ficheAttributeAsInt(perso, 'affaibli', 20) == 12);
} else { // Autre cas
valEtat = ficheAttributeAsBool(perso, etat, false);
}
let field = cof_states[etat];
if (perso.token.get(field) != valEtat) perso.token.set(field, valEtat);
}
}
//Opérations diverses au moment où on pose un token.
//Si le token représente un personnage et avec la barre de vie non liée,
// assure un nom unique en ajoutant un numéro
// On en profite aussi pour mettre certaines valeurs par défaut
// retourne un perso si c'est un token de personnage
//Si la barre de vie est liée, on met à jour les valeurs, ce n'est plus fait automatiquement oar Roll20
function renameToken(token, tokenName) {
let charId = token.get('represents');
if (charId === undefined || charId === '') return;
let perso = {
token: token,
charId: charId
};
const pageId = token.get('pageid');
//Vision
let udl;
let visionNoir = predicateAsInt(perso, 'visionDansLeNoir', 0);
if (visionNoir > 0) {
let vs = scaleDistance(perso, visionNoir);
let page = getObj('page', pageId);
udl = page && page.get('dynamic_lighting_enabled');
if (udl) {
token.set('has_night_vision', true);
//token.set('night_vision_tint', '#555555');
token.set('night_vision_distance', vs);
} else {
token.set('light_radius', vs);
token.set('light_dimradius', -1);
token.set('light_otherplayers', false);
token.set('light_hassight', true);
token.set('light_angle', 360);
}
}
if (visionNoir <= 30 && predicateAsBool(perso, 'batonDesRunesMortes') && attributeAsBool(perso, 'runeBryniza')) {
if (!udl) {
let page = getObj('page', pageId);
udl = page && page.get('dynamic_lighting_enabled');
}
if (udl) {
let vs = scaleDistance(perso, 50);
token.set('has_night_vision', true);
token.set('night_vision_effect', 'Dimming');
token.set('night_vision_distance', vs);
}
}
if (udl) forceLightingRefresh(pageId);
if (token.get('bar1_link') !== '') {
//Cas des tokens non mooks
let attrMonteSur = tokenAttribute(perso, 'monteSur');
if (attrMonteSur.length > 0) {
let monture = persoOfIdName(attrMonteSur[0].get('current'), pageId);
if (monture === undefined) {
sendPerso(perso, "descend de sa monture");
attrMonteSur[0].remove();
} else {
if (monture.token.get('pageid') != pageId) {
sendPerso(perso, "descend de " + nomPerso(monture));
removeTokenAttr(monture, 'estMontePar');
removeTokenAttr(monture, 'positionSurMonture');
attrMonteSur[0].remove();
}
}
}
synchronisationDesEtats(perso);
for (let barNumber = 1; barNumber <= 3; barNumber++) {
let attrId = token.get('bar' + barNumber + '_link');
if (attrId) {
let attr = getObj('attribute', attrId);
if (attr) {
let fieldv = 'bar' + barNumber + '_value';
token.set(fieldv, attr.get('current'));
let fieldm = 'bar' + barNumber + '_max';
token.set(fieldm, attr.get('max'));
}
}
}
//On synchronise les barres, au cas où la fiche n'a pas été ouverte
//déjà fait par l'appel de renameToken
//synchronisationDesLumieres(perso, pageId);
return perso;
}
//cas des mooks : numérotation
let copyOf = 0;
let tokenBaseName = tokenName;
if (tokenBaseName.includes('%%NUMBERED%%')) {
if (typeof TokenNameNumber !== 'undefined') return perso; //On laisse tokenNameNumber gérer ça
tokenBaseName = tokenBaseName.replace('%%NUMBERED%%', '');
} else {
// On regarde si le nom se termine par un entier
let lastSpace = tokenBaseName.lastIndexOf(' ');
if (lastSpace > 0) {
copyOf = +tokenBaseName.substring(lastSpace + 1);
if (isNaN(copyOf)) copyOf = 0;
else tokenBaseName = tokenBaseName.substring(0, lastSpace);
}
}
let otherTokens = findObjs({
_type: 'graphic',
//_pageid: token.get('pageid'),
represents: charId
});
otherTokens = otherTokens.filter(function(tok) {
let pid = tok.get('pageid');
const page = getObj('page', pid);
if (page) {
return !(page.get('archived'));
}
return false;
});
let numero = 1;
let nePasModifier = false;
if (typeof TokenNameNumber !== 'undefined' && tokenBaseName.length > 0) {
if (!isNaN(tokenBaseName[tokenBaseName.length - 1]))
nePasModifier = true;
}
otherTokens.forEach(function(ot) {
if (ot.id == token.id) return;
let name = ot.get('name');
if (nePasModifier && name == tokenBaseName) nePasModifier = false;
if (name.startsWith(tokenBaseName)) {
let suffixe = name.replace(tokenBaseName + ' ', '');
if (isNaN(suffixe)) return;
let n = parseInt(suffixe);
if (n == copyOf) {
if (ot.get('pageid') == pageId) copyOf = 0;
}
if (n >= numero) numero = n + 1;
}
});
if (nePasModifier || copyOf > 0) return perso;
token.set('name', tokenBaseName + ' ' + numero);
return perso;
}
function addToken(token, nb) {
let tokenName = token.get('name');
//La plupart du temps, il faut attendre un peu que le nom soit affecté
if (tokenName === '') {
nb = nb || 1;
if (nb > 10) {
//error("Token posé sans nom, ou alors gros lag chez Roll20", token);
} else {
_.delay(function() {
addToken(token, nb + 1);
}, 50);
return;
}
}
//Maintenant, le nom du token est affecté, ou bien nb > 10 et dans ce cas, peut-être que le nom est juste vide
let perso = renameToken(token, tokenName);
if (perso === undefined) return;
let arme = predicateAsBool(perso, 'armeParDefaut');
if (arme === undefined && persoEstPNJ(perso) && !armesEnMain(perso)) {
//Si le perso est PNJ avec une seule arme, on lui met en main
let {
armes
} = listeDesArmes(perso);
for (let l in armes) {
if (arme) arme = true;
else arme = l;
}
}
if (arme !== undefined && arme !== true) {
degainerArme(perso, arme, {}, {
secret: true
});
}
synchronisationDesLumieres(perso);
}
// Surveillance sur le changement d'état du token
function changeMarker(token, prev) {
const charId = token.get('represents');
if (charId === undefined || charId === '') return; // Uniquement si token lié à un perso
const perso = {
token,
charId
};
const evt = {
type: "set_state",
};
let aff = affectToken(token, 'statusmarkers', prev.statusmarkers, evt);
let currentMarkers = [];
let markers = token.get("statusmarkers");
if (markers !== '') {
currentMarkers = markers.split(',');
}
let previousMarkers = [];
if (prev.statusmarkers !== '') {
previousMarkers = prev.statusmarkers.split(',');
}
let options = {
affectToken: aff
};
// Pour tous les markers disparus
previousMarkers.forEach(function(marker) {
if (currentMarkers.includes(marker)) return;
let etat = etat_de_marker[marker];
if (etat) {
setState(perso, etat, false, evt, options);
} else {
let effet = effet_de_marker[marker];
if (effet) {
let attr = tokenAttribute(perso, effet);
if (attr.length === 0) return;
finDEffet(attr[0], effet, attr[0].get('name'), perso.charId, evt);
}
}
});
// Ensuite les markers apparus
currentMarkers.forEach(function(marker) {
if (previousMarkers.includes(marker)) return;
let etat = etat_de_marker[marker];
if (etat) {
let succes = setState(perso, etat, true, evt, options);
if (!succes) token.set('status_' + marker, false);
} else {
let effet = effet_de_marker[marker];
if (effet) { //si on a un effet de combat, on peut le lancer.
let mEffet = messageEffetCombat[effet];
if (mEffet) {
setTokenAttr(perso, effet, true, evt, {
msg: messageActivation(perso, mEffet)
});
}
}
}
});
addEvent(evt);
}
function synchronisationDesLumieres(perso, pageId) {
let attrLumiere = tokenAttribute(perso, 'lumiere');
if (attrLumiere) {
let token = perso.token;
attrLumiere.forEach(function(al) {
let lumId = al.get('max');
if (lumId == 'surToken') {
if (!token.get('emits_bright_light') && !token.get('emits_low_light')) {
//On cherche un token qui représente le même personnage et émet de la lumière
let allTokens = findObjs({
type: 'graphic',
represents: perso.charId
});
let tok = allTokens.find(function(t) {
return t.get('emits_bright_light') || t.get('emits_low_light');
});
if (!tok) {
al.remove();
return;
}
token.set('emits_bright_light', tok.get('emits_bright_light'));
token.set('bright_light_distance', tok.get('bright_light_distance'));
token.set('emits_low_light', tok.get('emits_low_light'));
token.set('low_light_distance', tok.get('low_light_distance'));
}
return;
}
//Lumière sur un token qui suit le perso.
let lumiere = getObj('graphic', lumId);
if (lumiere && lumiere.get('pageid') != pageId) {
let copyLum = createObj('graphic', {
_pageid: pageId,
imgsrc: lumiere.get('imgsrc'),
left: token.get('left'),
top: token.get('top'),
width: 70,
height: 70,
layer: 'walls',
name: lumiere.get('name'),
emits_low_light: lumiere.get('emits_low_light'),
low_light_distance: lumiere.get('low_light_distance'),
emits_bright_light: lumiere.get('emits_bright_light'),
bright_light_distance: lumiere.get('bright_light_distance'),
});
if (copyLum) {
al.set('max', copyLum.id);
lumiere.remove();
}
}
});
}
}
//Actions à faire pour maintenir la cohérence des tokens qui représentent le même personnage.
function changePlayerPage(campaign) {
let currentMap = getObj('page', campaign.get('playerpageid'));
let tokens = findObjs({
_pageid: currentMap.id,
_type: 'graphic',
_subtype: 'token'
});
tokens.forEach(function(token) {
let charId = token.get('represents');
if (charId === undefined || charId === '') return; // Si token lié à un perso
if (token.get('bar1_link') === '') return; // Si unique
let perso = {
token,
charId
};
synchronisationDesEtats(perso);
synchronisationDesLumieres(perso, currentMap.id);
});
}
function enleveDecoince(perso, evt) {
let tokenBougeAttr = tokenAttribute(perso, 'bougeGraceA');
if (tokenBougeAttr.length === 0) return;
if (evt) evt.deletedAttributes = evt.deletedAttributes || [];
tokenBougeAttr.forEach(function(a) {
let tokenBouge = getObj('graphic', a.get('current'));
if (tokenBouge) {
if (evt) deleteTokenWithUndo(tokenBouge, evt);
else tokenBouge.remove();
} else {
let pageId = perso.token.get('pageid');
tokenBouge = findObjs({
_type: 'graphic',
_pageid: pageId,
represents: perso.charId,
name: 'decoince ' + perso.token.get('name')
});
if (tokenBouge.length > 0) {
tokenBouge = tokenBouge[0];
if (evt) deleteTokenWithUndo(tokenBouge, evt);
else tokenBouge.remove();
}
}
if (evt) evt.deletedAttributes.push(a);
a.remove();
});
}
function changeTokenLock(token, prev) {
const charId = token.get('represents');
if (charId === undefined || charId === '') return; // Uniquement si token lié à un perso
if (token.get('lockMovement')) return; //Rien de spécial à faire
const perso = {
token,
charId
};
const evt = {
type: "unlock",
deletedAttributes: []
};
addEvent(evt);
affectToken(perso.token, 'lockMovement', prev.lockMovement, evt);
enleveDecoince(perso, evt);
}
function changeDoor(door, prev) {
if (!stateCOF.pause) return;
if (prev.isOpen) return;
if (door.get('isOpen')) {
door.set('isOpen', false);
let b = boutonSimple('!cof-open-door ' + door.id, "Ouvrir");
sendChat('COF', "/w GM " + b + " (jeu en pause)");
}
}
function updateVersion(version, characters) {
if (version === undefined) {
state.COFantasy.eventId = 0;
}
if (version < 1.0) {
log("Mise à jour des attributs et macros vers la version 1.0");
//Mise à jour des effets temporaires avec _
let strReg = "(rayon_affaiblissant|peau_d_ecorce|chant_des_heros|image_decalee|a_couvert|sous_tension|forgeron_|armeEnflammee)";
let regName = new RegExp("^" + strReg);
let regText = new RegExp(strReg);
let attrs = findObjs({
_type: 'attribute',
});
attrs.forEach(function(attr) {
let attrName = attr.get('name');
if (regName.test(attrName)) {
attrName = attrName.replace(/rayon_affaiblissant/, 'rayonAffaiblissant');
attrName = attrName.replace(/peau_d_ecorce/, 'peauDEcorce');
attrName = attrName.replace(/chant_des_heros/, 'chantDesHeros');
attrName = attrName.replace(/image_decalee/, 'imageDecalee');
attrName = attrName.replace(/a_couvert/, 'aCouvert');
attrName = attrName.replace(/sous_tension/, 'sousTension');
attrName = attrName.replace(/forgeron_([^_\s)]*)/, 'forgeron($1)');
attrName = attrName.replace(/armeEnflammee([^_\s)]*)/, 'armeEnflammee($1)');
attr.set('name', attrName);
}
//Pour les consommables, il faut aussi changer le champ max;
let attrMax = attr.get('max');
if (regText.test(attrMax)) {
attrMax = attrMax.replace(/rayon_affaiblissant/g, 'rayonAffaiblissant');
attrMax = attrMax.replace(/peau_d_ecorce/g, 'peauDEcorce');
attrMax = attrMax.replace(/chant_des_heros/g, 'chantDesHeros');
attrMax = attrMax.replace(/image_decalee/g, 'imageDecalee');
attrMax = attrMax.replace(/a_couvert/g, 'aCouvert');
attrMax = attrMax.replace(/sous_tension/g, 'sousTension');
attrMax = attrMax.replace(/forgeron_([^_\s)]*)/g, 'forgeron($1)');
attrMax = attrMax.replace(/armeEnflammee([^_\s)]*)/g, 'armeEnflammee($1)');
attr.set('max', attrMax);
}
});
let macros = findObjs({
_type: 'macro'
}).concat(findObjs({
_type: 'ability'
}));
macros.forEach(function(m) {
let action = m.get('action');
if (regText.test(action)) {
action = action.replace(/rayon_affaiblissant/g, 'rayonAffaiblissant');
action = action.replace(/peau_d_ecorce/g, 'peauDEcorce');
action = action.replace(/chant_des_heros/g, 'chantDesHeros');
action = action.replace(/image_decalee/g, 'imageDecalee');
action = action.replace(/a_couvert/g, 'aCouvert');
action = action.replace(/sous_tension/g, 'sousTension');
action = action.replace(/forgeron_([^_\s)]*)/g, 'forgeron($1)');
action = action.replace(/armeEnflammee([^_\s)]*)/g, 'armeEnflammee($1)');
m.set('action', action);
}
});
log("Mise à jour effectuée.");
}
if (version < 2.0) {
log("Mise à jour des attributs et macros vers la version 2.0");
let strReg = "(--argent)";
let regName = new RegExp("^" + strReg);
let regText = new RegExp(strReg);
let attrs = findObjs({
_type: 'attribute',
});
attrs.forEach(function(attr) {
let attrName = attr.get('name');
if (regName.test(attrName)) {
attrName = attrName.replace(/--argent/, '--armeDArgent');
attr.set('name', attrName);
}
//Pour les consommables, il faut aussi changer le champ max;
let attrMax = attr.get('max');
if (regText.test(attrMax)) {
attrMax = attrMax.replace(/--argent/g, '--armeDArgent');
attr.set('max', attrMax);
}
});
let macros = findObjs({
_type: 'macro'
}).concat(findObjs({
_type: 'ability'
}));
macros.forEach(function(m) {
let action = m.get('action');
if (regText.test(action)) {
action = action.replace(/--argent/g, '--armeDArgent');
m.set('action', action);
}
});
log("Mise à jour effectuée.");
}
if (version < 2.02) {
let attrs = findObjs({
_type: 'attribute',
});
attrs.forEach(function(attr) {
let attrName = attr.get('name');
if (attrName == 'mort-vivant') attr.set('name', 'mortVivant');
});
log("Mise à jour effectuée.");
}
if (version < 2.03) {
let attrs = findObjs({
_type: 'attribute',
});
attrs.forEach(function(attr) {
let attrName = attr.get('name');
if (attrName == 'runeDEnergie') attr.set('name', 'runeForgesort_énergie');
if (attrName == 'runeDeProtection') attr.set('name', 'runeForgesort_protection');
if (attrName.includes('runeDePuissance')) {
attr.set('name', 'runeForgesort_puissance(' + attrName.substring(attrName.indexOf("(") + 1, attrName.indexOf(")")) + ')');
}
});
log("Mise à jour des runes effectuée.");
}
if (version < 2.04) {
let attrs = findObjs({
_type: 'attribute',
});
attrs.forEach(function(a) {
let attrName = a.get('name');
if (!attrName.startsWith('RD_')) return;
if (attrName == 'RD_critique') return;
let rds = parseInt(a.get('current'));
if (isNaN(rds) || rds < 1) {
a.remove();
return;
}
let cid = a.get('characterid');
let attrRD = 'RDS';
let attrRDS = findObjs({
_type: 'attribute',
_characterid: cid,
name: attrRD
}, {
caseInsensitive: true
});
if (attrRDS.length === 0) {
attrRDS = createObj('attribute', {
characterid: cid,
name: attrRD,
current: '',
max: ''
});
} else attrRDS = attrRDS[0];
let rdPerso = attrRDS.get('current');
attrName = attrName.substring(3);
if (attrName.startsWith('sauf_')) {
attrName = attrName.substr(5);
if (rdPerso.trim() === '') rdPerso = rds + '/' + attrName;
else rdPerso += ', ' + rds + '/' + attrName;
attrRDS.set('current', rdPerso);
a.remove();
return;
}
if (attrName == 'rdt' || attrName == 'sauf') return;
if (rdPerso.trim() === '') rdPerso = attrName + ':' + rds;
else rdPerso += ', ' + attrName + ':' + rds;
attrRDS.set('current', rdPerso);
a.remove();
});
log("Mise à jour de la RD effectuée");
}
if (version < 2.05) {
let attrs = findObjs({
_type: 'attribute',
});
attrs.forEach(function(a) {
let attrName = a.get('name');
if (!attrName.startsWith('dose_') && !attrName.startsWith('consommable_')) return;
let action = a.get('max');
if (!action.startsWith('!cof-lancer-sort')) return;
let mana = action.charAt(17);
let message = action.substring(19);
a.set("max", "!cof-lancer-sort --message " + message + " --mana " + mana);
});
log("Mise à jour des consommables !cof-lancer-sort effectuée");
let macros = findObjs({
_type: 'macro'
}).concat(findObjs({
_type: 'ability'
}));
macros.forEach(function(m) {
let macro = m.get("action");
if (!macro.startsWith('!cof-lancer-sort')) return;
let mana = macro.charAt(17);
let message = macro.substring(19);
m.set("action", "!cof-lancer-sort --message " + message + " --mana " + mana);
});
log("Mise à jour des ability & macros !cof-lancer-sort effectuée");
}
if (version < 2.10) {
let tokens = findObjs({
_type: 'graphic',
_subtype: 'token'
});
tokens.forEach(function(token) {
let charId = token.get('represents');
if (charId === '') return;
let bar1_link = token.get('bar1_link');
if (bar1_link === '') return;
let attrLie = getObj('attribute', bar1_link);
if (attrLie === undefined) return;
if (attrLie.get('name') != 'pnj_pv') return;
let attrPV = findObjs({
_type: 'attribute',
_characterid: charId,
name: 'PV'
}, {
caseInsensitive: true
});
if (attrPV.length === 0) return;
token.set('bar1_link', attrPV[0].id);
});
let charsToTreat = characters.length;
let removeAttrs = function() {
charsToTreat--;
if (charsToTreat > 0) return;
log("Supression des attributs obsolètes");
let attrs = findObjs({
_type: 'attribute',
});
attrs.forEach(function(a) {
switch (a.get('name')) {
case 'PR1':
case 'PR2':
case 'PR3':
case 'PR4':
case 'PR5':
case 'pnj_pv':
case 'pnj_dmtemp':
case 'pnj_rd':
a.remove();
}
});
};
if (charsToTreat === 0) removeAttrs();
const pageId = Campaign().get('playerpageid');
characters.forEach(function(character) {
character.get('_defaulttoken', function(token) {
if (token === '') {
removeAttrs();
return;
}
token = JSON.parse(token);
if (!token) {
removeAttrs();
return;
}
let bar1_link = token.bar1_link;
if (bar1_link === '') {
removeAttrs();
return;
}
let attrLie = getObj('attribute', bar1_link);
if (attrLie === undefined) {
removeAttrs();
return;
}
if (attrLie.get('name') != 'pnj_pv') {
removeAttrs();
return;
}
let attrPV = findObjs({
_type: 'attribute',
_characterid: character.id,
name: 'PV'
}, {
caseInsensitive: true
});
if (attrPV.length === 0) {
removeAttrs();
return;
}
token.bar1_link = attrPV[0].id;
token.pageid = pageId;
token.imgsrc = token.imgsrc.replace('/med.png', '/thumb.png');
token.imgsrc = token.imgsrc.replace('/max.png', '/thumb.png');
let newToken = createObj('graphic', token);
if (newToken) {
setDefaultTokenForCharacter(character, newToken);
newToken.remove();
} else {
log('Impossible de créer un token pour ' + token.name);
log(token);
}
removeAttrs();
});
});
log("Mise à jour des attributs et tokens effectuée");
}
if (version < 2.11) {
let attrs = findObjs({
_type: 'attribute',
});
attrs.forEach(function(a) {
if (a.get('name') != 'capitaine') return;
let nomCapitaine = a.get('current');
let idCapitaine = a.get('max');
let na = {
current: idCapitaine + ' ' + nomCapitaine,
max: 2
};
a.set(na);
});
log("Mise à jour des attributs de capitaine effectuée");
}
if (version < 2.12) {
let attrs = findObjs({
_type: 'attribute',
});
attrs.forEach(function(a) {
let attrName = a.get('name');
if (!attrName.startsWith('dose_') && !attrName.startsWith('consommable_')) return;
let charId = a.get('characterid');
//On ne passe dans la liste que pour les persos de type PJ
let typePerso = findObjs({
_type: 'attribute',
_characterid: charId,
name: 'type_personnage',
}, {
caseInsensitive: true
});
if (typePerso.length > 0 && typePerso[0].get('current') != 'PJ') return;
let consName = attrName.substring(attrName.indexOf('_') + 1);
consName = consName.replace(/_/g, ' ');
if (consName === '') return;
let quantite = parseInt(a.get('current'));
if (isNaN(quantite) || quantite < 0) return;
let pref = 'repeating_equipement_' + generateRowID() + '_';
let versionFiche = findObjs({
_type: 'attribute',
_characterid: charId,
name: 'version',
}, {
caseInsensitive: true
});
if (versionFiche.length === 0) versionFiche = 0;
else versionFiche = parseFloat(versionFiche[0].get('current'));
if (versionFiche < 4.01) {
createObj('attribute', {
_characterid: charId,
name: pref + 'equip-nom',
current: consName
});
createObj('attribute', {
_characterid: charId,
name: pref + 'equip-qte',
current: quantite
});
createObj('attribute', {
_characterid: charId,
name: pref + 'equip-effet',
current: a.get('max').trim(),
});
} else {
createObj('attribute', {
_characterid: charId,
name: pref + 'equip_nom',
current: consName
});
createObj('attribute', {
_characterid: charId,
name: pref + 'equip_qte',
current: quantite
});
createObj('attribute', {
_characterid: charId,
name: pref + 'equip_effet',
current: a.get('max').trim(),
});
}
a.remove();
});
log("Déplacement des attributs de consommables vers la fiche");
}
if (version < 2.13) {
//On enlève les attributs obsolètes de la verison 4.00 de la fiche
let attrs = findObjs({
_type: 'attribute',
});
attrs.forEach(function(a) {
if (a.get('name').toLowerCase() == 'equip-div') {
if (a.get('current') === '') {
a.remove();
return;
}
let charId = a.get('characterid');
let newAttr = findObjs({
_type: 'attribute',
_characterid: charId,
name: 'equip_div',
}, {
caseInsensitive: true
});
newAttr = newAttr.filter(function(na) {
if (na.get('current') === '') {
na.remove();
return false;
}
return true;
});
if (newAttr.length === 0) {
a.set('name', 'equip_div');
} else {
let character = getObj('character', charId);
if (character === undefined) return;
sendChat("COFantasy", "Supprimer l'attribut 'equip-div' du personnage" + character.get('name'));
log("Supprimer l'attribut 'equip-div' du personnage" + character.get('name'));
log("valeur courante " + newAttr[0].get('current'));
log("Ancienne valeur " + a.get('current'));
return;
}
}
});
}
if (version < 2.14) {
//Migration des éléments de règles optionnels
//Divers
if (state.COFantasy.options.regles.val.poudre_explosif) {
state.COFantasy.options.regles.val.divers.val.poudre_explosif.val = state.COFantasy.options.regles.val.poudre_explosif.val;
delete state.COFantasy.options.regles.val.poudre_explosif;
}
if (state.COFantasy.options.regles.val.forme_d_arbre_amelioree) {
state.COFantasy.options.regles.val.divers.val.forme_d_arbre_amelioree.val = state.COFantasy.options.regles.val.forme_d_arbre_amelioree.val;
delete state.COFantasy.options.regles.val.forme_d_arbre_amelioree;
}
if (state.COFantasy.options.regles.val.interchangeable_attaque) {
state.COFantasy.options.regles.val.divers.val.interchangeable_attaque.val = state.COFantasy.options.regles.val.interchangeable_attaque.val;
delete state.COFantasy.options.regles.val.interchangeable_attaque;
}
//Dommages
if (state.COFantasy.options.regles.val.dm_minimum) {
state.COFantasy.options.regles.val.dommages.val.dm_minimum.val = state.COFantasy.options.regles.val.dm_minimum.val;
delete state.COFantasy.options.regles.val.dm_minimum;
}
if (state.COFantasy.options.regles.val.crit_elementaire) {
state.COFantasy.options.regles.val.dommages.val.crit_elementaire.val = state.COFantasy.options.regles.val.crit_elementaire.val;
delete state.COFantasy.options.regles.val.crit_elementaire;
}
if (state.COFantasy.options.regles.val.blessures_graves) {
state.COFantasy.options.regles.val.dommages.val.blessures_graves.val = state.COFantasy.options.regles.val.blessures_graves.val;
delete state.COFantasy.options.regles.val.blessures_graves;
}
//Haute DEF
if (state.COFantasy.options.regles.val.usure_DEF) {
state.COFantasy.options.regles.val.haute_DEF.val.usure_DEF.val = state.COFantasy.options.regles.val.usure_DEF.val;
delete state.COFantasy.options.regles.val.usure_DEF;
}
if (state.COFantasy.options.regles.val.generer_options_attaques) {
log("Options d'attaques supprimées ; veuiller utiliser le bouton 'Options' de la liste d'Actions à la place");
sendChat('COFantasy', "Options d'attaques supprimées ; veuiller utiliser le bouton 'Options' de la liste d'Actions à la place");
delete state.COFantasy.options.regles.val.generer_options_attaques;
}
if (state.COFantasy.options.regles.val.generer_attaque_groupe) {
log("Attaques de groupe supprimées ; veuiller utiliser le bouton 'Options' de la liste d'Actions à la place");
sendChat('COFantasy', "Options d'attaques supprimées ; veuiller utiliser le bouton 'Options' de la liste d'Actions à la place");
delete state.COFantasy.options.regles.val.generer_attaque_groupe;
}
if (state.COFantasy.options.regles.val.bonus_attaque_groupe) {
state.COFantasy.options.regles.val.haute_DEF.val.bonus_attaque_groupe.val = state.COFantasy.options.regles.val.bonus_attaque_groupe.val;
delete state.COFantasy.options.regles.val.bonus_attaque_groupe;
}
if (state.COFantasy.options.regles.val.crit_attaque_groupe) {
state.COFantasy.options.regles.val.haute_DEF.val.crit_attaque_groupe.val = state.COFantasy.options.regles.val.crit_attaque_groupe.val;
delete state.COFantasy.options.regles.val.crit_attaque_groupe;
}
//Initiative
if (state.COFantasy.options.regles.val.initiative_variable) {
state.COFantasy.options.regles.val.initiative.val.initiative_variable.val = state.COFantasy.options.regles.val.initiative_variable.val;
delete state.COFantasy.options.regles.val.initiative_variable;
}
if (state.COFantasy.options.regles.val.initiative_variable_individuelle) {
state.COFantasy.options.regles.val.initiative.val.initiative_variable_individuelle.val = state.COFantasy.options.regles.val.initiative_variable_individuelle.val;
delete state.COFantasy.options.regles.val.initiative_variable_individuelle;
}
//Mana
if (state.COFantasy.options.regles.val.mana_totale) {
state.COFantasy.options.regles.val.mana.val.mana_totale.val = state.COFantasy.options.regles.val.mana_totale.val;
delete state.COFantasy.options.regles.val.mana_totale;
}
if (state.COFantasy.options.regles.val.elixirs_sorts) {
state.COFantasy.options.regles.val.mana.val.elixirs_sorts.val = state.COFantasy.options.regles.val.elixirs_sorts.val;
delete state.COFantasy.options.regles.val.elixirs_sorts;
}
log("Règles optionelles mises à jour");
}
if (version < 2.15) {
let attrs = findObjs({
_type: 'attribute',
});
attrs.forEach(function(a) {
let attrName = a.get('name');
if (!attrName.startsWith('dose_') && !attrName.startsWith('consommable_')) return;
let charId = a.get('characterid');
//On ne passe dans la liste que pour les persos de type PNJ
let typePerso = findObjs({
_type: 'attribute',
_characterid: charId,
name: 'type_personnage',
}, {
caseInsensitive: true
});
if (typePerso.length > 0 && typePerso[0].get('current') == 'PJ') return;
let consName = attrName.substring(attrName.indexOf('_') + 1);
consName = consName.replace(/_/g, ' ');
if (consName === '') return;
let quantite = parseInt(a.get('current'));
if (isNaN(quantite) || quantite < 0) return;
let pref = 'repeating_equipement_' + generateRowID() + '_';
createObj('attribute', {
_characterid: charId,
name: pref + 'equip_nom',
current: consName
});
createObj('attribute', {
_characterid: charId,
name: pref + 'equip_qte',
current: quantite
});
createObj('attribute', {
_characterid: charId,
name: pref + 'equip_effet',
current: a.get('max').trim(),
});
a.remove();
});
log("Déplacement des attributs de consommables de PNJs vers la fiche");
}
if (version < 2.16) {
let attrs = findObjs({
_type: 'attribute',
});
let handouts = findObjs({
_type: 'handout'
});
let handhoutComp = handouts.find(function(h) {
let handName = h.get('name');
return (handName == 'Compétences' || handName == 'Competences');
});
if (handhoutComp) {
let listeCompetences = {
FOR: [],
DEX: [],
CON: [],
SAG: [],
INT: [],
CHA: [],
nombre: 0
};
handhoutComp.get('notes', function(note) { // asynchronous
let carac; //La carac dont on spécifie les compétences actuellement
note = note.trim();
if (note.startsWith('
';
});
for (let label in attaquesNonTriees) {
ligne += attaquesNonTriees[label] + '
';
}
//On ajoute aussi les lancers de feu grégeois, si il y en a
let attrFeuxGregeois = tokenAttribute(perso, 'elixir_feu_grégeois');
if (attrFeuxGregeois.length > 0) {
attrFeuxGregeois = attrFeuxGregeois[0];
let feuxGregeois = parseInt(attrFeuxGregeois.get('current'));
if (feuxGregeois > 0) {
let command = attrFeuxGregeois.get('max').trim();
ligne += bouton(command, 'Feu grégeois', perso, {
ressource: attrFeuxGregeois
});
ligne += " (reste " + feuxGregeois + ")
";
}
}
return ligne;
}
function argsAttaqueAMainsNues(perso) {
let bonusAtk = computeArmeAtk(perso, '@{ATKCAC}');
return "Mains nues --toucher " + bonusAtk + " --dm 1d4 + [[@{selected|FOR}]] --tempDmg";
}
function listeDesArmes(perso) {
const listeAttaques = listAllAttacks(perso);
let attaqueNaturelleNonVisible;
let armes = {};
let armeVisible = 0;
let possedeAttaqueNaturelle;
for (let label in listeAttaques) {
const arme = listeAttaques[label];
const t = fieldAsString(arme, 'armetypeattaque', 'Naturel');
if (fieldAsInt(arme, 'armeactionvisible', 1) === 1) {
let options = ' ' + fieldAsString(arme, 'armeoptions', '');
if (actionImpossible(perso, options.split(' --'), label)) continue;
switch (t) {
case 'Naturel':
possedeAttaqueNaturelle = true;
break;
case 'Arme 1 main':
case 'Arme 2 mains':
case 'Arme gauche':
armes[label] = arme;
armeVisible = true;
break;
}
} else if (!attaqueNaturelleNonVisible && t == 'Naturel') {
attaqueNaturelleNonVisible = arme;
}
}
return {
listeAttaques,
armes,
armeVisible,
possedeAttaqueNaturelle,
attaqueNaturelleNonVisible
};
}
//retourne soit une ability, soit un nombre entre 1 et 4, soit undefined si
// la liste n'existe pas
function findListeActions(perso, listActions, abilities) {
if (listActions == ficheAttribute(perso, 'nomlisteaction1', 'Liste 1')) return 1;
if (listActions == ficheAttribute(perso, 'nomlisteaction2', 'Liste 2')) return 2;
if (listActions == ficheAttribute(perso, 'nomlisteaction3', 'Liste 3')) return 3;
if (listActions == ficheAttribute(perso, 'nomlisteaction4', 'Liste 4')) return 4;
let fullListActions = '#' + listActions + '#';
let res = abilities.find(function(a) {
return a.get('name') == fullListActions;
});
return res;
}
function proposerDeDegainer(perso, armes, labelArmePrincipale, armePrincipale, labelArmeGauche, ligneArme, cote) {
let degainer = "!cof-degainer ?{Arme?|";
let armeADegainer;
//Prise en compte des prédicats pour ce qu'on veut voir en premier
let i = 1;
let labelsVus = new Set();
let principale = cote != ' gauche';
if (principale) {
while (true) {
let labels = predicateAsBool(perso, 'actionDegainer' + i);
i++;
if (!labels) break;
let mid = labels.indexOf('-');
if (mid > 0) {
let label = labels.substring(0, mid);
let labelGauche = labels.substring(mid + 1);
if (label == labelArmePrincipale || labelGauche == labelArmeGauche) continue;
let arme = armes[label];
if (arme === undefined) {
error("Impossible de trouver l'arme de label " + label + " mentionnée dans le prédicat actionDegainer" + (i - 1), armes);
continue;
}
let armeGauche = armes[labelGauche];
if (armeGauche === undefined) {
error("Impossible de trouver l'arme de label " + labelGauche + " mentionnée dans le prédicat actionDegainer" + (i - 1), armes);
continue;
}
degainer += arme.armenom + " et " + armeGauche.armenom + "," + label + ' ' + labelGauche + "|";
if (armeADegainer) armeADegainer.unique = undefined;
else armeADegainer = {
unique: true,
nom: arme.armenom + " et " + armeGauche.armenom,
label: label + ' ' + labelGauche
};
} else {
if (labels == labelArmePrincipale || labels == labelArmeGauche) continue;
let arme = armes[labels];
if (arme === undefined) {
error("Impossible de trouver l'arme de label " + labels + " mentionnée dans le prédicat actionDegainer" + (i - 1), armes);
continue;
}
labelsVus.add(labels);
degainer += arme.armenom + "," + labels + "|";
if (armeADegainer) armeADegainer.unique = undefined;
else armeADegainer = {
unique: true,
nom: arme.armenom,
label: labels
};
}
}
}
//Ajout des autres armes
for (let l in armes) {
let a = weaponStatsOfAttack(perso, l, armes[l]);
if (principale && l == labelArmePrincipale && armePrincipale.batarde) {
degainer += a.name + ' ';
if (armePrincipale.deuxMains) {
degainer += "(1M)," + l + cote + "|";
} else {
degainer += "(2M)," + l + " 2mains|";
}
if (armeADegainer) armeADegainer.unique = undefined;
else armeADegainer = {
unique: true,
nom: a.name,
label: l
};
} else if (l != labelArmePrincipale && l != labelArmeGauche && !labelsVus.has(l) &&
(principale || !a.deuxMains)) {
degainer += a.name;
if (principale && a.batarde) {
degainer += " (2M)," + l + " 2mains|" +
a.name + cote + " (1M)";
} else if (cote == ' gauche' && !a.armeLegere) {
continue;
}
if (a.charge && attributeAsInt(perso, 'charge_' + l, 1) === 0)
degainer += ' (vide)';
else if (a.poudre && attributeAsInt(perso, 'chargeGrenaille_' + l, 0) > 0)
degainer += ' (grenaille)';
degainer += "," + l + cote + "|";
if (armeADegainer) armeADegainer.unique = undefined;
else armeADegainer = {
unique: true,
nom: a.name,
label: l
};
}
}
// Pictos : https://wiki.roll20.net/CSS_Wizardry#Pictos
if (armeADegainer) {
if ((labelArmePrincipale && principale) ||
(labelArmeGauche && cote != ' droite')) {
degainer += "Rengainer ";
if (cote === '' && labelArmePrincipale && labelArmeGauche)
degainer += "ses armes";
else degainer += "son arme" + cote;
if (cote) degainer += ",-1 " + cote + "}";
else degainer += ", }";
} else if (armeADegainer.unique) {
//Dans ce cas, pas de choix, juste une arme à dégainer
degainer = '!cof-degainer ' + armeADegainer.label + cote;
} else {
degainer = degainer.substr(0, degainer.length - 1) + '}';
}
degainer += ' --montreActions';
if (ligneArme)
ligneArme += bouton(degainer, ';', perso);
else {
let b = 'Dégainer';
if (cote) b += ' à' + cote;
ligneArme = bouton(degainer, b, perso);
}
if (armeADegainer.unique && !labelArmePrincipale && !labelArmeGauche)
ligneArme += armeADegainer.nom;
} else {
if (ligneArme)
ligneArme += bouton('!cof-degainer --montreActions', '}', perso);
}
return ligneArme;
}
//Si listActions est fourni, ça peut faire référence à une ability
//dont le nom commence et termine par #, contenant une liste d'actions
//à afficher
//sinon, fait référence à une des listes d'action de la fiche
function turnAction(perso, playerId, listActions) {
const pageId = perso.token.get('pageid');
// Toutes les Abilities du personnage lié au Token
const abilities = findObjs({
_type: 'ability',
_characterid: perso.charId,
});
let title = 'Actions possibles :';
if (stateCOF.chargeFantastique) title = "Charge fantastique: action d'attaque";
let opt_display = {
chuchote: true
};
let actionsDuTour;
let actionsParDefaut;
if (listActions) {
title = listActions;
actionsDuTour = findListeActions(perso, listActions, abilities);
if (actionsDuTour === undefined) {
return;
}
} else {
afficherOptionsAttaque(perso, opt_display);
actionsDuTour = 0;
actionsParDefaut = true;
}
let formeDarbre = false;
if (actionsDuTour === 0) {
if (!isActive(perso)) {
if (!getState(perso, 'surpris') || !compagnonPresent(perso, 'surveillance')) {
sendPerso(perso, "ne peut pas agir à ce tour");
return true;
}
}
formeDarbre = attributeAsBool(perso, 'formeDArbre');
if (formeDarbre) {
actionsDuTour = findListeActions(perso, "Forme d'arbre", abilities);
if (actionsDuTour === undefined) actionsDuTour = findListeActions(perso, 'FormeArbre', abilities);
if (actionsDuTour) {
actionsParDefaut = true;
} else {
actionsDuTour = 0;
formeDarbre = false;
}
}
}
//actionDuTour peut être undefined, pour la liste par défaut
let actionsAAfficher;
let ligne = '';
let command = '';
if (actionsParDefaut && !stateCOF.chargeFantastique && attributeAsBool(perso, 'hate')) {
ligne += "Effet de hâte : une action d'attaque ou de mouvement en plus
";
}
if (actionsParDefaut && !stateCOF.chargeFantastique && attributeAsBool(perso, 'reactionViolente')) {
ligne += "Crise de folie : doit attaquer la personne qui l'a provoqué et ceux qui l'en empêchent.
";
ligne += boutonSimple('!cof-fin-reaction-violente ' + perso.token.id, "Prendre sur soi");
}
//Les dégâts aux personnages enveloppés par perso
let attrs_enveloppe = tokenAttribute(perso, 'enveloppe');
attrs_enveloppe.forEach(function(a) {
let cible = persoOfIdName(a.get('current'), pageId);
if (cible === undefined) {
error("Attribut d'enveloppe mal formé ou obsolète", a.get('current'));
a.remove();
return;
}
let enveloppeDM = a.get('max');
if (enveloppeDM.startsWith('ability ')) {
enveloppeDM = enveloppeDM.substring(8);
let abEnveloppe = abilities.find(function(abilitie) {
return (abilitie.get('name') === enveloppeDM);
});
if (abEnveloppe) {
command = abEnveloppe.get('action').trim();
command = replaceAction(command, perso);
command = command.replace(new RegExp(escapeRegExp('@{target|token_id}'), 'g'), cible.token.id);
ligne += bouton(command, "Infliger DMS à " + nomPerso(cible), perso) + '
';
}
} else if (enveloppeDM.startsWith('label ')) {
actionsAAfficher = true;
command = '!cof-attack ' + perso.token.id + ' ' + cible.token.id + ' ' + enveloppeDM.substring(6) + ' --auto --acide --effet paralyseTemp [[2d6]] --save CON 15';
ligne += bouton(command, "Infliger DMs à " + nomPerso(cible), perso) + '
';
} else if (enveloppeDM.startsWith('etreinte ')) {
actionsAAfficher = true;
enveloppeDM = enveloppeDM.substring(9);
command = '!cof-attack ' + perso.token.id + ' ' + cible.token.id + ' --dm ' + enveloppeDM + ' --auto --nom étreinte ';
ligne += bouton(command, "Infliger DMs à " + nomPerso(cible), perso) + '
';
} //else pas reconnu
//On ajoute aussi un bouton pour libérer
command = '!cof-echapper-enveloppement libere --target ' + cible.token.id;
ligne += boutonSimple(command, "Libérer " + nomPerso(cible)) + '
';
});
//Bouton pour libérer une personne qu'on agrippe
let attrs_agrippe = tokenAttribute(perso, 'agrippe');
attrs_agrippe =
attrs_agrippe.concat(
tokenAttribute(perso, 'etreinteImmole'),
tokenAttribute(perso, 'etreinteScorpionSur'),
tokenAttribute(perso, 'devore'));
attrs_agrippe.forEach(function(a) {
let cible = persoOfIdName(a.get('current'), pageId);
if (cible === undefined) {
a.remove();
return;
}
command = '!cof-liberer-agrippe ' + cible.token.id + ' libere';
ligne += boutonSimple(command, "Libérer " + nomPerso(cible)) + '
';
});
let gobePar;
let attrGobePar = tokenAttribute(perso, 'estGobePar');
if (attrGobePar.length > 0) {
gobePar = persoOfIdName(attrGobePar[0].get('current', pageId));
command = '!cof-jet FOR 15 --target ' + perso.token.id;
ligne += boutonSimple(command, "Jet de force") + "pour pouvoir attaquer avec une dague
";
toFront(gobePar.token);
}
if (!gobePar && attributeAsBool(perso, 'enveloppePar')) {
actionsAAfficher = true;
command = '!cof-echapper-enveloppement --target ' + perso.token.id;
ligne += boutonSimple(command, 'Se libérer') + '
';
} else if (getState(perso, 'enseveli')) {
actionsAAfficher = true;
ligne += boutonSaveState(perso, 'enseveli') + '
';
} else if (!gobePar && attributeAsBool(perso, 'etreinteScorpionPar')) {
actionsAAfficher = true;
command = '!cof-liberer-agrippe ' + perso.token.id;
ligne += boutonSimple(command, 'Se libérer') + "de l'étreinte du scorpion
";
} else if (!gobePar && attributeAsBool(perso, 'estEcrasePar')) {
actionsAAfficher = true;
command = '!cof-liberer-ecrase ' + perso.token.id;
ligne += boutonSimple(command, 'Se libérer') + "de l'étreinte
";
} else { //On affiche les actions normales
if (stateCOF.combat && stateCOF.combat.armeesDesMorts && !gobePar) {
let combattreArmee = false;
for (let aid in stateCOF.combat.armeesDesMorts) {
if (aid == perso.token.id) continue;
let persoArmee = stateCOF.combat.armeesDesMorts[aid];
if (!persoArmee || !persoArmee.token) {
error("Erreur dans l'armée des morts", stateCOF.combat.armeesDesMorts);
delete stateCOF.combat.armeesDesMorts[aid];
continue;
}
let boost = 0;
if (charAttributeAsBool(persoArmee, "armeeDesMortsPuissant")) boost = 1;
else boost = charAttributeAsInt(persoArmee, "armeeDesMortsTempeteDeManaIntense", 0);
let rayon = Math.floor(20 * Math.sqrt(1 + boost));
if (distanceCombat(perso.token, persoArmee.token, pageId) <= rayon &&
(!alliesParPerso[persoArmee.charId] || !alliesParPerso[persoArmee.charId].has(perso.charId))) {
actionsAAfficher = true;
combattreArmee = true;
}
}
if (combattreArmee) {
command = '!cof-defense-armee-des-morts ' + perso.token.id;
ligne += bouton(command, 'Combattre les Morts-Vivants', perso) + '
';
}
}
if (!gobePar && (attributeAsBool(perso, 'estAgrippePar') || attributeAsBool(perso, 'estDevorePar'))) {
actionsAAfficher = true;
command = '!cof-liberer-agrippe ' + perso.token.id;
ligne += bouton(command, 'Se libérer', perso) + '(action de mvt)
';
}
if (!gobePar && attributeAsBool(perso, 'etreinteImmolePar')) {
actionsAAfficher = true;
command = '!cof-liberer-agrippe ' + perso.token.id;
ligne += bouton(command, 'Se libérer', perso) + ' (action limitée)
';
}
//Actions pour les saves actifs
let attrs = findObjs({
_type: 'attribute',
_characterid: perso.charId,
});
const estMook = perso.token.get('bar1_link') === '';
let suffixe = '';
if (estMook) suffixe = '_' + perso.token.get('name');
attrs.forEach(function(attr) {
let attrName = attr.get('name');
if (estMook && !attrName.endsWith(suffixe)) return;
let indexSave = attrName.indexOf('SaveActifParTour');
if (indexSave < 0) return;
let effetC = attrName.substring(0, indexSave);
let effetTemp = estEffetTemp(effetC);
if (!effetTemp && !estEffetCombat(effetC)) return;
attrName = effetC + attrName.substr(indexSave + 11);
let met;
if (effetTemp) met = messageOfEffetTemp(effetC);
else met = messageEffetCombat[effetC];
let msgPour;
if (met.msgSave) msgPour = met.msgSave;
else {
msgPour += "save contre ";
if (effetC.startsWith('dotGen('))
msgPour += effetC.substring(7, effetC.indexOf(')'));
else msgPour += effetC;
}
ligne += boutonSimple("!cof-save-effet " + perso.token.id + " " + attr.id, msgPour) + '
';
});
let ecraser = predicateAsBool(perso, 'ecraser');
if (ecraser) {
let attrEcrase = tokenAttribute(perso, 'ecrase');
attrEcrase.forEach(function(a) {
let cible = persoOfIdName(a.get('current'));
if (cible) {
let commande = "!cof-attack " + perso.token.id + ' ' + cible.token.id + ' ' + ecraser;
ligne += boutonSimple(commande, 'Écraser') + ' ' + nomPerso(cible) + '
';
} else {
a.remove();
}
});
}
if (formeDarbre) {
actionsAAfficher = true;
command = '!cof-attack @{selected|token_id} ';
if (gobePar) command += gobePar.token.id;
else command += '@{target|token_id}';
let niveau = ficheAttributeAsInt(perso, 'niveau', 1);
command += " Branches --toucher " + niveau + " --dm 1d6+3";
ligne += bouton(command, 'Attaque', perso) + '
';
}
let armesAutorisees = true;
if (attributeAsBool(perso, 'lycanthropie')) {
armesAutorisees = false;
actionsAAfficher = true;
let atk = computeArmeAtk(perso, '@{ATKCAC}');
let force = modCarac(perso, 'force');
command = '!cof-attack @{selected|token_id} ';
if (gobePar) command += gobePar.token.id;
else command += '@{target|token_id}';
let c = command + " Griffes --toucher " + atk + " --dm 2d6+" + force;
ligne += bouton(c, 'Griffes', perso) + '
';
c = command + " Morsure --toucher " + atk + " --dm 1d6+" + force;
ligne += bouton(c, 'Morsure', perso) + '
';
}
//On cherche si il y a une armée conjurée à attaquer
let attrs_armee =
findObjs({
_type: 'attribute',
name: 'armeeConjuree',
});
if (attrs_armee.length > 0) {
let allTokens =
findObjs({
_type: "graphic",
_pageid: pageId,
_subtype: "token",
layer: "objects"
});
let scale = computeScale(pageId);
let px = perso.token.get('left');
let py = perso.token.get('top');
let pxp = px + 10 * PIX_PER_UNIT / scale;
let pxm = px - 10 * PIX_PER_UNIT / scale;
let pyp = py + 10 * PIX_PER_UNIT / scale;
let pym = py - 10 * PIX_PER_UNIT / scale;
let ps = tokenSize(perso.token, 0);
pxp += ps;
pxm -= ps;
pyp += ps;
pym -= ps;
attrs_armee.forEach(function(aa) {
let aacid = aa.get('characterid');
if (aacid == perso.charId) return;
let invocId = aa.get('current');
if (invocId == perso.charId) return;
let allies = alliesParPerso[invocId] || new Set();
if (allies.has(perso.charId)) return;
allTokens.forEach(function(t) {
if (t.get('represents') == aacid) {
//teste si dans un carré de 20 m de coté autour de l'armée.
let tx = t.get('left');
let ty = t.get('top');
if (tx < pxp && tx > pxm && ty < pyp && ty > pym) {
command = '!cof-attack ' + perso.token.id + ' ' + t.id +
" Attaque de l'armée --dm " +
(ficheAttributeAsInt(perso, 'niveau', 1) + 1) +
" --auto --attaqueArmeeConjuree --allonge 20";
ligne += bouton(command, "Attaque de l'armée", perso) + '
';
}
}
});
});
}
//Les soins pour les élémentaires
if (predicateAsBool(perso, 'corpsElementaire')) {
command = '!cof-soin 5';
ligne += bouton(command, "Régénération", perso) + " si source élémentaire proche
";
}
//Regard pétrifiant
let regardPetrifiant = predicateAsInt(perso, 'regardPetrifiant', 0, 16);
if (regardPetrifiant) {
let c = '!cof-effet petrifie --lanceur ' + perso.token.id + ' --target @{target|token_id} --regard --save CON ';
ligne += boutonSimple(c + (regardPetrifiant - 4), "Regard pétrifiant") +
boutonSimple(c + regardPetrifiant, "(inconscient)") + '
';
}
//Violence ciblée
if (predicateAsBool(perso, 'violenceCiblee') && !attributeAsBool(perso, 'reactionViolente')) {
let pointsDeViolence = attributeAsInt(perso, 'pointsDeViolence', 0);
if (pointsDeViolence > 0) {
let attr = tokenAttribute(perso, 'pointsDeViolence')[0];
command = "!cof-effet-temp reactionViolente [[1d4]] --decrAttribute " + attr.id + " --target " + perso.token.id;
ligne += boutonSimple(command, 'Violence ciblée') + '
';
}
}
//Les attaques de la fiche à afficher dans la liste d'actions
const montrerAttaques = ficheAttributeAsInt(perso, 'montrerattaques', 1);
const afficherAttaquesFiche =
actionsParDefaut ||
(actionsDuTour === 0 && montrerAttaques);
const montrerArmeEnMain = (actionsDuTour === 0 && ficheAttributeAsInt(perso, 'montrerarmeenmain', 1));
if (afficherAttaquesFiche) {
let attackOptions = {};
if (gobePar) attackOptions.target = gobePar.token.id;
if (montrerArmeEnMain || !armesAutorisees)
attackOptions.nePasAfficherArmes = true;
ligne += listeAttaquesVisibles(perso, attackOptions);
}
//L'arme en main et dégainer, si besoin
if (montrerArmeEnMain && armesAutorisees) {
let {
listeAttaques,
armes,
armeVisible,
possedeAttaqueNaturelle,
attaqueNaturelleNonVisible
} = listeDesArmes(perso);
let labelArmePrincipale;
let labelArmeGauche;
let ligneArmePrincipale;
let ligneArmeGauche;
let armePrincipale = armesEnMain(perso);
if (armePrincipale) {
labelArmePrincipale = armePrincipale.label;
}
if (perso.armeGauche) labelArmeGauche = perso.armeGauche.label;
if ((labelArmeGauche === undefined || labelArmeGauche === '') &&
predicateAsBool(perso, 'attaqueAuBouclier') && ficheAttributeAsInt(perso, 'defbouclieron', 0)) {
labelArmeGauche = predicateAsBool(perso, 'attaqueAuBouclier');
}
let command = '!cof-attack ' + perso.token.id + ' @{target|token_id} ';
if (armePrincipale) {
let nomCommande = armePrincipale.name;
if (armePrincipale.batarde) {
if (armePrincipale.deuxMains) nomCommande += ' (2M)';
else nomCommande += ' (1M)';
}
if (attributeAsBool(perso, 'paradeCroisee')) {
//On connaîtra vraiment l'arme au moment de faire l'attaque
command += '-1';
if (labelArmeGauche &&
listeAttaques[labelArmeGauche]) {
nomCommande += ' ou ' + listeAttaques[labelArmeGauche].armenom;
labelArmeGauche = undefined; //Pour ne pas l'afficher
}
} else {
command += labelArmePrincipale;
if (armeDechargee(perso, armePrincipale)) nomCommande += ' (vide)';
else if (armeChargeeDeGrenaille(perso, armePrincipale)) nomCommande += ' (grenaille)';
}
ligneArmePrincipale = bouton(command, nomCommande, perso);
} else if (!possedeAttaqueNaturelle) {
if (attaqueNaturelleNonVisible) {
ligneArmePrincipale =
bouton(command + attaqueNaturelleNonVisible.armelabel, attaqueNaturelleNonVisible.armenom, perso);
} else {
ligneArmePrincipale = bouton(command + argsAttaqueAMainsNues(perso), 'Mains nues', perso);
}
}
if (perso.armeGauche) {
let nomCommande = perso.armeGauche.name;
if (armeDechargee(perso, perso.armeGauche)) nomCommande += ' (vide)';
else if (armeChargeeDeGrenaille(perso, perso.armeGauche)) nomCommande += ' (grenaille)';
ligneArmeGauche = bouton("!cof-attack @{selected|token_id} @{target|token_id} " + labelArmeGauche, nomCommande, perso);
}
//Maintenant on propose de dégainer
if (armeVisible) {
if (predicateAsBool(perso, 'combatADeuxArmes')) {
ligneArmePrincipale = proposerDeDegainer(perso, armes, labelArmePrincipale, armePrincipale, labelArmeGauche, ligneArmePrincipale, ' droite');
if (!armePrincipale || !armePrincipale.deuxMains) {
ligneArmeGauche = proposerDeDegainer(perso, armes, labelArmePrincipale, armePrincipale, labelArmeGauche, ligneArmeGauche, ' gauche');
}
} else {
ligneArmePrincipale = proposerDeDegainer(perso, armes, labelArmePrincipale, armePrincipale, labelArmeGauche, ligneArmePrincipale, '');
}
}
if (ligneArmePrincipale) ligne += ligneArmePrincipale + '
';
if (ligneArmeGauche) ligne += ligneArmeGauche + '
';
// Le tir de semonce, si disponible et qu'on tient une arme à distance
if (predicateAsBool(perso, 'tirDeSemonce') && armePrincipale &&
armePrincipale.portee > 0 &&
attributeAsInt(perso, 'attaqueADistanceRatee', 0) == 1) {
command = '!cof-attack ' + perso.token.id + ' @{target|token_id} -1 --semonce';
ligne += bouton(command, "Tir de semonce (L)", perso) + '
';
}
}
//L'action de traverser pour un cyclone
if (attributeAsBool(perso, 'cyclone')) {
let labelCyclone = getIntValeurOfEffet(perso, 'cyclone', 1);
let diffRenverse = 10 + modCarac(perso, 'force');
let commandTraverser = "!cof-attack @{selected|token_id} @{target|token_id} " + labelCyclone + " --auto --ifSaveFails DEXFOR " + diffRenverse + " --etat renverse --else --diviseDmg 2 --endif";
ligne += bouton(commandTraverser, 'Traverser', perso) + '
';
}
//Affichage du second souffle
if (actionsParDefaut && predicateAsBool(perso, 'secondSouffle') &&
!attributeAsBool(perso, 'secondSouffleUtilise')) {
let pvDebut = attributeAsInt(perso, 'PVsDebutCombat', 0);
let pv = parseInt(perso.token.get('bar1_value'));
if (!isNaN(pv) && pv < pvDebut) {
command = "!cof-soin @{selected|token_id} secondSouffle";
ligne += bouton(command, 'Second souffle', perso) + '
';
}
}
//Changement de phase pour intangibilité avec changement de phase
if (attributeAsBool(perso, 'intangiblePuissant')) {
if (attributeAsInt(perso, 'intangibleValeur', 1)) {
command = "!cof-set-attribute intangibleValeur 0 --target " + perso.token.id + " --message redevient tangible";
ligne += boutonSimple(command, "Redevenir tangible") + '
';
} else {
command = "!cof-set-attribute intangibleValeur 1 --target " + perso.token.id + " --message devient légèrement translucide";
ligne += boutonSimple(command, "Redevenir intangible") + '
';
}
}
if (attributeAsBool(perso, 'intangibleInvisiblePuissant')) {
if (attributeAsInt(perso, 'intangibleInvisibleValeur', 1)) {
command = "!cof-set-attribute intangibleInvisibleValeur 0 --etat invisible false --target " + perso.token.id + " --message réapparaît";
ligne += boutonSimple(command, "Redevenir tangible") + '
';
} else {
command = "!cof-set-attribute intangibleInvisibleValeur 1 --etat invisible true --target " + perso.token.id + " --message disparaît";
ligne += boutonSimple(command, "Redevenir intangible") + '
';
}
}
//La liste d'action proprement dite
actionsAAfficher = treatActions(perso, actionsDuTour, abilities, function(command, text, macros, attackStats) {
if (command == 'liste des attaques') {
//Dans ce cas, attackStats est une chaine d'options à ajouter
let attackOptions = {
ligneOptions: attackStats
};
if (gobePar) attackOptions.target = gobePar.token.id;
ligne += listeAttaquesVisibles(perso, attackOptions);
} else {
let options = {
attackStats
};
let b = bouton(command, text, perso, options);
if (!options.actionImpossible) ligne += b + '
';
}
});
if (predicateAsBool(perso, 'batonDesRunesMortes')) {
if (attributeAsBool(perso, 'runeIsulys') && attributeAsInt(perso, 'limiteParTour_runeIsulys', 1) > 0) {
let caracSag = modCarac(perso, 'sagesse');
let caracInt = modCarac(perso, 'intelligence');
let caracDM = caracSag;
if (caracInt > caracSag) caracDM = caracInt;
command = "!cof-attack " + perso.token.id + " @{target|token_id} Isulys --toucher [[@{selected|ATKMAG}]] --portee 40 --sortilege --dm 2d6";
if (caracDM > 0) command += '+' + caracDM;
else if (caracDM < 0) command += caracDM;
command += "--energie --magique --limiteParTour 1 runeIsulys --effet affaibli 1";
ligne += bouton(command, "Rayon d'énergie", perso) + '
';
}
command = "!cof-gerer-runes-mortes " + perso.token.id;
ligne += boutonSimple(command, "Gestion des runes mortes") + '
';
}
if (actionsParDefaut) {
actionsAAfficher = true;
command = "!cof-attendre ?{Nouvelle initiative}";
ligne += bouton(command, 'Attendre', perso) + '
';
if (!gobePar && !charAttributeAsBool(perso, 'armeeConjuree')) {
command = "!cof-action-defensive ?{Action défensive|simple|totale}";
ligne += bouton(command, 'Se défendre', perso) + '
';
let manoeuvreDuelliste = predicateAsBool(perso, 'manoeuvreDuelliste');
if (manoeuvreDuelliste) {
command = "!cof-manoeuvre @{selected|token_id} @{target|token_id} ?{Manoeuvre?|bloquer|desarmer|renverser|tenirADistance|repousser}";
ligne += bouton(command, 'Manoeuvres de duelliste', perso) + '
';
}
if (stateCOF.options.affichage.val.manoeuvres.val) {
if (manoeuvreDuelliste) {
command = "!cof-manoeuvre @{selected|token_id} @{target|token_id} ?{Manoeuvre?|aveugler|faireDiversion|menacer}";
ligne += bouton(command, 'Autres manoeuvres', perso) + '
';
} else {
command = "!cof-manoeuvre @{selected|token_id} @{target|token_id} ?{Manoeuvre?|aveugler|bloquer|desarmer|faireDiversion|menacer|renverser|tenirADistance|repousser}";
ligne += bouton(command, 'Manoeuvres', perso) + '
';
}
}
}
}
for (let etat in cof_states) {
let saveEtat = boutonSaveState(perso, etat);
if (saveEtat) {
ligne += saveEtat + '
';
actionsAAfficher = true;
}
}
}
if (actionsAAfficher) {
// on envoie la liste aux joueurs qui gèrent le personnage dont le token est lié
let lastPlayerid;
// on récupère les players_ids qui controllent le Token
let playerIds;
if (playerId) playerIds = [playerId];
else playerIds = getPlayerIds(perso);
playerIds.forEach(function(playerid) {
lastPlayerid = playerid;
let display = startFramedDisplay(playerid, title, perso, opt_display);
addLineToFramedDisplay(display, ligne);
sendFramedDisplay(display);
});
// En prime, on l'envoie au MJ, si besoin
let envoieAuMJ = playerIds.length === 0;
if (!envoieAuMJ && stateCOF.options.affichage.val.MJ_voit_actions.val) {
envoieAuMJ = playerIds.every(function(pid) {
return !playerIsGM(pid);
});
}
if (envoieAuMJ) {
opt_display.chuchote = 'gm';
const display = startFramedDisplay(lastPlayerid, title, perso, opt_display);
addLineToFramedDisplay(display, ligne);
sendFramedDisplay(display);
}
}
return actionsAAfficher;
}
function apiTurnAction(msg) {
const options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) return;
let liste;
if (cmd.length > 1) {
liste = cmd.slice(1).join(' ');
}
getSelected(msg, function(selected, playerId) {
iterSelected(selected, function(perso) {
let actions = turnAction(perso, playerId, liste);
if (!actions) {
let l = liste || '';
sendPerso(perso, "n'a pas de liste d'actions " + l + " définie");
}
});
}, options);
}
function removeDernieresCiblesAttaquees(perso, evt) {
let attrDernieresCibles = tokenAttribute(perso, 'dernieresCiblesAttaquees');
if (attrDernieresCibles.length > 0) {
attrDernieresCibles = attrDernieresCibles[0];
if (predicateAsBool(perso, 'attaqueEnMeute') || alliesDAttaqueEnMeute.has(perso.charId)) {
let dernieresCibles = attrDernieresCibles.get('current');
let cibles = new Set(dernieresCibles.split(' '));
cibles.forEach(function(ci) {
let cible = persoOfId(ci);
if (cible === undefined) return;
let attrCibleMeute = tokenAttribute(cible, 'attaqueParMeute');
if (attrCibleMeute.length > 0) {
attrCibleMeute = attrCibleMeute[0];
let cibleMeute = attrCibleMeute.get('current');
let ensembleCibleMeute = new Set(cibleMeute.split(' '));
if (ensembleCibleMeute.has(perso.token.id)) {
ensembleCibleMeute.delete(perso.token.id);
if (ensembleCibleMeute.size > 0) {
evt.attributes = evt.attributes || [];
evt.attributes.push({
attribute: attrCibleMeute,
current: cibleMeute,
});
cibleMeute = '';
ensembleCibleMeute.forEach(function(ai) {
if (cibleMeute === '') cibleMeute = ai;
else cibleMeute += ' ' + ai;
});
attrCibleMeute.set('current', cibleMeute);
} else {
evt.deletedAttributes = evt.deletedAttributes || [];
evt.deletedAttributes.push(attrCibleMeute);
attrCibleMeute.remove();
}
}
}
});
}
evt.deletedAttributes = evt.deletedAttributes || [];
evt.deletedAttributes.push(attrDernieresCibles);
attrDernieresCibles.remove();
}
}
function getTurnOrder(combat, evt) {
let turnOrder = Campaign().get('turnorder');
evt.turnorder = evt.turnorder || turnOrder;
if (turnOrder === '') {
turnOrder = [{
id: "-1",
pr: 1,
custom: "Tour",
formula: "+1"
}];
if (!evt.combat) evt.combat = {...combat
};
combat.tour = 1;
} else {
turnOrder = JSON.parse(turnOrder);
}
let indexTour = turnOrder.findIndex(function(elt) {
return (elt.id == "-1" && elt.custom == "Tour");
});
if (indexTour == -1) {
indexTour = turnOrder.length;
turnOrder.push({
id: "-1",
pr: 1,
custom: "Tour",
formula: "+1"
});
if (!evt.combat) evt.combat = {...combat
};
combat.tour = 1;
}
let res = {
tour: turnOrder[indexTour],
pasAgi: turnOrder.slice(0, indexTour),
dejaAgi: turnOrder.slice(indexTour + 1, turnOrder.length)
};
return res;
}
//ne rajoute pas evt à l'historique
function setTurnOrder(to, evt) {
if (to.pasAgi.length > 0) {
to.pasAgi.sort(function(a, b) {
if (a.id == "-1") return 1;
if (b.id == "-1") return -1;
if (a.pr < b.pr) return 1;
if (b.pr < a.pr) return -1;
// Priorité aux joueurs
// Premier critère : la barre de PV des joueurs est liée
let tokenA = getObj('graphic', a.id);
if (tokenA === undefined) return 1;
let tokenB = getObj('graphic', b.id);
if (tokenB === undefined) return -1;
if (tokenA.get('bar1_link') === '') {
if (tokenB.get('bar1_link') === '') return 0;
return 1;
}
if (tokenB.get('bar1_link') === '') return -1;
// Deuxième critère : les joueurs ont un DV
let charIdA = tokenA.get('represents');
if (charIdA === '') return 1;
let charIdB = tokenB.get('represents');
if (charIdB === '') return -1;
let persoA = {
token: tokenA,
charId: charIdA
};
let persoB = {
token: tokenB,
charId: charIdB
};
let dvA = ficheAttributeAsInt(persoA, "DV", 0);
let dvB = ficheAttributeAsInt(persoB, "DV", 0);
if (dvA === 0) {
if (dvB === 0) return 0;
return 1;
}
if (dvB === 0) return -1;
//Entre joueurs, priorité à la plus grosse sagesse
let sagA = caracCourante(persoA, 'sagesse');
let sagB = caracCourante(persoB, 'sagesse');
if (sagA < sagB) return 1;
if (sagA > sagB) return -1;
return 0;
});
setActiveToken(stateCOF.combat, to.pasAgi[0].id, evt);
}
to.pasAgi.push(to.tour);
let turnOrder = to.pasAgi.concat(to.dejaAgi);
Campaign().set('turnorder', JSON.stringify(turnOrder));
}
function attendreInit(msg) {
let combat = stateCOF.combat;
if (!combat) {
error("On ne peut pas attendre en dehors du combat", msg);
return;
}
getSelected(msg, function(selected) {
if (selected === undefined || selected.length === 0) {
error("La fonction !cof-attendre : rien à faire, pas de token selectionné", msg);
return;
}
let cmd = msg.content.split(' ');
if (cmd.length < 2) {
error("Attendre jusqu'à quelle initiative ?", cmd);
return;
}
let newInit = parseInt(cmd[1]);
if (isNaN(newInit) || newInit < 1) {
error("On ne peut attendre que jusqu'à une initiative de 1", cmd);
newInit = 1;
}
let evt = {
type: "attente"
};
let to = getTurnOrder(combat, evt);
iterSelected(selected, function(perso) {
let token = perso.token;
if (!isActive(perso)) return;
let tokenPos =
to.pasAgi.findIndex(function(elt) {
return (elt.id == token.id);
});
if (tokenPos == -1) { // token ne peut plus agir
sendPerso(perso, " a déjà agit ce tour");
return;
}
if (newInit < to.pasAgi[tokenPos].pr) {
to.pasAgi[tokenPos].pr = newInit;
sendPerso(perso, " attend un peu avant d'agir...");
updateNextInit(perso);
} else {
sendPerso(perso, " a déjà une initiative inférieure à " + newInit);
}
});
setTurnOrder(to, evt);
addEvent(evt);
});
}
// Affiche des informations sur le personnage sélectionné
function statut(msg) {
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
error("Dans !cof-statut : rien à faire, pas de token selectionné", msg);
return;
}
iterSelected(selected, function(perso) {
//Au cas où
unlockToken(perso);
const token = perso.token;
const charId = perso.charId;
const name = nomPerso(perso);
let lie = true;
if (token.get('bar1_link') === '') lie = false;
const display = startFramedDisplay(playerId, "État de " + name, perso, {
chuchote: true
});
const estPNJ = persoEstPNJ(perso);
let line;
let hasMana = false;
let manaAttr = [];
if (!estPNJ)
manaAttr = findObjs({
_type: 'attribute',
_characterid: charId,
name: 'PM'
}, {
caseInsensitive: true
});
if (manaAttr.length > 0) {
let manaMax = parseInt(manaAttr[0].get('max'));
hasMana = !isNaN(manaMax) && manaMax > 0;
}
let dmTemp = parseInt(token.get('bar2_value'));
if (hasMana) { //ne peut pas être un PNJ
if (lie) {
dmTemp = ficheAttributeAsInt(perso, 'DMTEMP', 0);
} else {
dmTemp = attributeAsInt(perso, 'DMTEMP', 0);
}
} else if (lie) {
dmTemp = ficheAttributeAsInt(perso, 'DMTEMP', 0);
}
if (!isNaN(dmTemp) && dmTemp > 0) {
line = "Dommages temporaires : " + dmTemp;
addLineToFramedDisplay(display, line);
}
let douleurIgnoree = attributeAsInt(perso, 'douleurIgnoree', 0);
if (douleurIgnoree > 0) {
line = "a ignoré " + douleurIgnoree + " pv dans ce combat.";
addLineToFramedDisplay(display, line);
}
let aDV = ficheAttributeAsInt(perso, 'DV', 0);
if (aDV > 0) { // correspond aux PJs
let pr = pointsDeRecuperation(perso);
if (pr) {
line =
"Points de récupération : " + pr.current + " / " + pr.max;
addLineToFramedDisplay(display, line);
}
if (ficheAttributeAsInt(perso, 'option_pc', 1)) {
let pc = 3;
let pc_max = 3;
let attr_pc = charAttribute(perso.charId, 'pc', {
caseInsensitive: true
});
if (attr_pc !== undefined && attr_pc.length > 0) {
pc = parseInt(attr_pc[0].get('current'));
if (isNaN(pc)) pc = 0;
pc_max = parseInt(attr_pc[0].get('max'));
if (isNaN(pc_max)) pc_max = 3;
}
line = "Points de chance : " + pc + " / " + pc_max;
addLineToFramedDisplay(display, line);
}
if (predicateAsBool(perso, 'pacifisme')) {
if (attributeAsBool(perso, 'attributDeCombat_pacifismeAnnule')) {
addLineToFramedDisplay(display, "Pacifisme non actif");
} else {
addLineToFramedDisplay(display, "Pacifisme actif");
}
}
}
let attrsChar = findObjs({
_type: 'attribute',
_characterid: charId
});
let attaques = listAllAttacks(perso);
let armeEnMain = armesEnMain(perso);
let armeEnMainGauche = perso.armeGauche;
let armeEnMainLabel;
if (armeEnMain) armeEnMainLabel = armeEnMain.label;
let armeEnMainGaucheLabel;
if (armeEnMainGauche) armeEnMainGaucheLabel = armeEnMainGauche.label;
_.forEach(attaques, function(att, armeLabel) {
let nomArme = att.armenom;
if (att.armespec && predicateOfRaw(att.armespec).charge) {
let charge = attributeAsInt(perso, 'charge_' + armeLabel, 0);
if (charge === 0) {
line = nomArme + " n'est pas chargé";
} else {
let na = fullAttributeName(perso, 'chargeGrenaille_' + armeLabel);
let grenaille = attrsChar.find(function(a) {
return a.get('name') == na;
});
if (grenaille) {
grenaille = parseInt(grenaille.get('current'));
if (isNaN(grenaille) || grenaille < 0) grenaille = 0;
} else grenaille = 0;
if (charge == 1) {
line = nomArme + " est chargé";
if (grenaille) line += " de grenaille";
} else if (charge > 1) {
line = nomArme + " contient encore " + charge + " charges";
if (grenaille == charge) line += " de grenaille";
else if (grenaille)
line += ", dont " + grenaille + " de grenaille";
}
}
if (armeEnMainLabel == armeLabel) line += " et en main";
else if (armeEnMainGaucheLabel == armeLabel) line += " et en main gauche";
else line += ", pas en main";
addLineToFramedDisplay(display, line);
} else if (armeEnMainLabel == armeLabel) {
if (armeEnMain.deuxMains) {
addLineToFramedDisplay(display, "tient " + nomArme + " à 2 mains.");
} else {
addLineToFramedDisplay(display, "tient " + nomArme + " en main.");
}
} else if (armeEnMainGaucheLabel == armeLabel) {
addLineToFramedDisplay(display, "tient " + nomArme + " en main gauche.");
} else if (att.armetypeattaque == "Arme de jet") {
let n = fieldAsInt(att, 'armejetqte', 1);
addLineToFramedDisplay(display, nomArme + " : " + n);
}
if (attributeAsBool(perso, 'enduitDePoison_' + armeLabel)) {
addLineToFramedDisplay(display, nomArme + " est enduit de poison.");
}
});
let attrEnveloppe = tokenAttribute(perso, 'enveloppePar');
if (attrEnveloppe.length > 0) {
let cube = persoOfIdName(attrEnveloppe[0].get('current'));
if (cube) {
let actE = "est enveloppé dans ";
if ((attrEnveloppe[0].get('max') + '').startsWith('etreinte')) actE = "est prisonnier de l'étreinte de ";
addLineToFramedDisplay(display, actE + cube.tokName);
}
}
let pageId = perso.token.get('pageid');
let defense = defenseOfPerso(undefined, perso, pageId, undefined, {
test: true
});
let defenseMontree;
let bufDef = attributeAsInt(perso, 'bufDEF', 0);
if (bufDef > 0) {
addLineToFramedDisplay(display, "Défense temporairement modifiée de " + bufDef + " (DEF " + defense + ")");
defenseMontree = true;
}
for (let etat in cof_states) {
if (getState(perso, etat)) {
let markerName = cof_states[etat].substring(7).split("::")[0];
let marker = markerCatalog[markerName];
let etext = stringOfEtat(etat, perso);
if (marker) {
etext = " " + etext;
}
let saveEtat = boutonSaveState(perso, etat);
if (saveEtat) etext += ", " + saveEtat;
addLineToFramedDisplay(display, etext);
}
}
if (ficheAttributeAsInt(perso, 'defarmureon', 0) === 0) {
let possedeArmure = ficheAttributeAsInt(perso, 'defarmure', 0) > 0;
if (possedeArmure) addLineToFramedDisplay(display, "Ne porte pas son armure");
if (predicateAsInt(perso, 'vetementsSacres', 0) > 0) {
if (possedeArmure) addLineToFramedDisplay(display, " mais bénéficie de ses vêtements sacrés (DEF " + defense + ")");
else addLineToFramedDisplay(display, "porte des vêtements sacrés (DEF " + defense + ")");
defenseMontree = true;
}
if (predicateAsInt(perso, 'armureDeVent', 0) > 0) {
if (possedeArmure) addLineToFramedDisplay(display, " mais bénéficie de son armure de vent (DEF " + defense + ")");
else addLineToFramedDisplay(display, "bénéficie de son armure de vent (DEF " + defense + ")");
defenseMontree = true;
}
}
let armures = listAllArmors(perso);
let aBouclier;
let casques = [];
for (let label in armures) {
let a = armures[label];
switch (a.typearmure) {
case 'Casque':
casques.push(a);
break;
case 'Bouclier':
aBouclier = true;
break;
}
}
if (ficheAttributeAsInt(perso, 'defbouclieron', 0) === 0 &&
(aBouclier || ficheAttributeAsBool(perso, 'defbouclier', false)))
addLineToFramedDisplay(display, "Ne porte pas son bouclier");
if (casques.length > 0 || ficheAttributeAsInt(perso, 'casque_rd', 0)) {
if (ficheAttributeAsBool(perso, 'casque_on', false)) {
let b = boutonSimple('!cof-mettre-casque 0 --target ' + perso.token.id, "l'enlever");
addLineToFramedDisplay(display, "Porte son casque :" + b);
} else {
let ligne;
let action = '!cof-mettre-casque ';
let bmsg;
if (casques.length > 1) {
ligne = "Ne porte pas de casque :";
bmsg = "en mettre un";
action += "?{";
casques.forEach(function(c) {
action += "|" + c.nomarmure + "," + c.labelarmure;
});
action += "}";
} else {
ligne = "Ne porte pas son casque :";
bmsg = "le mettre";
action += '-1';
}
let b = boutonSimple(action + ' --target ' + perso.token.id, bmsg);
addLineToFramedDisplay(display, ligne + b);
}
}
if (attributeAsBool(perso, 'etatExsangue')) {
addLineToFramedDisplay(display, "est exsangue");
}
if (attributeAsBool(perso, 'malediction')) {
addLineToFramedDisplay(display, "est maudit" + eForFemale(perso) + "...");
}
const allAttrs = findObjs({
_type: 'attribute',
_characterid: charId
});
allAttrs.forEach(function(attr) {
const attrName = attr.get('name');
if (!lie && !attrName.endsWith('_' + name)) return;
if (estEffetTemp(attrName)) {
let effet = effetTempOfAttribute(attr);
let mt = messageEffetTemp[effet];
if (lie) {
if (mt.generic) {
if (attrName.indexOf(')_') > 0) return;
} else if (effet != attrName) return;
}
let explEffetMsg = messageActif(perso, mt);
if (stateCOF.options.affichage.val.duree_effets.val || playerIsGM(playerId)) {
let effetVal = attr.get('current');
if (parseInt(effetVal)) {
explEffetMsg += " (" + effetVal + " tour" + ((effetVal > 1) ? 's' : '') + ")";
} else {
explEffetMsg += " (tour final)";
}
}
addLineToFramedDisplay(display, explEffetMsg);
} else if (estEffetCombat(attrName)) {
let effetC = effetCombatOfAttribute(attr);
if (lie && effetC != attrName) return;
addLineToFramedDisplay(display, messageActif(perso, messageEffetCombat[effetC]));
} else if (estEffetIndetermine(attrName)) {
let effetI = effetIndetermineOfAttribute(attr);
if (lie && effetI != attrName) return;
let mi = messageActif(perso, messageEffetIndetermine[effetI]);
if (playerIsGM(playerId)) {
mi += ' ' + boutonSimple('!cof-effet ' + effetI + ' false --target ' + perso.token.id, 'X');
}
addLineToFramedDisplay(display, mi);
} else if (attrName.startsWith('perteDeSubstance')) {
if (!predicateAsBool(perso, 'perteDeSubstance')) {
attr.remove();
return;
}
let perteDeSubstance = parseInt(attr.get('current'));
if (isNaN(perteDeSubstance) || perteDeSubstance < 1) {
error("Attribut de perte de substance mal formé", attr);
return;
}
if (playerIsGM(playerId)) {
let m = "Perte de substance depuis " + perteDeSubstance + " jour" + ((perteDeSubstance > 1) ? 's' : '') + ' ';
m += boutonSimple('!cof-set-attribute perteDeSubstance ?{Nombre de jours} --target ' + perso.token.id, 'X');
addLineToFramedDisplay(display, m);
}
if (perteDeSubstance < 3) {
addLineToFramedDisplay(display, "Dans le noir, on peut distinguer les lumières vives au travers de son corps");
} else if (perteDeSubstance < 5) {
addLineToFramedDisplay(display, "Le soleil diffuse à travers le corps à contre-jour");
} else if (perteDeSubstance < 7) {
addLineToFramedDisplay(display, "On peut distinguer les objets à travers ses mains, et sa couleur s'estompe");
} else if (perteDeSubstance < 10) {
addLineToFramedDisplay(display, "Est translucide comme une eau sale");
} else if (perteDeSubstance < 15) {
addLineToFramedDisplay(display, "Est aussi transparent que de l'eau et n'a plus faim ni soif");
} else {
addLineToFramedDisplay(display, "A perdu sa substance : impossible de tenir un objet et possibilité de passer à travers des obstacles");
}
}
});
//ancienne version de munitions, obsolète depuis mars 2023
allAttributesNamed(attrsChar, 'munition').forEach(function(attr) {
let attrName = attr.get('name');
let underscore = attrName.indexOf('_');
if (underscore < 0 || underscore == attrName.length - 1) return;
let munitionNom = attrName.substring(underscore + 1).replace(/_/g, ' ');
addLineToFramedDisplay(display, munitionNom + " : " + attr.get('current') + " / " + attr.get('max'));
});
let attrPosture = tokenAttribute(perso, 'postureDeCombat');
if (attrPosture.length > 0) {
attrPosture = attrPosture[0];
let posture = attrPosture.get('max');
let postureMsg = "a une posture ";
switch (posture.substr(-3, 3)) {
case 'DEF':
postureMsg += "défensive";
break;
case 'ATT':
postureMsg += "offensive";
break;
case '_DM':
postureMsg += "puissante";
break;
default:
}
postureMsg += " mais ";
switch (posture.substr(0, 3)) {
case 'DEF':
postureMsg += "risquée";
break;
case 'ATT':
postureMsg += "moins précise";
break;
case 'DM_':
postureMsg += "moins puissante";
break;
default:
}
addLineToFramedDisplay(display, postureMsg);
}
let attaqueAOutrance = attributeAsInt(perso, 'attaqueAOutrance', 0);
if (attaqueAOutrance) {
let attaqueAOutranceMsg = "attaque à outrance ";
switch (attaqueAOutrance) {
case 2:
attaqueAOutranceMsg += "(-2 DEF, +1D6 DM)";
break;
case 5:
attaqueAOutranceMsg += "(-5 DEF, +2D6 DM)";
break;
default:
}
addLineToFramedDisplay(display, attaqueAOutranceMsg);
}
let rangSoin = predicateAsInt(perso, 'voieDesSoins', 0);
if (rangSoin > 0) {
let msgSoins;
let soinsRestants;
let soins = "";
let soinsLegers = attributeAsInt(perso, 'soinsLegers', 0);
if (soinsLegers < rangSoin) {
soinsRestants = rangSoin - soinsLegers;
if (soinsRestants > 1) soins = 's';
msgSoins = "peut encore faire " + soinsRestants + " soin" + soins + " léger" + soins;
addLineToFramedDisplay(display, msgSoins);
} else {
addLineToFramedDisplay(display, "ne peut plus faire de soin léger aujourd'hui");
}
if (rangSoin > 1) {
let soinsModeres = attributeAsInt(perso, 'soinsModeres', 0);
if (soinsModeres < rangSoin) {
soinsRestants = rangSoin - soinsModeres;
if (soinsRestants > 1) soins = 's';
else soins = '';
msgSoins = "peut encore faire " + soinsRestants + " soin" + soins + " modéré" + soins;
addLineToFramedDisplay(display, msgSoins);
} else {
addLineToFramedDisplay(display, "ne peut plus faire de soin modéré aujourd'hui");
}
}
if (rangSoin > 3) {
let soinsGuerison = attributeAsInt(perso, 'limiteParJour_guérison', rangSoin);
if (soinsGuerison > 0) {
addLineToFramedDisplay(display, "peut encore faire " + soinsGuerison + " guérison" + (soinsGuerison > 1 ? 's' : '') + " aujourd'hui");
} else {
addLineToFramedDisplay(display, "ne peut plus faire de guérison aujourd'hui");
}
}
}
let voieDesExplosifs = predicateAsInt(perso, 'voieDesExplosifs', 0);
if (voieDesExplosifs) {
let charges = attributeAsInt(perso, 'limiteParJour_chargesExplosives', voieDesExplosifs);
if (charges > 0) {
let s = charges > 1 ? 's' : '';
addLineToFramedDisplay(display, "peut encore utiliser " + charges + " charge" + s + " explosive" + s + " aujourd'hui");
} else {
addLineToFramedDisplay(display, "ne peut plus utiliser de charge explosive aujourd'hui");
}
}
let ebriete = attributeAsInt(perso, 'niveauEbriete', 0);
if (ebriete > 0 && ebriete < niveauxEbriete.length) {
addLineToFramedDisplay(display, "est " + niveauxEbriete[ebriete]);
}
let bonusCouvert = attributeAsInt(perso, 'bonusCouvert');
if (bonusCouvert) {
addLineToFramedDisplay(display, "est à couvert (+" + bonusCouvert + " DEF)");
}
if (!defenseMontree) {
let defenseAffichee = 10;
if (estPNJ) {
defenseAffichee = ficheAttributeAsInt(perso, 'pnj_def', 10);
} else {
defenseAffichee += ficheAttributeAsInt(perso, 'defarmure', 0) * ficheAttributeAsInt(perso, 'defarmureon', 0);
defenseAffichee += ficheAttributeAsInt(perso, 'defbouclier', 0) * ficheAttributeAsInt(perso, 'defbouclieron', 0);
defenseAffichee += ficheAttributeAsInt(perso, 'DEFDIV', 0);
defenseAffichee += modCarac(perso, 'dexterite');
}
if (defense != defenseAffichee)
addLineToFramedDisplay(display, "Défense actuelle : " + defense);
}
let predicatExpertDuCombat = predicateAsInt(perso, "expertDuCombat", 0);
if (stateCOF.combat && predicatExpertDuCombat > 0) {
let nbDesExpertDuCombat_combat_max = predicatExpertDuCombat * 2;
let nbDesExpertDuCombat_combat = attributeAsInt(perso, "limiteParCombat_expertDuCombat", predicatExpertDuCombat * 2);
let nbDesExpertDuCombat_tour_max;
if (predicatExpertDuCombat > 4) nbDesExpertDuCombat_tour_max = 3;
else if (predicatExpertDuCombat > 2) nbDesExpertDuCombat_tour_max = 2;
else nbDesExpertDuCombat_tour_max = 1;
let nbDesExpertDuCombat_tour = Math.min(nbDesExpertDuCombat_combat,
attributeAsInt(perso, "limiteParTour_expertDuCombat", nbDesExpertDuCombat_tour_max));
addLineToFramedDisplay(display, "Dés d'expertise du combat :
" +
"Tour : " + nbDesExpertDuCombat_tour + "/" + nbDesExpertDuCombat_tour_max + "
" +
"Combat : " + nbDesExpertDuCombat_combat + "/" + nbDesExpertDuCombat_combat_max + "
");
}
//Affaiblissements de caractéristiques
allCaracs.forEach(function(carac) {
let malus = attributeAsInt(perso, 'affaiblissementde' + carac, 0);
if (malus > 0) {
let normal = caracNormale(perso, carac);
let ligne = carac + " : " + (normal - malus) + " / " + normal;
addLineToFramedDisplay(display, ligne);
}
});
//Violence ciblée
if (predicateAsBool(perso, 'violenceCiblee')) {
let pointsDeViolence = attributeAsInt(perso, 'pointsDeViolence', 0);
if (pointsDeViolence > 0) {
addLineToFramedDisplay(display, "Points de violence : " + pointsDeViolence);
}
}
if (attributeAsBool(perso, 'lumiere')) {
addLineToFramedDisplay(display, "éclaire ou fait de la lumière");
}
let autresAttributs = predicatesNamed(perso, 'attributsDeStatut');
autresAttributs.forEach(function(attr) {
let listeAttrs = attr.split(',');
listeAttrs.forEach(function(a) {
a = a.trim();
if (a === '') return;
let aDisplay = tokenAttribute(perso, a);
aDisplay.forEach(function(ad) {
let line = a + " : " + ad.get('current');
let admax = ad.get('max');
if (admax) line += " / " + admax;
addLineToFramedDisplay(display, line);
});
});
});
sendFramedDisplay(display);
}); //fin du iterSelected
});
}
//retourne l'id du suivant si le token actuel était en tête de liste
function removeFromTurnTracker(perso, evt) {
removeDernieresCiblesAttaquees(perso, evt);
let tokenId = perso.token.id;
let turnOrder = Campaign().get('turnorder');
if (turnOrder === '' || !stateCOF.combat) {
return;
}
evt.turnorder = evt.turnorder || turnOrder;
turnOrder = JSON.parse(turnOrder);
if (turnOrder.length === 0) return;
let res;
let change;
if (turnOrder[0].id == tokenId) {
change = true;
if (turnOrder.length > 1) {
res = {
nextId: turnOrder[1].id
};
turnOrder.shift();
if (turnOrder[0].id == "-1" && turnOrder[0].custom == "Tour") {
//Il faut aussi augmenter la valeur du tour
let tour = parseInt(turnOrder[0].pr);
if (isNaN(tour)) {
error("Tour invalide", turnOrder);
return;
}
turnOrder[0].pr = tour + 1;
}
let cmp = Campaign();
cmp.set('turnorder', JSON.stringify(turnOrder));
nextTurn(cmp, undefined, evt);
return res;
} else {
res = {
nextId: false
};
turnOrder = [];
}
} else {
turnOrder = turnOrder.filter(
function(elt) {
let f = elt.id == tokenId;
change = change || f;
return !f;
});
}
if (change) Campaign().set('turnorder', JSON.stringify(turnOrder));
return res;
}
function replaceInTurnTracker(tidOld, tidNew, evt) {
let combat = stateCOF.combat;
if (!combat) return;
let turnOrder = Campaign().get('turnorder');
if (turnOrder === '') return;
evt.turnorder = evt.turnorder || turnOrder;
turnOrder = JSON.parse(turnOrder);
turnOrder.forEach(function(elt) {
if (elt.id == tidOld) elt.id = tidNew;
});
Campaign().set('turnorder', JSON.stringify(turnOrder));
if (tidOld == combat.activeTokenId)
combat.activeTokenId = tidNew;
}
function armureMagique(msg) {
msg.content += " armureMagique";
effetCombat(msg);
}
function bufDef(msg) {
let cmd = msg.content.split(' ');
if (cmd.length < 2) {
error("La fonction !cof-buf-def attend un argument", cmd);
return;
}
let buf = parseInt(cmd[1]);
if (isNaN(buf)) {
error("Argument de !cof-buf-def invalide", cmd);
return;
}
if (buf === 0) return;
let message = "";
if (buf > 0) message = "voit sa défense augmenter";
else message = "voit sa défense baisser";
let evt = {
type: 'other'
};
getSelected(msg, function(selected, playerId) {
if (selected === undefined || selected.length === 0) {
sendPlayer(msg, "Pas de token sélectionné pour !cof--buf-def", playerId);
}
iterSelected(selected, function(perso) {
setTokenAttr(perso, 'bufDEF', buf, evt, {
msg: message
});
setToken(perso.token, 'status_blue', buf, evt);
});
if (evt.attributes.length === 0) {
error("Pas de cible valide sélectionnée pour !cod-buf-def", msg);
return;
}
addEvent(evt);
});
}
function removeBufDef(msg) {
const evt = {
type: 'other'
};
getSelected(msg, function(selected, playerId) {
if (selected === undefined || selected.length === 0) {
sendPlayer(msg, "Pas de token sélectionné pour !cof-remove-buf-def", playerId);
}
iterSelected(selected, function(perso) {
removeTokenAttr(perso, 'bufDEF', evt, {
msg: "retrouve sa défense normale"
});
setToken(perso.token, 'status_blue', false, evt);
});
addEvent(evt);
});
}
function parseDmgOptions(text, options) {
let optArgs = text.split(' --');
optArgs.forEach(function(opt) {
opt = opt.trim().split(' ');
opt = opt.filter(function(c) {
return c !== '';
});
switch (opt[0]) {
case 'psave':
let psaveopt = options;
if (options.additionalDmg && opt.length > 3 && opt[3] == 'local') {
let psavel = options.additionalDmg.length;
if (psavel > 0) {
psaveopt = options.additionalDmg[psavel - 1];
}
}
let psaveParams = parseSave(opt);
if (psaveParams) {
psaveopt.partialSave = psaveParams;
}
return;
case 'asphyxie':
case 'affute':
case "metal":
case 'magique':
case 'artificiel':
case 'tranchant':
case 'percant':
case 'contondant':
case 'tempDmg':
case 'mortsVivants':
case 'ignoreMoitieRD':
case 'maxDmg':
case 'sortilege':
options[opt[0]] = true;
return;
case 'feu':
case 'froid':
case 'acide':
case 'electrique':
case 'sonique':
case 'poison':
case 'maladie':
case 'argent':
case 'energie':
if (options.additionalDmg) {
let l = options.additionalDmg.length;
if (l > 0) {
options.additionalDmg[l - 1].type = opt[0];
} else {
options.type = opt[0];
}
} else options.type = opt[0];
return;
case 'nature':
case 'naturel':
options.nature = true;
return;
case 'vampirise':
{
let vampirise = 100;
if (opt.length > 1) {
vampirise = parseInt(opt[1]);
if (isNaN(vampirise)) {
error("Il faut un pourcentage entier comme argument à --vampirise", opt);
vampirise = 100;
}
}
options.vampirise = vampirise;
return;
}
case "ignoreRD":
if (opt.length < 2) {
options.ignoreTouteRD = true;
return;
}
options.ignoreRD = parseInt(opt[1]);
if (isNaN(options.ignoreRD) || options.ignoreRD < 1) {
log("Pas un nombre positif après --ignoreRD, interprété comme ignore toute la RD");
options.ignoreRD = undefined;
options.ignoreTouteRD = true;
}
return;
case 'attaquant':
{
if (opt.length < 2) {
error("Manque l'id de l'attaquant, option ignorée", optArgs);
return;
}
const attaquant = persoOfId(opt[1]);
if (attaquant) {
options.attaquant = attaquant;
return;
}
error("Attaquant non trouvé", opt);
return;
}
case 'titre':
if (opt.length < 2) {
error("Il manque le message après --message", text);
return;
}
options.titre = opt.slice(1).join(' ');
return;
}
});
}
// Ne pas remplacer les inline rolls, il faut les afficher correctement
function parseDmgDirects(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 2) {
error("cof-dmg prend les dégats en argument, avant les options",
msg.content);
return;
}
getSelected(msg, function(selected, playerId) {
if (selected === undefined || selected.length === 0) {
sendPlayer(msg, "pas de cible trouvée, action annulée", playerId);
return;
}
options.aoe = true;
parseDmgOptions(msg.content, options);
let cibles = [];
iterSelected(selected, function(perso) {
cibles.push(perso);
});
cibles = enleveDoublonsPartagePV(cibles);
if (options.return) return;
//L'expression à lancer est tout ce qui est entre le premier blanc et le premier --
let debutDmgRollExpr = msg.content.indexOf(' ') + 1;
let dmgRollExpr = msg.content.substring(debutDmgRollExpr);
let finDmgRollExpr = msg.content.indexOf(' --');
if (finDmgRollExpr > debutDmgRollExpr)
dmgRollExpr = msg.content.substring(debutDmgRollExpr, finDmgRollExpr);
else dmgRollExpr = msg.content.substring(debutDmgRollExpr);
dmgRollExpr = dmgRollExpr.trim();
let dmgType = options.type || 'normal';
let dmg = {
type: dmgType,
value: dmgRollExpr
};
if (options.maxDmg) {
dmgRollExpr = dmgRollExpr.replace(/d([1-9])/g, "*$1");
}
let playerName = msg.who;
if (playerIsGM(playerId)) playerName = 'GM';
dmgDirects(playerId, playerName, cibles, dmg, options);
}, options); //fin du getSelected
}
function copyDmgOptionsToTarget(target, options) {
target.ignoreRD = options.ignoreRD;
target.ignoreTouteRD = options.ignoreTouteRD;
target.ignoreMoitieRD = options.ignoreMoitieRD;
target.tempDmg = options.tempDmg;
target.attaquant = options.lanceur;
}
function dmgDirects(playerId, playerName, cibles, dmg, options) {
let evt;
if (options.evt) {
evt = options.evt;
} else {
evt = {
type: 'dmgDirects'
};
addEvent(evt);
}
evt.action = {
titre: "Dégâts",
playerId: playerId,
playerName: playerName,
cibles: cibles,
dmg: dmg,
options: options
};
if (options.lanceur && limiteRessources(options.lanceur, options, 'dmg', 'dmg', evt)) return;
let action = "Dégâts. ";
if (options.titre) action += options.titre + "
";
if (options.partialSave) {
action +=
" Jet de " + options.partialSave.carac + " difficulté " + options.partialSave.seuil +
" pour réduire les dégâts";
}
let display = startFramedDisplay(playerId, action);
let tokensToProcess = cibles.length;
let someDmgDone;
let finalDisplay = function() {
if (tokensToProcess == 1) {
if (someDmgDone) {
sendFramedDisplay(display);
} else {
sendPlayer(playerName, "Aucune cible valide n'a été sélectionée");
}
}
tokensToProcess--;
};
dmg.rolls = dmg.rolls || [];
cibles.forEach(function(perso) {
if (getState(perso, 'mort')) { //pas de dégâts aux morts
finalDisplay();
return;
}
if (options.mortsVivants && !(estMortVivant(perso))) {
sendPlayer(playerName, nomPerso(perso) + " n'est pas un mort-vivant");
finalDisplay();
return;
}
let name = nomPerso(perso);
let explications = [];
copyDmgOptionsToTarget(perso, options);
try {
sendChat('', '[[' + dmg.value + ']]', function(resDmg) {
dmg.rolls[perso.token.id] = dmg.rolls[perso.token.id] || resDmg[0];
let roll = dmg.rolls[perso.token.id];
let afterEvaluateDmg = roll.content.split(' ');
let dmgRollNumber = rollNumber(afterEvaluateDmg[0]);
dmg.total = roll.inlinerolls[dmgRollNumber].results.total;
dmg.display = buildinline(roll.inlinerolls[dmgRollNumber], dmg.type, options.magique);
dealDamage(perso, dmg, [], evt, false, options, explications, function(dmgDisplay, dmgFinal) {
someDmgDone = true;
addLineToFramedDisplay(display,
name + " reçoit " + dmgDisplay + " DM");
explications.forEach(function(e) {
addLineToFramedDisplay(display, e, 80, false);
});
finalDisplay();
});
}); //fin du jet de dés
} catch (rollError) {
error("Jet " + dmg.value + " mal formé", dmg);
}
}); //fin forEach
}
function estElementaire(t) {
if (t === undefined) return false;
return (t == "feu" || t == "froid" || t == "acide" || t == "electrique");
}
function parseSetState(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 3) {
error("Pas assez d'arguments pour !cof-set-state", msg.content);
return;
}
let etat = cmd[1];
let valeur = cmd[2];
if (valeur == 'false' || valeur == '0' || valeur == 'non' || valeur == 'no') valeur = false;
if (valeur == 'true' || valeur == 'oui' || valeur == 'yes') valeur = true;
if (!_.has(cof_states, etat)) {
error("Le premier argument de !cof-set-state n'est pas un état valide", cmd);
return;
}
if (isCarac(cmd[2])) {
if (cmd.length < 4) {
error("Il manque la difficulté du jet de sauvegarde.", cmd);
return;
}
valeur = true;
options.saveActifParTour = {
carac: cmd[2]
};
let opposition = persoOfId(cmd[3]);
if (opposition) {
options.saveActifParTour.difficulte = cmd[3] + ' ' + nomPerso(opposition);
} else {
options.saveActifParTour.difficulte = parseInt(cmd[3]);
if (isNaN(options.saveActifParTour.difficulte)) {
error("Difficulté du jet de sauvegarde incorrecte", cmd);
return;
}
}
}
let cibles = [];
getSelected(msg, function(selected, playerId, aoe) {
if (selected === undefined || selected.length === 0) {
error("Pas de cible pour le changement d'état", msg);
return;
}
options.aoe = aoe;
iterSelected(selected, function(perso) {
if (options.seulementVivant && estNonVivant(perso)) {
sendPlayer(msg, "cet effet n'affecte que les créatures vivantes", playerId);
return;
}
switch (etat) {
case 'apeure':
case 'endormi':
if (predicateAsBool(perso, 'liberteDAction')) {
sendPerso(perso, "reste libre de ses mouvements !");
return;
}
break;
case 'paralyse':
if (predicateAsBool(perso, 'liberteDAction')) {
sendPerso(perso, "reste libre de ses mouvements !");
return;
}
if ((options.magique || options.mana != undefined) &&
predicateAsBool(perso, 'actionLibre')) {
sendPerso(perso, "reste libre de ses mouvements !");
return;
}
break;
case 'immobilise':
case 'ralenti':
if (predicateAsBool(perso, 'liberteDAction')) {
sendPerso(perso, "reste libre de ses mouvements !");
return;
}
if ((options.magique || options.mana != undefined) &&
predicateAsBool(perso, 'actionLibre')) {
sendPerso(perso, "reste libre de ses mouvements !");
return;
}
if (predicateAsInt(perso, 'voieDeLArchange', 1) > 1 && attributeAsBool(perso, 'formeDAnge')) {
sendPerso(perso, "reste libre de ses mouvements !");
return;
}
break;
default:
}
cibles.push(perso);
});
}, options);
doSetState(cibles, etat, valeur, options);
}
function doSetState(cibles, etat, valeur, options) {
let evt = {
type: "set_state",
action: {
titre: "Interface Set State",
cibles: cibles,
etat: etat,
valeur: valeur,
options: options
}
};
addEvent(evt);
if (options.terrainDifficile && options.aoe && options.aoe.type == 'disque') {
ajouteTerrainDifficile(options, evt);
}
let lanceur = options.lanceur;
if (lanceur === undefined && cibles.length == 1) lanceur = persoOfId(cibles[0].token.id);
if (limiteRessources(lanceur, options, etat, etat, evt)) return;
if (options.messages) {
options.messages.forEach(function(m) {
if (lanceur) sendPerso(lanceur, m, options.secret);
else sendChat('', m);
});
}
if (options.messagesMJ) {
let source = 'COF';
if (lanceur) source = 'character|' + lanceur.charId;
options.messagesMJ.forEach(function(m) {
sendChat(source, '/w gm ' + m);
});
}
cibles.forEach(function(perso) {
let setEffect = function() {
setState(perso, etat, valeur, evt);
effetsSpeciaux(lanceur, perso, options);
if (options.saveActifParTour) {
setTokenAttr(perso, etat + 'Save', options.saveActifParTour.carac, evt, {
maxVal: options.saveActifParTour.difficulte
});
}
};
if (options.save) {
let saveOpts = {
msgPour: " pour résister à l'effet " + stringOfEtat(etat),
msgRate: ", raté.",
rolls: options.rolls,
chanceRollId: options.chanceRollId,
type: options.type
};
let expliquer = function(s) {
sendPerso(perso, s);
};
let saveId = 'effet_' + etat + '_' + perso.token.id;
save(options.save, perso, saveId, expliquer, saveOpts, evt, function(reussite, rollText) {
if (!reussite) {
setEffect();
}
});
} else {
setEffect();
}
});
}
function textOfSaveState(etat, perso) {
switch (etat) {
case 'immobilise':
return "se libérer";
case 'aveugle':
return "retrouver la vue";
case 'etourdi':
return "reprendre ses esprits";
case 'assomme':
return "reprendre conscience";
case 'renverse':
return "se relever";
case 'endormi':
return "se réveiller";
case 'apeure':
return "retrouver du courage";
case 'enseveli':
return "sortir de terre";
default:
return "ne plus être " + stringOfEtat(etat, perso);
}
}
function parseSaveState(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 4 || !_.has(cof_states, cmd[1])) {
error("Paramètres de !cof-save-state incorrects", cmd);
return;
}
let etat = cmd[1];
let carac = cmd[2];
let carac2;
if (!isCarac(carac)) {
if (carac.length == 6) {
carac2 = carac.substring(3, 6);
carac = carac.substring(0, 3);
if (!isCarac(carac) || !isCarac(carac)) {
error("Paramètres de !cof-save-state incorrects", cmd);
return;
}
} else {
error("Paramètres de !cof-save-state incorrects", cmd);
return;
}
}
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
error("Pas de token sélectionné", msg.content);
return;
}
let pageId = options.pageId;
if (pageId === undefined) {
iterSelected(selected, function(perso) {
if (pageId) return;
pageId = perso.token.get('pageid');
});
}
let opposant;
if (cmd.length > 4) opposant = persoOfId(cmd[3], cmd[4], pageId);
if (opposant) {
iterSelected(selected, function(perso) {
if (!getState(perso, etat)) {
sendPerso(perso, "n'est pas " + stringOfEtat(etat, perso));
return;
}
doSaveState(playerId, perso, etat, carac, options, opposant);
});
} else {
let seuil = parseInt(cmd[3]);
if (isNaN(seuil)) {
error("La difficulté n'est pas un nombre", cmd);
return;
}
iterSelected(selected, function(perso) {
if (!getState(perso, etat)) {
sendPerso(perso, "n'est pas " + stringOfEtat(etat, perso));
return;
}
if (carac2) carac = meilleureCarac(carac, carac2, perso, seuil);
doSaveState(playerId, perso, etat, carac, options, undefined, seuil);
});
}
}, options);
}
function doSaveState(playerId, perso, etat, carac, options, opposant, seuil) {
let evt = {
type: "save_state",
action: {
titre: "Interface Save State",
perso: perso,
etat: etat,
carac: carac,
options: options,
opposant: opposant,
seuil: seuil,
playerId: playerId
}
};
addEvent(evt);
let titre = "Jet " + deCarac(carac) + " pour " + textOfSaveState(etat, perso);
if (opposant) {
let display = startFramedDisplay(playerId, titre, perso, {
perso2: opposant
});
let explications = [];
let rollId = 'saveState_' + perso.token.id;
testOppose(rollId, perso, carac, options, opposant, carac,
options, explications, evt,
function(resultat, crit, rt1, rt2) {
if (resultat == 2) {
explications.push(nomPerso(perso) + " est toujours " + stringOfEtat(etat, perso));
} else {
setState(perso, etat, false, evt);
explications.push(nomPerso(perso) + " n'est plus " + stringOfEtat(etat, perso));
}
explications.forEach(function(e) {
addLineToFramedDisplay(display, e);
});
sendFramedDisplay(display);
});
} else {
let testId = 'saveState_' + carac + seuil;
testCaracteristique(perso, carac, seuil, testId, options, evt, function(res) {
sendPerso(perso, titre);
if (res.reussite) {
setState(perso, etat, false, evt);
sendPerso(perso, res.texte + " ≥ " + seuil + ", " + nomPerso(perso) + " n'est plus " + stringOfEtat(etat, perso) + res.modifiers);
} else {
sendPerso(perso, res.texte + " < " + seuil + ", " + nomPerso(perso) + " est toujours " + stringOfEtat(etat, perso) + res.rerolls + res.modifiers);
}
});
}
}
//Renvoie false si le personnage n'a pas d'attribut etatSave
function boutonSaveState(perso, etat) {
let attr = tokenAttribute(perso, etat + 'Save');
if (attr.length === 0) return false;
attr = attr[0];
let carac = attr.get('current');
let action = "!cof-save-state " + etat + ' ' + carac + ' ' + attr.get('max');
if (etat == 'enseveli') action += " --bonus ?{Bonus au jet}";
let b = bouton(action, "Jet", perso);
return b + " pour " + textOfSaveState(etat, perso);
}
//!cof-save-effet token_id attr_id
// où attr_id est l'id de l'attribut de save
function parseSaveEffet(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 3) {
error("Paramètres de !cof-save-effet incorrects", cmd);
return;
}
let perso = persoOfId(cmd[1]);
let attr = getObj('attribute', cmd[2]);
if (attr === undefined || perso === undefined) {
error("Impossible de trouver le personnage ou l'attribut", cmd);
return;
}
let attrName = attr.get('name');
let indexSave = attrName.indexOf('SaveActifParTour');
if (indexSave < 0) {
error("L'attribut n'est pas un attribut de save actif", attrName);
return;
}
let effetC = attrName.substring(0, indexSave);
let effetTemp = estEffetTemp(effetC);
if (!effetTemp && !estEffetCombat(effetC)) {
error("Impossible de trouver l'effet correspondant à " + effetC, attrName);
return;
}
attrName = effetC + attrName.substr(indexSave + 16);
let met;
if (effetTemp) met = messageOfEffetTemp(effetC);
else met = messageEffetCombat[effetC];
let msgPour = " pour ";
if (met.msgSave) msgPour += met.msgSave;
else {
msgPour += "ne plus être sous l'effet de ";
if (effetC.startsWith('dotGen('))
msgPour += effetC.substring(7, effetC.indexOf(')'));
else msgPour += effetC;
}
let carac = attr.get('current');
if (!isCarac(carac)) {
error("Save par tour " + attrName + " mal formé", carac);
return;
}
let seuil = parseInt(attr.get('max'));
if (isNaN(seuil)) {
error("Save par tour " + attrName + " mal formé", seuil);
return;
}
let attrEffet = findObjs({
_type: 'attribute',
_characterid: perso.charId,
name: attrName
});
if (attrEffet === undefined || attrEffet.length === 0) {
error("Save sans effet temporaire " + attrName, attr);
findObjs({
_type: 'attribute',
_characterid: perso.charId,
name: attr.get('name').replace('SaveParTour', 'SaveParTourType')
}).forEach(function(a) {
a.remove();
});
attr.remove();
return;
}
attrEffet = attrEffet[0];
let playerId = getPlayerIdFromMsg(msg);
options.msgPour = msgPour;
let sujet = onGenre(perso, 'il', 'elle');
options.msgReussite = ", " + sujet + ' ' + messageFin(perso, met);
options.msgRate = ", " + sujet + ' ' + messageActif(perso, met);
let attrType = findObjs({
_type: 'attribute',
_characterid: perso.charId,
name: attr.get('name').replace('SaveParTour', 'SaveParTourType')
});
if (attrType.length > 0) {
options.type = attrType[0].get('current');
}
doSaveEffet(playerId, perso, effetC, attr, attrEffet, attrName, met, carac, seuil, options);
}
function doSaveEffet(playerId, perso, effetC, attr, attrEffet, attrName, met, carac, seuil, options) {
let evt = {
type: "save_effet",
action: {
titre: "Interface save effet",
perso,
effetC,
attr,
attrEffet,
attrName,
met,
carac,
options,
seuil,
playerId
}
};
addEvent(evt);
let titre = "Jet " + deCarac(carac) + options.msgPour;
let display = startFramedDisplay(playerId, titre, perso);
let explications = [];
let expliquer = function(msg) {
explications.push(msg);
};
let saveId = 'saveParTour_' + attrEffet.id + '_' + perso.token.id;
let s = {
carac: carac,
seuil: seuil,
entrave: met.entrave
};
save(s, perso, saveId, expliquer, options, evt,
function(reussite, texte) { //asynchrone
explications.forEach(function(e) {
addLineToFramedDisplay(display, e);
});
sendFramedDisplay(display);
if (reussite) {
let eff = effetC;
if (estEffetTemp(effetC)) eff = effetTempOfAttribute(attrEffet);
finDEffet(attrEffet, eff, attrName, perso.charId, evt, {
attrSave: attr,
pageId: perso.token.get('pageid')
});
}
});
}
function updateInit(token, evt) {
if (stateCOF.combat &&
token.get('pageid') == stateCOF.combat.pageId)
initiative([{
_id: token.id
}], evt, true);
}
function updateNextInit(perso) {
updateNextInitSet.add(perso.token.id);
}
function parseDegainer(msg) {
const options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("commande non formée", msg.content);
return;
}
let armeLabel = '';
if (cmd.length > 1) armeLabel = cmd[1];
if (cmd.length > 2) {
switch (cmd[2]) {
case 'faible':
case 'gauche':
options.gauche = true;
break;
case 'droite':
case 'dominant':
case 'dominante':
case 'principal':
case 'principale':
options.seulementDroite = true;
break;
case '2mains':
options.deuxMains = true;
break;
default:
options.armeGaucheLabel = cmd[2];
}
}
let personnages = [];
getSelected(msg, function(selected) {
if (selected === undefined || selected.length === 0) {
error("Qui doit dégainer ?", msg);
return;
}
iterSelected(selected, function(perso) {
personnages.push(perso);
});
}, options);
doDegainer(personnages, armeLabel, options);
}
function doDegainer(persos, labelArme, options) {
const evt = {
type: 'degainer',
attributes: [],
action: {
persos,
armeLabel: labelArme,
options
}
};
addEvent(evt);
if (options.son) playSound(options.son);
persos.forEach(function(perso) {
function afterSave() {
let nomArme = degainerArme(perso, labelArme, evt, options);
if (nomArme) sendPerso(perso, "a déjà " + nomArme + " en main");
else if (options.montreActions && persos.length === 1)
turnAction(perso);
}
if (options.save) {
let saveOpts = {
msgPour: " pour garder son arme en main",
msgRate: ", raté.",
rolls: options.rolls,
chanceRollId: options.chanceRollId
};
let expliquer = function(s) {
sendPerso(perso, s);
};
const saveId = 'garderArme_' + perso.token.id;
save(options.save, perso, saveId, expliquer, saveOpts, evt, function(reussite, rollText) {
if (!reussite) {
afterSave();
}
});
} else {
afterSave();
}
});
}
function echangeInit(msg) {
let combat = stateCOF.combat;
if (!combat) {
error("Échange d'intiative en dehors du combat", msg);
return;
}
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 3) {
error("Pas assez d'arguments pour !cof-echange-init", msg.content);
return;
}
let perso1 = persoOfId(cmd[1], cmd[1]);
if (perso1 === undefined) {
error("le premier argument n'est pas un token valide", cmd[1]);
return;
}
let pageId = perso1.token.get('pageid');
let perso2 = persoOfId(cmd[2], cmd[2], pageId);
if (perso2 === undefined) {
error("le second argument n'est pas un token valide", cmd[2]);
return;
}
let attackBonus = 0;
if (cmd.length > 3) {
attackBonus = parseInt(cmd[3]);
if (isNaN(attackBonus)) {
error("Le troisième argument n'est pas un nombre", cmd[3]);
return;
}
}
let evt = {
type: "echange_init"
};
let to = getTurnOrder(combat, evt);
let tourTok1 = to.pasAgi.findIndex(function(t) {
return (t.id == perso1.token.id);
});
let tourTok2 = to.pasAgi.findIndex(function(t) {
return (t.id == perso2.token.id);
});
if (tourTok1 < 0) {
sendPerso(perso1, "a déjà agit, pas moyen d'échanger son initiative");
return;
}
if (tourTok2 < 0) {
sendPerso(perso2, "a déjà agit, pas moyen d'échanger son initiative");
return;
}
let pr1 = to.pasAgi[tourTok1].pr;
let pr2 = to.pasAgi[tourTok2].pr;
if (pr1 == pr2) {
sendPerso(perso1, "a la même initiative que " + nomPerso(perso2));
return;
}
if (limiteRessources(perso1, options, "actionConcertee", "action concertée", evt)) return;
if (pr1 > pr2) {
if (attackBonus) {
setTokenAttr(perso1, 'actionConcertee', attackBonus, evt, {
msg: "gagne un bonus de " + attackBonus + " à ses attaques et en DEF pour ce tour"
});
}
setActiveToken(combat, perso2.token.id, evt);
} else {
setActiveToken(combat, perso1.token.id, evt);
}
to.pasAgi[tourTok1].pr = pr2;
to.pasAgi[tourTok2].pr = pr1;
let t1 = to.pasAgi[tourTok1];
to.pasAgi[tourTok1] = to.pasAgi[tourTok2];
to.pasAgi[tourTok2] = t1;
updateNextInit(perso1);
updateNextInit(perso2);
to.pasAgi.push(to.tour);
let turnOrder = to.pasAgi.concat(to.dejaAgi);
Campaign().set('turnorder', JSON.stringify(turnOrder));
addEvent(evt);
}
function aCouvert(msg) {
let args = msg.content.split(" ");
if (args.length < 2) {
error("Pas assez d'arguments pour !cof-a-couvert: " + msg.content, args);
return;
}
let perso1 = persoOfId(args[1], args[1]);
if (perso1 === undefined) {
error("Le premier argument n'est pas un token valide", args[1]);
return;
}
let evt = {
type: "aCouvert"
};
let init = getInit();
let secret = args.some(function(arg) {
return arg == '--secret';
});
setTokenAttr(perso1, 'aCouvert', 1, evt, {
msg: "reste à couvert",
maxVal: init,
secret: secret
});
if (args.length > 2) {
let perso2 = persoOfId(args[2], args[2]);
if (perso2 === undefined) {
error("Le second argument n'est pas un token valide", args[2]);
addEvent(evt);
return;
}
if (perso2.token.id == perso1.token.id) {
if (secret) {
whisperChar(perso1.charId, "s'est ciblé " + onGenre(perso1, 'lui', 'elle') + "-même, il est donc le seul à couvert");
} else {
sendPerso(perso1, "s'est ciblé " + onGenre(perso1, 'lui', 'elle') + "-même, il est donc le seul à couvert");
}
addEvent(evt);
return;
}
let d = distanceCombat(perso1.token, perso2.token);
if (d > 0) {
if (secret) {
whisperChar(perso2.charId, "est trop éloigné de " + nomPerso(perso1) + " pour rester à couvert avec lui");
} else {
sendPerso(perso2, "est trop éloigné de " + nomPerso(perso1) + " pour rester à couvert avec lui");
}
} else {
setTokenAttr(perso2, 'aCouvert', 1, evt, {
msg: "suit " + nomPerso(perso1) + " et reste à couvert",
maxVal: init,
secret: secret
});
}
}
addEvent(evt);
}
function getInit() {
let combat = stateCOF.combat;
if (!combat) return 1000;
return combat.init;
}
function parseEffetTemporaire(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 3) {
error("Pas assez d'arguments pour !cof-effet-temp", msg.content);
return;
}
let effet = cmd[1];
let lanceur = options.lanceur;
let charId;
if (lanceur) charId = lanceur.charId;
if (cof_states[effet]) { //remplacer par sa version effet temporaire
effet += 'Temp';
}
if (effet == 'forgeron' || effet == 'armeEnflammee' || effet == 'armeGlacee') {
//Compléter description de l'effet
if (!lanceur) {
error("Pas de lanceur pour " + effet, msg.content);
return;
}
let armeActuelle = armesEnMain(lanceur);
if (!armeActuelle) {
whisperChar(charId, "Pas d'arme en main, impossible de savoir à quoi appliquer " + effet);
return;
}
effet = effet + '(' + armeActuelle.label + ')';
} else if (!estEffetTemp(effet)) {
error(effet + " n'est pas un effet temporaire répertorié", msg.content);
return;
}
if (lanceur && options.mana !== undefined && attributeAsBool(lanceur, 'frappeDesArcanes')) {
sendPerso(lanceur, "ne peut pas encore lancer de sort");
return;
}
if (!options.type && options.valeurMax && effet.startsWith('dotGen(')) {
options.type = options.valeurMax;
}
let pp = effet.indexOf('(');
let mEffet = (pp > 0) ? messageEffetTemp[effet.substring(effet, pp)] : messageEffetTemp[effet];
if (mEffet === undefined) {
error("Impossible de trouver l'effet " + effet, cmd);
return;
}
let duree = parseInt(cmd[2]);
if (isNaN(duree) || duree < 1) duree = 0; //On veut terminer l'effet
if (options.puissantDuree || options.tempeteDeManaDuree) duree = duree * 2;
getSelected(msg, function(selected, playerId, aoe) {
if (selected === undefined || selected.length === 0) {
sendPlayer(msg, "Pas de cible sélectionée pour l'effet", playerId);
return;
}
options.aoe = aoe;
if (lanceur === undefined) {
if (options.portee) {
error("Impossible de savoir l'origine de l'effet", options);
return;
}
if (selected.length == 1) {
lanceur = persoOfId(selected[0]._id);
if (lanceur) {
options.lanceur = lanceur;
charId = lanceur.charId;
}
}
}
if (lanceur && options.tempeteDeMana) {
if (duree > 0 && options.tempeteDeMana.cout === 0) {
//On demande de préciser les options
let optMana = {
mana: options.mana,
dm: mEffet.dm,
soins: mEffet.soins,
portee: options.portee,
duree: true,
rang: options.rang,
altruiste: options.altruiste
};
setTempeteDeMana(playerId, lanceur, msg.content, optMana);
return;
} else {
if (options.rang && options.tempeteDeMana.cout > options.rang) {
sendPlayer(msg, "Attention, le coût de la tempête de mana (" + options.tempeteDeMana.cout + ") est supérieur au rang du sort");
}
if (selected.length == 1 && options.tempeteDeMana.altruiste) {
selected[0]._id = options.tempeteDeMana.altruiste.token.id;
if (options.portee === undefined) options.portee = 0;
}
}
}
let cibles = [];
iterSelected(selected, function(perso) {
if (options.portee !== undefined) {
if (options.puissantPortee || options.tempeteDeManaPortee) options.portee = options.portee * 2;
let dist = distanceCombat(lanceur.token, perso.token);
if (dist > options.portee) {
sendPerso(perso, " est trop loin de " + nomPerso(perso));
return;
}
}
if ((mEffet.seulementVivant || options.seulementVivant) &&
estNonVivant(perso)) {
sendPlayer(msg, "cet effet n'affecte que les créatures vivantes", playerId);
return;
}
if (predicateAsBool(perso, 'liberteDAction') &&
(effet == 'confusion ' ||
effet == 'charme ' ||
effet == 'prisonVegetale' ||
effet == 'toiles' ||
effet == 'foretVivanteEnnemie' ||
((options.magique || options.mana != undefined) &&
(effet == 'apeureTemp' ||
effet == 'endormiTemp' ||
effet == 'etourdiTemp' ||
effet == 'immobiliseTemp' ||
effet == 'paralyseTemp' ||
effet == 'paralyseGoule' ||
effet == 'ralentiTemp'))
)) {
sendPerso(perso, "reste libre de ses mouvements !");
return;
}
if ((options.magique || options.mana != undefined) &&
((predicateAsBool(perso, 'actionLibre') &&
(effet == 'immobiliseTemp' ||
effet == 'paralyseTemp' ||
effet == 'paralyseGoule' ||
effet == 'ralentiTemp' ||
effet == 'toiles')) ||
(mEffet.entrave && effet != 'paralyseTemp' && effet != 'paralyseGoule' && predicateAsInt(perso, 'voieDeLArchange', 1) > 1 && attributeAsBool(perso, 'formeDAnge'))
)) {
sendPerso(perso, "reste libre de ses mouvements !");
return;
}
cibles.push(perso);
});
if (cibles.length == 0) {
return;
}
effetTemporaire(playerId, cibles, effet, mEffet, duree, options);
}, options);
}
//Si options.terrainDifficile, doit avoir des champs imgsrc, nom et duree
function ajouteTerrainDifficile(options, evt) {
if (!options.aoe || !options.aoe.rayon) return;
let terrainDifficile = options.terrainDifficile;
if (!terrainDifficile || terrainDifficile.done) return;
terrainDifficile.done = true;
let diametre = options.aoe.rayon * 2 * PIX_PER_UNIT / computeScale(options.pageId);
let t = createObj('graphic', {
_pageid: options.pageId,
imgsrc: terrainDifficile.imgsrc,
represents: '',
left: options.aoe.centre.left,
top: options.aoe.centre.top,
width: diametre,
height: diametre,
layer: 'map',
isDrawing: true,
name: terrainDifficile.nom,
gmnotes: 'terrainDifficile:disque ' + Math.floor(diametre)
});
if (t) {
toFront(t);
evt.tokens = evt.tokens || [];
evt.tokens.push(t);
stateCOF.tokensTemps = stateCOF.tokensTemps || [];
stateCOF.tokensTemps.push({
tid: t.id,
duree: terrainDifficile.duree,
init: getInit()
});
}
}
//Si display est défini, l'envoie dans le chat à la fin de l'appel
function activerEffetTemporaire(lanceur, cibles, effet, mEffet, duree, options, evt, whisper, explications, display) {
let ef = {
effet: effet,
duree: duree,
acumuleDuree: options.accumuleDuree,
typeDmg: options.type,
message: mEffet,
valeur: options.valeur,
valeurMax: options.valeurMax,
saveParTour: options.saveParTour,
saveActifParTour: options.saveActifParTour,
whisper: whisper,
attaquant: options.lanceur,
options: options.optionsEffet,
tokenSide: options.tokenSide
};
if (display) ef.whisper = undefined;
let nbCibles = cibles.length;
let finalize = function() {
nbCibles--;
if (nbCibles === 0) { //affichage
if (display) {
cibles.forEach(function(cible) {
if (cible.messages.length > 0) {
if (cibles.length > 1 || (lanceur && cible.token.id != lanceur.token.id))
addLineToFramedDisplay(display, "" + nomPerso(cible) + " :");
cible.messages.forEach(function(expl) {
addLineToFramedDisplay(display, expl, 80);
});
}
});
explications.forEach(function(e) {
addLineToFramedDisplay(display, e);
});
sendFramedDisplay(display);
} else {
explications.forEach(function(e) {
sendChat('', e);
});
}
}
};
let setOneEffect = function(perso, d) {
let expliquer = function(m) {
if (display) {
perso.messages.push(m);
} else sendPerso(perso, m, options.secret);
};
if (options.limiteCibleParJour) {
let ressource = effet;
if (options.limiteCibleParJourRessource)
ressource = options.limiteCibleParJourRessource;
ressource = "limiteParJour_" + ressource;
let utilisations = attributeAsInt(perso, ressource, options.limiteCibleParJour);
if (utilisations === 0) {
expliquer("ne peut plus bénéficier de " + effet + " aujourd'hui");
return;
}
setTokenAttr(perso, ressource, utilisations - 1, evt);
}
let combat;
if (mEffet.dm || mEffet.prejudiciable) {
combat = entrerEnCombat(lanceur, cibles, explications, evt);
} else { //On met juste dans la liste d'initiative
let ini = [...cibles];
if (lanceur) ini.push(lanceur);
combat = entrerEnCombat(undefined, ini, explications, evt);
}
if (options.aoe && options.pageId && effet == 'prisonVegetale' && options.aoe.type == 'disque') {
options.terrainDifficile = options.terrainDifficile || {
duree,
nom: "Prison végétale",
imgsrc: stateCOF.options.images.val.prison_vegetale.val
};
ajouteTerrainDifficile(options, evt);
}
let renew = attributeAsBool(perso, effet);
setEffetTemporaire(perso, ef, d, evt, options);
if (!renew) {
if (effet.startsWith('forgeron(')) {
//Il faut dégainer l'arme si elle n'est pas en main, et ajouter une lumière
let labelArmeForgeron = effet.substring(9, effet.indexOf(')'));
degainerArme(perso, labelArmeForgeron, evt);
let feu = getIntValeurOfEffet(perso, effet, 1, 'voieDuMetal');
ajouteUneLumiere(perso, effet, feu * 3, feu, evt);
} else if (effet.startsWith('armeEnflammee(')) {
let labelArmeEnflammee = effet.substring(14, effet.indexOf(')'));
degainerArme(perso, labelArmeEnflammee, evt);
ajouteUneLumiere(perso, effet, 9, 3, evt);
}
}
if (effet == 'cercleDeProtection') {
let protecteur = options.lanceur || perso;
if (!attributeAsBool(protecteur, 'cercleDeProtectionActif')) {
setTokenAttr(protecteur, 'cercleDeProtectionActif', 1, evt, {
maxVal: 1
});
}
}
if (effet == 'armeeDesMorts') {
if (!evt.combat) evt.combat = {...combat
};
if (combat.armeesDesMorts) {
evt.combat.armeesDesMorts = {...combat.armeesDesMorts
};
} else {
evt.combat.armeesDesMorts = {};
combat.armeesDesMorts = {};
}
combat.armeesDesMorts[perso.token.id] = perso;
}
if (options.puissant) {
let puissant = (options.puissant != 'off');
setTokenAttr(perso, effet + 'Puissant', puissant, evt);
}
effetsSpeciaux(lanceur, perso, options);
finalize();
};
cibles.forEach(function(perso) {
if (display) perso.messages = perso.messages || [];
let expliquer = function(s) {
if (display) perso.messages.push(s);
else sendPerso(perso, s);
};
if (options.type && immuniseAuType(perso, options.type, lanceur, options)) {
expliquer("ne semble pas affecté par " + stringOfType(options.type));
finalize();
return;
}
if (options.save) {
let saveOpts = {
msgPour: options.save.msgPour || " pour résister à l'effet " + effet,
msgRate: ", raté.",
attaquant: lanceur,
rolls: options.rolls,
chanceRollId: options.chanceRollId,
type: options.type
};
options.save.entrave = mEffet.entrave;
let d = duree;
let saveId = 'effet_' + effet + "_" + perso.token.id;
save(options.save, perso, saveId, expliquer, saveOpts, evt,
function(reussite, rollText) {
if (reussite && options.save.demiDuree) {
reussite = false;
d = Math.ceil(d / 2);
}
if (reussite) {
if (effet == 'reactionViolente' && predicateAsBool(perso, 'violenceCiblee')) {
expliquer("résiste à la provocation, mais emmagasine de la violence");
addToAttributeAsInt(perso, 'pointsDeViolence', 0, 1, evt);
}
finalize();
} else setOneEffect(perso, d);
});
} else {
setOneEffect(perso, duree);
}
});
}
function porteArmure(perso) {
if (!ficheAttributeAsBool(perso, 'defarmureon', false) &&
!ficheAttributeAsBool(perso, 'defbouclieron', false)) return false;
let magieEnArmure = predicateAsInt(perso, 'magieEnArmure', 0);
if (magieEnArmure === 0) return true;
let ma = malusArmure(perso);
let defa = defenseArmure(perso);
return (2 * magieEnArmure < defa + ma);
}
//options doit être défini
function effetTemporaire(playerId, cibles, effet, mEffet, duree, options) {
const evt = {
type: 'effetTemp',
action: {
titre: "Effet Temporaire",
playerId: playerId,
cibles: cibles,
effet: effet,
mEffet: mEffet,
duree: duree,
options: options
}
};
let lanceur = options.lanceur;
let explications = options.messages || [];
if (options.magieRapide) explications.push("Magie rapide");
let whisper = '';
if (options.secret && playerId) {
let player = getObj('player', playerId);
if (player !== undefined) {
whisper = '/w "' + player.get('displayname') + '" ';
}
}
addEvent(evt);
if (limiteRessources(lanceur, options, effet, effet, evt)) return;
if (duree > 0) {
if (options.magieEnArmure && lanceur && porteArmure(lanceur)) {
let difficulte = 10;
if (options.magieEnArmure.base) difficulte = options.magieEnArmure.base;
else if (options.rang) difficulte = 10 + options.rang;
else difficulte = 11;
difficulte += malusArmure(lanceur);
if (predicateAsBool(lanceur, 'magieEnArmureFacilitee')) {
options.bonusPreds = options.bonusPreds || [];
options.bonusPreds.push('magieEnArmure');
}
let display = startFramedDisplay(playerId, "Sort en armure", lanceur, options);
let testId = 'magieEnArmure_' + lanceur.token.id;
testCaracteristique(lanceur, 'INT', difficulte, testId, options, evt,
function(tr) {
let line = "Jet d'INT : " + tr.texte;
if (tr.reussite) {
line += '≥ ' + difficulte;
addLineToFramedDisplay(display, line);
activerEffetTemporaire(lanceur, cibles, effet, mEffet, duree, options, evt, whisper, explications, display);
} else {
line += '< ' + difficulte + ", le sort est raté";
addLineToFramedDisplay(display, line);
sendFramedDisplay(display);
}
});
} else {
activerEffetTemporaire(lanceur, cibles, effet, mEffet, duree, options, evt, whisper, explications);
}
} else { //On met fin à l'effet
explications.forEach(function(e) {
sendChat('', e);
});
let opt = {
pageId: options.pageId
};
cibles.forEach(function(perso) {
let attr = tokenAttribute(perso, effet);
if (attr.length === 0) {
log(nomPerso(perso) + " n'a pas d'attribut " + effet);
return;
}
finDEffet(attr[0], effetTempOfAttribute(attr[0]), attr[0].get('name'), perso.charId, evt, opt);
});
}
if (options.montreActions && cibles.length === 1)
turnAction(cibles[0], playerId);
}
function afficheOptionImage(options) {
let img = options.image;
let extraImg = '';
if (img && (img.toLowerCase().endsWith(".jpg") || img.toLowerCase().endsWith(".png") || img.toLowerCase().endsWith(".gif"))) {
extraImg = ' ';
extraImg += '';
extraImg += '';
}
return extraImg;
}
function activeOuValeur(cmd, options) {
let activer = true;
let valeur = true;
if (cmd.length > 2) {
switch (cmd[2]) {
case 'oui':
case 'Oui':
case 'true':
case 'début':
case 'debut':
valeur = true;
activer = true;
break;
case 'non':
case 'Non':
case 'false':
case 'fin':
valeur = false;
activer = false;
break;
default:
valeur = parseInt(cmd[2]);
if (isNaN(valeur)) {
error("Option de !cof-effet inconnue", cmd);
return;
}
activer = valeur !== 0;
if (cmd[2].startsWith('+') || valeur < 0)
options.valeurAjoutee = valeur;
}
}
return {
activer,
valeur
};
}
function effetCombat(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 2) {
error("Pas assez d'arguments pour !cof-effet-combat", msg.content);
return;
}
let effet = cmd[1];
if (!estEffetCombat(effet)) {
error(effet + " n'est pas un effet de combat répertorié", msg.content);
return;
}
const evt = {
type: 'Effet ' + effet
};
let av = activeOuValeur(cmd, options);
if (!av) return;
let {
activer,
valeur
} = av;
if (!options.valeur && !isNaN(valeur)) options.valeur = valeur;
let lanceur = options.lanceur;
let charId;
if (lanceur) charId = lanceur.charId;
getSelected(msg, function(selected, playerId) {
let whisper = '';
if (options.secret) {
let player;
if (playerId) player = getObj('player', playerId);
if (player !== undefined) {
whisper = '/w "' + player.get('displayname') + '" ';
}
}
if (selected === undefined || selected.length === 0) {
sendPlayer(msg, "Pas de cible sélectionée pour l'effet", playerId);
return;
}
if (lanceur === undefined) {
if (options.portee) {
error("Impossible de savoir l'origine de l'effet", options);
return;
}
if (selected.length == 1) {
lanceur = persoOfId(selected[0]._id);
if (lanceur) charId = lanceur.charId;
}
}
if (lanceur && options.tempeteDeMana) {
if (options.tempeteDeMana.cout === 0) {
//On demande de préciser les options
let optMana = {
mana: options.mana,
dm: messageEffetCombat[effet].dm,
soins: messageEffetCombat[effet].soins,
portee: options.portee,
altruiste: options.altruiste,
rang: options.rang
};
setTempeteDeMana(playerId, lanceur, msg.content, optMana);
return;
} else {
if (options.rang && options.tempeteDeMana.cout > options.rang) {
sendPerso(lanceur, "Attention, le coût de la tempête de mana (" + options.tempeteDeMana.cout + ") est supérieur au rang du sort", options.secret);
}
if (selected.length == 1 && options.tempeteDeMana.altruiste) {
selected[0]._id = options.tempeteDeMana.altruiste.token.id;
if (options.portee === undefined) options.portee = 0;
}
}
}
if (options.portee !== undefined) {
if (options.puissantPortee || options.tempeteDeManaPortee) options.portee = options.portee * 2;
selected = selected.filter(function(sel) {
let token = getObj('graphic', sel._id);
let dist = distanceCombat(lanceur.token, token);
if (dist > options.portee) {
whisperChar(charId, " est trop loin de " + token.get('name'));
return false;
}
return true;
});
}
if (selected.length === 0) return;
if (limiteRessources(lanceur, options, effet, effet, evt)) {
addEvent(evt);
return;
}
let mEffet = messageEffetCombat[effet];
let extraImg = afficheOptionImage(options);
if (activer) {
initiative(selected, evt);
iterSelected(selected, function(perso) {
if (effet == 'blessureQuiSaigne' && predicateAsBool(perso, 'immuniteSaignement')) {
sendPerso(perso, "ne saigne pas");
return;
}
let actMsg = messageActivation(perso, mEffet) + extraImg;
let effetAttr = setTokenAttr(perso, effet, true, evt, {
msg: whisper + actMsg
});
if (options.lanceur && options.mana !== undefined && mEffet.prejudiciable) {
addEffetTemporaireLie(options.lanceur, effetAttr, evt);
}
if (options.puissant) {
let puissant = (options.puissant != 'off');
setTokenAttr(perso, effet + 'Puissant', puissant, evt);
}
if (options.valeur !== undefined) {
setTokenAttr(perso, effet + 'Valeur', options.valeur, evt, {
maxVal: options.valeurMax
});
}
if (options.optionsEffet !== undefined) {
setTokenAttr(perso, effet + 'Options', options.optionsEffet, evt);
}
if (options.saveParTour) {
setTokenAttr(perso, effet + 'SaveParTour', options.saveParTour.carac, evt, {
maxVal: options.saveParTour.seuil
});
}
if (options.saveActifParTour) {
setTokenAttr(perso, effet + 'SaveActifParTour', options.saveActifParTour.carac, evt, {
maxVal: options.saveActifParTour.seuil
});
}
if (options.tempeteDeManaIntense !== undefined) {
setTokenAttr(perso, effet + 'TempeteDeManaIntense', options.tempeteDeManaIntense, evt);
}
if (options.tokenSide !== undefined) {
let oldSide = changeTokenSide(perso, options.tokenSide, evt);
if (oldSide !== undefined)
setTokenAttr(perso, effet + 'TokenSide', oldSide, evt);
}
});
} else { //on désactive
iterSelected(selected, function(perso) {
let actMsg = messageFin(perso, mEffet) + extraImg;
removeTokenAttr(perso, effet, evt, {
msg: whisper + actMsg
});
removeTokenAttr(perso, effet + 'Puissant', evt);
removeTokenAttr(perso, effet + 'Valeur', evt);
removeTokenAttr(perso, effet + 'Options', evt);
removeTokenAttr(perso, effet + 'SaveParTour', evt);
removeTokenAttr(perso, effet + 'SaveActifParTour', evt);
removeTokenAttr(perso, effet + 'TempeteDeManaIntense', evt);
//On remet la face du token
let attrTS = tokenAttribute(perso, effet + 'TokenSide');
if (attrTS.length > 0) {
attrTS = attrTS[0];
let side = attrTS.get('current');
changeTokenSide(perso, side, evt);
evt.deletedAttributes.push(attrTS);
attrTS.remove();
}
});
}
addEvent(evt);
iterSelected(selected, function(target) {
effetsSpeciaux(lanceur, target, options);
});
if (options.degainer !== undefined) {
if (lanceur) {
degainerArme(lanceur, options.degainer, evt);
} else if (selected.length === 1) {
iterSelected(selected, function(target) {
degainerArme(target, options.degainer, evt);
});
}
}
if (options.montreActions) {
if (lanceur) {
turnAction(lanceur, playerId);
} else if (selected.length === 1) {
iterSelected(selected, function(target) {
turnAction(target, playerId);
});
}
}
}, options);
}
function parseEffetIndetermine(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 2) {
error("Pas assez d'arguments pour !cof-effet", msg.content);
return;
}
let effet = cmd[1];
if (!estEffetIndetermine(effet)) {
if (estEffetTemp(effet)) {
parseEffetTemporaire(msg);
return;
}
if (estEffetCombat(effet)) {
effetCombat(msg);
return;
}
error(effet + " n'est pas un effet répertorié", msg.content);
return;
}
let av = activeOuValeur(cmd, options);
if (!av) return;
let {
activer,
valeur
} = av;
let lanceur = options.lanceur;
let charId;
if (lanceur) charId = lanceur.charId;
getSelected(msg, function(selected, playerId, aoe) {
if (selected === undefined || selected.length === 0) {
sendPlayer(msg, "Pas de cible sélectionée pour l'effet", playerId);
return;
}
if (lanceur === undefined) {
if (options.portee) {
error("Impossible de savoir l'origine de l'effet", options);
return;
}
if (selected.length == 1) {
lanceur = persoOfId(selected[0]._id);
if (lanceur) {
options.lanceur = lanceur;
charId = lanceur.charId;
}
}
}
if (lanceur && options.tempeteDeMana) {
if (options.rang && options.tempeteDeMana.cout > options.rang) {
sendPlayer(msg, "Attention, le coût de la tempête de mana (" + options.tempeteDeMana.cout + ") est supérieur au rang du sort");
}
if (selected.length == 1 && options.tempeteDeMana.altruiste) {
selected[0]._id = options.tempeteDeMana.altruiste.token.id;
if (options.portee === undefined) options.portee = 0;
}
}
let cibles = [];
iterSelected(selected, function(perso) {
if (options.portee !== undefined) {
if (options.puissantPortee || options.tempeteDeManaPortee) options.portee = options.portee * 2;
let dist = distanceCombat(lanceur.token, perso.token);
if (dist > options.portee) {
sendPerso(perso, " est trop loin de " + nomPerso(perso));
return;
}
}
if (activer && valeur === true) {
if (attributeAsBool(perso, effet)) return;
} else if (!activer && valeur === false) {
if (!attributeAsBool(perso, effet)) return;
}
cibles.push(perso);
});
if (cibles.length == 0) {
return;
}
if (activer === undefined) {
if (cibles.length > 1) {
sendPlayer(msg, "On ne peut sélectionner qu'un token si on ne précise pas si il faut activer ou désactiver l'effet");
return;
}
let perso = persoOfId(selected[0]._id);
if (!perso) {
error("Token sélectionné non valide", selected);
return;
}
activer = !attributeAsBool(perso, effet);
}
effetIndetermine(playerId, cibles, effet, activer, valeur, options);
}, options);
}
//L'effet de grandeTaille sur le token
function grandeTaille(perso, evt) {
if (attributeAsBool(perso, 'tailleDeTokenNormale')) return;
let character = getObj('character', perso.charId);
if (character === undefined) {
error("Personnage introuvable", perso);
return;
}
perso.taille = undefined;
let token = perso.token;
character.get('_defaulttoken', function(normalToken) {
if (normalToken === '') return;
normalToken = JSON.parse(normalToken);
let nw = normalToken.width;
let nh = normalToken.height;
//Rien à faire si le token occupe déjà une case
if (nw >= PIX_PER_UNIT || nh >= PIX_PER_UNIT) return;
setTokenAttr(perso, 'tailleDeTokenNormale', nw, evt, {
maxVal: nh
});
evt.defaultTokens = evt.defaultTokens || [];
evt.defaultTokens.push({
character: character,
defaultToken: {...normalToken
}
});
//Les facteurs d'agrandissement
let fw = PIX_PER_UNIT / nw;
let fh = PIX_PER_UNIT / nh;
let f = Math.min(fw, fh);
normalToken.width = f * nw;
normalToken.height = f * nh;
let width = token.get('width');
let height = token.get('height');
affectToken(token, 'width', width, evt);
affectToken(token, 'height', height, evt);
setDefaultTokenFromSpec(character, normalToken, token);
token.set('width', f * width);
token.set('height', f * height);
});
}
function finDeGrandeTaille(perso, evt) {
let attr = tokenAttribute(perso, 'tailleDeTokenNormale');
if (attr.length === 0) return;
attr = attr[0];
let nw = parseInt(attr.get('current'));
let nh = parseInt(attr.get('max'));
if (isNaN(nw) || isNaN(nh)) {
error("La taille de token sauvegardée n'est pas correcte", attr);
attr.remove();
}
let character = getObj('character', perso.charId);
if (character === undefined) {
error("Personnage introuvable", perso);
return;
}
let token = perso.token;
character.get('_defaulttoken', function(currentToken) {
if (currentToken === '') return;
currentToken = JSON.parse(currentToken);
evt.deletedAttributes = evt.deletedAttributes || [];
evt.deletedAttributes.push(attr);
attr.remove();
evt.defaultTokens = evt.defaultTokens || [];
evt.defaultTokens.push({
character: character,
defaultToken: {...currentToken
}
});
let width = token.get('width');
let height = token.get('height');
affectToken(token, 'width', width, evt);
affectToken(token, 'height', height, evt);
let ntw = nw;
let nth = nh;
if (width != currentToken.width)
ntw = nw * width / currentToken.width;
if (height != currentToken.height)
nth = nh * height / currentToken.height;
currentToken.width = nw;
currentToken.height = nh;
setDefaultTokenFromSpec(character, currentToken, token);
token.set('width', ntw);
token.set('height', nth);
});
}
function effetIndetermine(playerId, cibles, effet, activer, valeur, options) {
const evt = {
type: 'Effet',
action: {
titre: "Effet " + effet,
playerId,
cibles,
effet,
activer,
valeur,
options
}
};
addEvent(evt);
let lanceur = options.lanceur;
let whisper = '';
if (options.secret && playerId) {
let player = getObj('player', playerId);
if (player !== undefined) {
whisper = '/w "' + player.get('displayname') + '" ';
}
}
let mEffet = messageEffetIndetermine[effet];
if (activer) {
if (limiteRessources(lanceur, options, effet, effet, evt)) {
return;
}
if (options.classeEffet) {
cibles = cibles.filter(function(perso) {
if (perso === undefined) return false;
if (attributeAsBool(perso, options.classeEffet)) {
let attrDeClasse = attributesOfClass(perso, options.classeEffet);
let mpc = "Non cumulable avec";
attrDeClasse.forEach(function(attrClasseEffet) {
let attr = attrClasseEffet.baseAttribute;
let attrName = attr.get('name');
if (estEffetIndetermine(attrName))
mpc += ' ' + messageActif(perso, messageEffetIndetermine[effetIndetermineOfAttribute(attr)]);
else mpc += ' ' + attrName;
});
sendPerso(perso, mpc, options.secret);
return false;
}
setTokenAttr(perso, options.classeEffet, true, evt);
setTokenAttr(perso, effet + 'ClasseEffet', options.classeEffet, evt);
return true;
});
}
cibles.forEach(function(perso) {
let expliquer = function(s) {
sendPerso(perso, s);
};
let doit = function() {
if (options.valeurAjoutee) {
addToAttributeAsInt(perso, effet, 0, options.valeurAjoutee, evt);
expliquer(effet + " varie de " + options.valeurAjoutee, options.secret);
} else {
setTokenAttr(
perso, effet, valeur, evt, {
msg: whisper + messageActivation(perso, mEffet)
});
switch (effet) {
case 'foretVivanteEnnemie':
if (stateCOF.combat) updateNextInit(perso);
break;
case 'sangDeLArbreCoeur':
guerisonPerso(perso, evt);
break;
case 'grandeTaille':
grandeTaille(perso, evt);
break;
}
}
if (options.puissant) {
let puissant = (options.puissant != 'off');
setTokenAttr(perso, effet + 'Puissant', puissant, evt);
}
if (options.valeur !== undefined) {
setTokenAttr(perso, effet + 'Valeur', options.valeur, evt, {
maxVal: options.valeurMax
});
}
if (options.tempeteDeManaIntense !== undefined) {
setTokenAttr(perso, effet + 'TempeteDeManaIntense', options.tempeteDeManaIntense, evt);
}
if (options.tokenSide !== undefined) {
let oldSide = changeTokenSide(perso, options.tokenSide, evt);
if (oldSide !== undefined)
setTokenAttr(perso, effet + 'TokenSide', oldSide, evt);
}
if (options.messages) {
options.messages.forEach(function(m) {
sendChat('', whisper + m);
});
}
if (options.messagesMJ) {
options.messagesMJ.forEach(function(m) {
sendChar(perso.charId, '/w gm ' + m);
});
}
};
if (options.save) {
let msgPour = options.save.msgPour;
if (!msgPour) {
msgPour = " pour ";
if (mEffet.msgSave) msgPour += mEffet.msgSave;
else msgPour += "résister à l'effet " + effet;
}
let saveOpts = {
msgPour,
msgRate: ", raté.",
silencieuxSiPasAffecte: options.silencieuxSiPasAffecte,
attaquant: lanceur,
rolls: options.rolls,
chanceRollId: options.chanceRollId,
type: options.type,
regard: options.regard,
};
let saveId = 'effet_' + effet + "_" + perso.token.id;
save(options.save, perso, saveId, expliquer, saveOpts, evt,
function(reussite, rollText) {
if (!reussite) doit();
});
} else {
doit();
}
});
} else {
cibles.forEach(function(perso) {
//On commence par enlever les attributs de classe d'effet, si besoin
let ace = tokenAttribute(perso, effet + 'ClasseEffet');
if (ace.length > 0) {
let ce = ace[0].get('current');
removeTokenAttr(perso, ce, evt);
evt.deletedAttributes = evt.deletedAttributes || [];
evt.deletedAttributes.push(ace[0]);
ace[0].remove();
}
removeTokenAttr(perso, effet, evt, {
msg: messageFin(perso, mEffet)
});
removeTokenAttr(perso, effet + 'Puissant', evt);
removeTokenAttr(perso, effet + 'Valeur', evt);
removeTokenAttr(perso, effet + 'TempeteDeManaIntense', evt);
removeTokenAttr(perso, effet + 'Options', evt);
//On remet la face du token
let attrTS = tokenAttribute(perso, effet + 'TokenSide');
if (attrTS.length > 0) {
attrTS = attrTS[0];
let side = attrTS.get('current');
changeTokenSide(perso, side, evt);
evt.deletedAttributes.push(attrTS);
attrTS.remove();
}
switch (effet) {
case 'foretVivanteEnnemie':
if (stateCOF.combat) updateNextInit(perso);
break;
case 'grandeTaille':
finDeGrandeTaille(perso, evt);
break;
case 'petrifie':
unlockToken(perso, evt);
break;
}
if (options.messages) {
options.messages.forEach(function(m) {
sendChat('', whisper + m);
});
}
if (options.messagesMJ) {
options.messagesMJ.forEach(function(m) {
sendChar(perso.charId, '/w gm ' + m);
});
}
});
}
}
function finClasseDEffet(msg) {
let cmd = msg.content.split(' ');
if (cmd.length < 2) {
error("Il manque l'argument de !cof-fin-classe-effet", cmd);
return;
}
let classeEffet = cmd[1];
getSelected(msg, function(selected) {
if (selected === undefined || selected.length === 0) {
error("Pas de cible sélectionnée pour la fin d'une classe d'effets", msg);
return;
}
const evt = {
type: "Fin des effets de classe " + classeEffet,
deletedAttributes: []
};
iterSelected(selected, function(perso) {
if (attributeAsBool(perso, classeEffet)) {
let attrDeClasse = attributesOfClass(perso, classeEffet);
attrDeClasse.forEach(function(adc) {
let attrName = adc.baseAttribute.get('name');
if (estEffetIndetermine(attrName)) {
let mf = messageFin(perso, messageEffetIndetermine[effetIndetermineOfAttribute(adc.baseAttribute)]);
if (mf && mf !== '')
sendPerso(perso,
messageFin(perso, messageEffetIndetermine[effetIndetermineOfAttribute(adc.baseAttribute)]));
}
evt.deletedAttributes.push(adc.baseAttribute);
adc.baseAttribute.remove();
evt.deletedAttributes.push(adc.classAttribute);
adc.classAttribute.remove();
});
removeTokenAttr(perso, classeEffet, evt);
}
});
addEvent(evt);
}); //fin de getSelected
}
function peurOneToken(target, difficulte, duree, options, messages, evt, callback) {
const targetName = nomPerso(target);
if (predicateAsBool(target, 'sansPeur') ||
predicateAsBool(target, 'immunite_peur') ||
predicateAsBool(target, 'proprioception') ||
predicateAsBool(target, 'sansEsprit') ||
attributeAsBool(target, 'enrage') ||
predicateAsBool(target, 'liberteDAction')) {
messages.push(targetName + " est insensible à la peur !");
callback();
return;
}
if (options.immuniseSiResiste && attributeAsBool(target, options.immuniseSiResiste)) {
messages.push(targetName + " a déjà résisté à cet effet, " + onGenre(target, 'il', 'elle') + " n'y est plus sensible");
callback();
return;
}
let carac = 'SAG'; //carac pour résister
if (options.resisteAvecForce)
carac = meilleureCarac('SAG', 'FOR', target, difficulte);
//chercher si un partenaire a sansPeur pour appliquer le bonus
let allieSansPeur = 0;
let msgAllieSansPeur;
let allies = alliesParPerso[target.charId];
if (allies) {
const pageId = options.pageId || target.token.get('pageid');
let allTokens;
allies.forEach(function(cid) {
if (charPredicateAsBool(cid, 'sansPeur')) {
//On cherche si l'allié est présent sur la même page
allTokens = allTokens ||
findObjs({
_type: "graphic",
_pageid: pageId,
_subtype: "token",
layer: "objects"
});
let alliePresent = allTokens.find(function(tok) {
return tok.get('represents') == cid;
});
if (alliePresent) {
let bonusAllie = 2 + modCarac(cid, 'charisme');
if (bonusAllie > allieSansPeur) {
allieSansPeur = bonusAllie;
let allie = {
charId: cid,
token: alliePresent
};
msgAllieSansPeur = nomPerso(allie) + " a donné +" + bonusAllie + " au jet";
}
}
}
});
}
let optionsPeur = {...options
};
optionsPeur.bonus = allieSansPeur;
optionsPeur.bonus += predicateAsInt(target, 'courage', 0);
let testId = 'peurOne_' + target.token.id;
testCaracteristique(target, carac, difficulte, testId, optionsPeur, evt,
function(tr, explications) {
let line = targetName + " fait " + tr.texte;
let sujet = onGenre(target, 'il', 'elle');
if (tr.reussite) {
line += ", " + sujet + " résiste à la peur." + tr.modifiers;
if (options.immuniseSiResiste)
setTokenAttr(target, options.immuniseSiResiste, true, evt);
} else {
line += ", " + sujet + ' ';
let effet = 'apeureTemp';
let etat = 'apeure';
if (options.etourdi) {
line += "s'enfuit ou reste recroquevillé" + eForFemale(target) + " sur place";
effet = 'peurEtourdi';
} else if (options.ralenti) {
line += "est ralenti" + eForFemale(target);
effet = 'ralentiTemp';
etat = 'ralenti';
} else if (options.paralyse) {
line += "est paralysé" + eForFemale(target);
effet = 'paralyseTemp';
etat = 'paralyse';
} else if (options.secoue && !attributeAsBool(target, 'secoue')) {
line += "est secoué" + eForFemale(target);
effet = 'secoue';
etat = false;
} else {
line += "s'enfuit.";
}
line += tr.rerolls + tr.modifiers;
if (etat) setState(target, etat, true, evt);
setAttrDuree(target, effet, duree, evt);
}
messages.push(line);
if (allieSansPeur) messages.push(msgAllieSansPeur);
explications.forEach(function(p) {
messages.push(p);
});
callback();
}); //fin testCaracteristique (asynchrone)
}
function parsePeur(msg) {
let optArgs = msg.content.split(' --');
let cmd = optArgs[0].split(' ');
if (cmd.length < 3) {
error("Pas assez d'arguments pour !cof-peur", msg.content);
return;
}
let playerId = getPlayerIdFromMsg(msg);
let pageId = getPageId(playerId);
let difficulte = parseInt(cmd[1]);
if (isNaN(difficulte)) {
error("Le premier argument de !cof-peur, la difficulté du test de résistance, n'est pas un nombre", cmd);
return;
}
let duree = parseDice(cmd[2], "durée de peur");
if (!dePositif(duree)) {
error("Le second argument de !cof-peur, la durée, n'est pas un nombre positif", cmd);
return;
}
let options = {};
options.playerId = playerId;
options.pageId = pageId;
optArgs.shift();
optArgs.forEach(function(opt) {
let optCmd = opt.split(' ');
switch (optCmd[0]) {
case 'resisteAvecForce':
case 'etourdi':
case 'ralenti':
case 'effroi':
case 'secoue':
case 'paralyse':
options[optCmd[0]] = true;
return;
case 'portee':
if (optCmd.length < 2) {
error("Il manque l'argument de portée", optArgs);
return;
}
options.portee = parseInt(optCmd[1]);
if (isNaN(options.portee) || options.portee < 0) {
error("La portée n'est pas un nombre positif", optCmd);
delete options.portee;
}
return;
case 'lanceur':
if (optCmd.length < 2) {
error("Il manque l'argument de lanceur", optArgs);
return;
}
options.lanceur = persoOfId(optCmd[1], optCmd[1]);
if (options.lanceur) pageId = options.lanceur.token.get('pageid');
return;
case 'titre':
if (optCmd.length < 2) {
error("Il manque le titre de l'action", optArgs);
return;
}
options.titre = optCmd.slice(1).join(' ');
return;
case 'immuniseSiResiste':
if (optCmd.length < 2) {
error("Il manque l'argument de l'effet de peur", optArgs);
return;
}
options.immuniseSiResiste = 'immunise24HA_' + optCmd[1];
return;
default:
return;
}
});
let cibles = [];
getSelected(msg, function(selected) {
if (selected === undefined || selected.length === 0) {
error("Pas de cible sélectionnée pour la peur", msg);
return;
}
iterSelected(selected, function(perso) {
if (options.portee !== undefined && options.lanceur) {
let distance = distanceCombat(options.lanceur.token, perso.token, pageId);
if (distance > options.portee) {
return;
}
}
perso.duree = rollDePlus(duree).val;
cibles.push(perso);
});
}, options);
if (cibles.length > 0) {
doPeur(cibles, difficulte, options);
} else {
error("Aucune cible valable à portée de l'effet de Peur", msg);
}
}
function doPeur(cibles, difficulte, options) {
let evt = {
type: 'peur',
action: {
cibles: cibles,
difficulte: difficulte,
options: options
}
};
addEvent(evt);
let action = "Effet de peur";
if (options.lanceur) {
action = "" + nomPerso(options.lanceur) + " ";
if (options.titre) action += options.titre;
else if (options.effroi)
action += "est vraiment effrayant" + eForFemale(options.lanceur);
else action = "Capacité : Effet de peur";
} else if (options.titre) action = options.titre;
let messages = [];
entrerEnCombat(options.lanceur, cibles, messages, evt);
let display = startFramedDisplay(options.playerId, action, options.lanceur);
let jet = " Jet de SAG ";
if (options.resisteAvecForce) jet += "ou FOR ";
jet += "difficulté " + difficulte;
addLineToFramedDisplay(display, jet, 80);
let counter = cibles.length;
let finalDisplay = function() {
counter--;
if (messages.length > 0) {
let m = messages.shift();
addLineToFramedDisplay(display, m);
while (messages.length > 0) {
m = messages.shift();
addLineToFramedDisplay(display, m, 80, false);
}
}
if (counter < 1) {
sendFramedDisplay(display);
}
};
cibles.forEach(function(perso) {
peurOneToken(perso, difficulte, perso.duree, options, messages, evt, finalDisplay);
});
}
function parseAttaqueMagique(msg, type) {
let options = parseOptions(msg);
if (options === undefined || options.cmd === undefined) return;
let cmd = options.cmd;
if (cmd.length < 3) {
error("Il faut au moins 2 arguments à !cof-attaque-magique", cmd);
return;
}
let attaquant = persoOfId(cmd[1], cmd[1]);
let cible = persoOfId(cmd[2], cmd[2]);
if (attaquant === undefined || cible === undefined) {
error("Arguments de !cof-attaque-magique", cmd);
return;
}
if (options.portee) {
let distance = distanceCombat(attaquant.token, cible.token, options.pageId);
if (distance > options.portee) {
sendPerso(attaquant, "est trop loin de " + nomPerso(cible) +
" pour l'attaque magique");
return;
}
}
type = type || '';
switch (type) {
case 'tueurFantasmagorique':
tueurFantasmagorique(getPlayerIdFromMsg(msg), attaquant, cible, options);
break;
case 'enkystementLointain':
enkystementLointain(getPlayerIdFromMsg(msg), attaquant, cible, options);
break;
case 'injonction':
injonction(getPlayerIdFromMsg(msg), attaquant, cible, options);
break;
default:
attaqueMagiqueOpposee(getPlayerIdFromMsg(msg), attaquant, cible, options);
break;
}
}
// callback est seulement appelé si on fait le test
// evt est facultatif ; si absent, en crée un nouveau générique et l'ajoute à l'historique
function attaqueMagiqueOpposee(playerId, attaquant, cible, options, callback, evt) {
if (options.attaqueMentale) {
if (predicateAsBool(cible, 'sansEsprit') || predicateAsBool(cible, 'vegetatif')) {
sendPerso(attaquant, " est sans esprit, " + onGenre(cible, 'il', 'elle') +
" est immunisé" + onGenre(cible, '', 'e') + " aux attaques mentales.");
return;
} else if (predicateAsBool(cible, 'liberteDAction')) {
sendPerso(cible, "reste libre de ses actions !");
return;
}
}
let explications = options.messages || [];
if (!evt) {
evt = {
type: 'attaqueMagique',
action: {
titre: "Attaque magique",
attaquant: attaquant,
cible: cible,
options: options
}
};
addEvent(evt);
} else if (!evt.action) {
evt.action = {
titre: "Attaque magique",
attaquant: attaquant,
cible: cible,
options: options
};
}
entrerEnCombat(attaquant, [cible], explications, evt);
if (limiteRessources(attaquant, options, 'attaqueMagique', "l'attaque magique", evt)) {
return;
}
let bonus1 = bonusDAttaque(attaquant, explications, evt);
if (bonus1 === 0) bonus1 = "";
else if (bonus1 > 0) bonus1 = " +" + bonus1;
let attk1 = addOrigin(attaquant.token.get("name"), "[[" + computeArmeAtk(attaquant, '@{ATKMAG}') +
bonus1 + "]]");
let bonus2 = bonusDAttaque(cible, explications, evt);
if (bonus2 === 0) bonus2 = "";
else if (bonus2 > 0) bonus2 = " +" + bonus2;
let attk2 = addOrigin(cible.token.get("name"), "[[" + computeArmeAtk(cible, '@{ATKMAG}') +
bonus1 + "]]");
let de1 = computeDice(attaquant);
let de2 = computeDice(cible);
let toEvaluate = "[[" + de1 + "]] [[" + de2 + "]] " + attk1 + " " + attk2;
sendChat("", toEvaluate, function(res) {
let rolls = res[0];
options.rolls = options.rolls || {};
// Determine which roll number correspond to which expression
let afterEvaluate = rolls.content.split(" ");
let att1RollNumber = rollNumber(afterEvaluate[0]);
let att2RollNumber = rollNumber(afterEvaluate[1]);
let attk1SkillNumber = rollNumber(afterEvaluate[2]);
let attk2SkillNumber = rollNumber(afterEvaluate[3]);
let roll1 = (options.rolls && options.rolls.roll1) ? options.rolls.roll1 : rolls.inlinerolls[att1RollNumber];
let atk1 = (options.rolls && options.rolls.atk1) ? options.rolls.atk1 : rolls.inlinerolls[attk1SkillNumber];
let roll2 = (options.rolls && options.rolls.roll2) ? options.rolls.roll2 : rolls.inlinerolls[att2RollNumber];
let atk2 = (options.rolls && options.rolls.atk2) ? options.rolls.atk2 : rolls.inlinerolls[attk2SkillNumber];
roll1.token = attaquant.token;
atk1.token = attaquant.token;
roll2.token = cible.token;
atk2.token = cible.token;
evt.action.rolls = evt.action.rolls || {};
evt.action.rolls.roll1 = roll1;
evt.action.rolls.atk1 = atk1;
evt.action.rolls.roll2 = roll2;
evt.action.rolls.atk2 = atk2;
let d20roll1 = roll1.results.total;
effetAuD20(attaquant, d20roll1);
let att1Skill = atk1.results.total;
if (estAffaibli(attaquant) && predicateAsBool(attaquant, 'insensibleAffaibli')) att1Skill -= 2;
let attackRoll1 = d20roll1 + att1Skill;
if (options.chanceRollId && options.chanceRollId.roll1)
attackRoll1 += options.chanceRollId.roll1;
let d20roll2 = roll2.results.total;
effetAuD20(cible, d20roll2);
let att2Skill = atk2.results.total;
if (estAffaibli(cible) && predicateAsBool(cible, 'insensibleAffaibli')) att2Skill -= 2;
let attackRoll2 = d20roll2 + att2Skill;
if (options.chanceRollId && options.chanceRollId.roll2)
attackRoll2 += options.chanceRollId.roll2;
let action = "Attaque magique opposée";
let reussi;
if (d20roll1 == 1) {
if (d20roll2 == 1) reussi = (attackRoll1 >= attackRoll2);
else reussi = false;
} else if (d20roll2 == 1) reussi = true;
else if (d20roll1 == 20) {
if (d20roll2 == 20) reussi = (attackRoll1 >= attackRoll2);
else reussi = true;
} else reussi = (attackRoll1 >= attackRoll2);
const display = startFramedDisplay(playerId, action, attaquant, {
perso2: cible
});
let line = nomPerso(attaquant) + " fait " + buildinline(roll1);
if (att1Skill > 0) line += "+" + att1Skill;
else if (att1Skill < 0) line += att1Skill;
if (options.chanceRollId && options.chanceRollId.roll1)
line += "+" + options.chanceRollId.roll1;
line += " = " + attackRoll1;
if (!reussi) {
const pcAttaquant = pointsDeChance(attaquant);
if (pcAttaquant > 0)
line += "
" + boutonSimple("!cof-bouton-chance " + evt.id + " roll1", "Chance") +
" (reste " + pcAttaquant + " PC)";
if (predicateAsInt(attaquant, 'pacteSanglant', 0) >= 3) {
line += "
" + boutonSimple("!cof-pacte-sanglant " + evt.id + " 3 roll1", "Pacte sanglant (+3)");
}
if (predicateAsInt(attaquant, 'pacteSanglant', 0) >= 5) {
line += "
" + boutonSimple("!cof-pacte-sanglant " + evt.id + " 5 roll1", "Pacte sanglant (+5)");
}
}
addLineToFramedDisplay(display, line);
line = nomPerso(cible) + " fait " + buildinline(roll2);
if (att2Skill > 0) line += "+" + att2Skill;
else if (att2Skill < 0) line += att2Skill;
if (options.chanceRollId && options.chanceRollId.roll2)
line += "+" + options.chanceRollId.roll2;
line += " = " + attackRoll2;
if (reussi) {
let pcCible = pointsDeChance(cible);
if (pcCible > 0)
line += "
" + boutonSimple("!cof-bouton-chance " + evt.id + " roll2", "Chance") +
" (reste " + pcCible + " PC)";
if (predicateAsInt(cible, 'pacteSanglant', 0) >= 3) {
line += "
" + boutonSimple("!cof-pacte-sanglant " + evt.id + " 3 roll2", "Pacte sanglant (+3)");
}
if (predicateAsInt(cible, 'pacteSanglant', 0) >= 5) {
line += "
" + boutonSimple("!cof-pacte-sanglant " + evt.id + " 5 roll2", "Pacte sanglant (+5)");
}
}
addLineToFramedDisplay(display, line);
if (reussi) {
diminueMalediction(cible, evt);
addLineToFramedDisplay(display, "Attaque réussie !");
} else {
diminueMalediction(attaquant, evt);
addLineToFramedDisplay(display, "L'attaque échoue.");
}
explications.forEach(explication => addLineToFramedDisplay(display, explication, 80));
if (callback) callback(display, reussi);
else {
sendFramedDisplay(display);
}
});
}
function injonction(playerId, attaquant, cible, options) {
options.attaqueMentale = true;
const evt = {
type: 'injonction',
action: {
titre: "Injonction",
attaquant: attaquant,
cible: cible,
options: options
}
};
addEvent(evt);
attaqueMagiqueOpposee(playerId, attaquant, cible, options,
function(display, reussi) {
if (reussi) {
if (attributeAsBool(cible, 'immunise24HA_injonction')) {
addLineToFramedDisplay(display, nomPerso(cible) + " a déjà résisté à une injonction aujourd'hui, c'est sans effet");
} else if (predicateAsBool(cible, 'liberteDAction')) {
addLineToFramedDisplay(display, nomPerso(cible) + " reste libre de ses actions !");
} else {
addLineToFramedDisplay(display, nomPerso(cible) + " obéit à l'injonction");
}
sendFramedDisplay(display);
} else {
setTokenAttr(cible, 'immunise24HA_injonction', true, evt);
addLineToFramedDisplay(display, nomPerso(cible) + " n'obéit pas à l'injonction");
sendFramedDisplay(display);
}
}, evt);
}
function tueurFantasmagorique(playerId, attaquant, cible, options) {
const evt = {
type: 'tueurFantasmagorique',
action: {
titre: "Tueur Fantasmagorique",
attaquant: attaquant,
cible: cible,
options: options
}
};
addEvent(evt);
attaqueMagiqueOpposee(playerId, attaquant, cible, options,
function(display, reussi) {
if (reussi) {
if (estNonVivant(cible)) {
addLineToFramedDisplay(display, nomPerso(cible) + " n'est pas une créature vivante, il ne peut croire à sa mort");
sendFramedDisplay(display);
return;
}
if (attributeAsBool(cible, 'limiteParJour_tueurFantasmagorique')) {
addLineToFramedDisplay(display, nomPerso(cible) + " a déjà été victime d'un tueur fantasmagorique aujourd'hui, c'est sans effet");
sendFramedDisplay(display);
return;
}
setTokenAttr(cible, 'limiteParJour_tueurFantasmagorique', true, evt);
const s = {
carac: 'SAG',
seuil: 10 + modCarac(attaquant, 'charisme')
};
const niveauAttaquant = ficheAttributeAsInt(attaquant, 'niveau', 1);
const niveauCible = ficheAttributeAsInt(cible, 'niveau', 1);
if (niveauCible > niveauAttaquant)
s.seuil -= (niveauCible - niveauAttaquant) * 5;
else if (niveauCible < niveauAttaquant)
s.seuil += (niveauAttaquant - niveauCible);
const expliquer = function(message) {
addLineToFramedDisplay(display, message, 80);
};
const saveOpts = {
msgPour: " pour résister au tueur fantasmagorique",
attaquant: attaquant,
rolls: options.rolls,
chanceRollId: options.chanceRollId,
type: 'magique'
};
const saveId = 'tueurFantasmagorique_' + cible.token.id;
save(s, cible, saveId, expliquer, saveOpts, evt,
function(reussiteSave, texte) {
if (reussiteSave) {
addLineToFramedDisplay(display, nomPerso(cible) + " perd l'équilibre et tombe par terre");
setState(cible, 'renverse', true, evt);
} else { //save raté
addLineToFramedDisplay(display, nomPerso(cible) + " succombe à ses pires terreurs");
updateCurrentBar(cible, 1, 0, evt);
setState(cible, 'mort', true, evt);
}
sendFramedDisplay(display);
});
} else {
setTokenAttr(cible, 'limiteParJour_tueurFantasmagorique', true, evt);
sendFramedDisplay(display);
}
}, evt);
}
function enkystementLointain(playerId, attaquant, cible, options) {
const evt = {
type: 'enkystementLointain',
action: {
titre: "Enkystement lointain",
attaquant: attaquant,
cible: cible,
options: options
}
};
addEvent(evt);
attaqueMagiqueOpposee(playerId, attaquant, cible, options,
function(display, reussi) {
if (reussi) {
if (attributeAsBool(cible, 'limiteParJour_enkystementLointain')) {
addLineToFramedDisplay(display, nomPerso(cible) + " a déjà été victime d'un enkystement lointain aujourd'hui, c'est sans effet");
sendFramedDisplay(display);
return;
}
setTokenAttr(cible, 'limiteParJour_enkystementLointain', true, evt);
const niveauAttaquant = ficheAttributeAsInt(attaquant, 'niveau', 1);
const niveauCible = ficheAttributeAsInt(cible, 'niveau', 1);
let {
val,
roll
} = rollDePlus(20);
let message = nomPerso(cible) + " est téléporté" + eForFemale(cible) + " à une distance de ";
if (niveauCible < niveauAttaquant / 2) {
message += (val * 100) + " kilomètres.";
} else if (niveauCible < niveauAttaquant) {
message += roll + " kilomètre" + ((val > 1) ? 's' : '');
} else {
message += (val * 10) + " mètres.";
}
options.targetFx = options.targetFx || 'bomb-magic';
effetsSpeciaux(attaquant, cible, options);
addLineToFramedDisplay(display, message);
sendFramedDisplay(display);
} else {
//Dans ce cas, pas victime, donc on permet d'autre tentatives
sendFramedDisplay(display);
}
}, evt);
}
function parseInjonctionMortelle(msg) {
const options = parseOptions(msg);
if (options === undefined || options.cmd === undefined) return;
const cmd = options.cmd;
if (cmd.length < 3) {
error("Il faut au moins 2 arguments à !cof-injonction-mortelle", cmd);
return;
}
let attaquant = persoOfId(cmd[1], cmd[1]);
let cible = persoOfId(cmd[2], cmd[2]);
if (attaquant === undefined || cible === undefined) {
error("Arguments de !cof-injonction-mortelle", cmd);
return;
}
let distance = distanceCombat(attaquant.token, cible.token, options.pageId);
if (distance > 30) {
sendPerso(attaquant, "est trop loin de " + nomPerso(cible) +
" pour l'injonction mortelle");
return;
}
injonctionMortelle(getPlayerIdFromMsg(msg), attaquant, cible, options);
}
function injonctionMortelle(playerId, attaquant, cible, options) {
const evt = {
type: 'injonctionMortelle',
action: {
titre: "Injonction Mortelle",
playerId: playerId,
attaquant: attaquant,
cible: cible,
options: options
}
};
addEvent(evt);
let explications = options.messages || [];
entrerEnCombat(attaquant, [cible], explications, evt);
const display = startFramedDisplay(playerId, evt.action.titre, attaquant, {
perso2: cible
});
explications.forEach(msg => addLineToFramedDisplay(display, msg, 80));
if (attributeAsBool(cible, 'injonctionMortelle')) {
addLineToFramedDisplay(display, nomPerso(cible) + " a déjà été victime d'une injonction mortelle ce combat, c'est sans effet");
sendFramedDisplay(display);
return;
}
if (predicateAsBool(cible, 'liberteDAction')) {
addLineToFramedDisplay(display, nomPerso(cible) + " reste libre de ses actions !");
sendFramedDisplay(display);
return;
}
setTokenAttr(cible, 'injonctionMortelle', true, evt);
let saveOpts = {
msgPour: " pour résister à l'injonction mortelle",
msgRate: ", raté.",
attaquant: attaquant,
rolls: options.rolls,
chanceRollId: options.chanceRollId,
necromancie: true
};
let saveId = 'injonctionMortelle_' + cible.token.id;
let expliquer = function(message) {
addLineToFramedDisplay(display, message, 80);
};
save({
carac: 'CON',
seuil: 15,
type: 'magique'
}, cible, saveId, expliquer, saveOpts, evt,
function(reussite, rollText) {
if (reussite) {
let nc = ficheAttributeAsInt(attaquant, "niveau", 0);
let dmg = {
type: 'normal',
value: '2d6+' + nc
};
sendChat('', '[[' + dmg.value + ']]', function(resDmg) {
dmg.roll = dmg.roll || resDmg[0];
let afterEvaluateDmg = dmg.roll.content.split(' ');
let dmgRollNumber = rollNumber(afterEvaluateDmg[0]);
dmg.total = dmg.roll.inlinerolls[dmgRollNumber].results.total;
dmg.display = buildinline(dmg.roll.inlinerolls[dmgRollNumber], dmg.type, options.magique);
let name = nomPerso(cible);
let explicationsDmg = [];
cible.attaquant = attaquant;
dealDamage(cible, dmg, [], evt, false, options, explicationsDmg, function(dmgDisplay, dmgFinal) {
addLineToFramedDisplay(display,
name + " reçoit " + dmgDisplay + " DM");
explicationsDmg.forEach(function(e) {
addLineToFramedDisplay(display, e, 80, false);
});
sendFramedDisplay(display);
});
});
} else {
addLineToFramedDisplay(display, nomPerso(cible) + " meurt sous l'injonction mortelle !", 80);
updateCurrentBar(cible, 1, 0, evt);
setState(cible, 'mort', true, evt);
sendFramedDisplay(display);
}
});
}
function parseSommeil(msg) { //sort de sommeil
const options = parseOptions(msg);
if (options === undefined) return;
const args = options.cmd;
if (args.length < 2) {
error("La fonction !cof-sommeil a besoin du nom ou de l'id du lanceur de sort", args);
return;
}
const lanceur = persoOfId(args[1], args[1]);
if (lanceur === undefined) {
error("Aucun personnage nommé " + args[1], args);
return;
}
const casterCharId = lanceur.charId;
const casterChar = getObj('character', casterCharId);
if (casterChar === undefined) {
error("Fiche de personnage manquante");
return;
}
getSelected(msg, function(selected, playerId) {
if (selected === undefined || selected.length === 0) {
sendPlayer(msg, "Pas de cible sélectionnée pour le sort de sommeil", playerId);
return;
}
let cibles = [];
iterSelected(selected, function(perso) {
cibles.push(perso);
});
doSommeil(lanceur, cibles, options);
}, {
lanceur
});
}
function doSommeil(lanceur, cibles, options, ciblesSansSave, ciblesAvecSave) {
const evt = {
type: 'sommeil',
action: {
lanceur,
cibles,
ciblesSansSave,
ciblesAvecSave,
options
}
};
addEvent(evt);
if (limiteRessources(lanceur, options, 'sommeil', "lancer un sort de sommeil", evt)) return;
let casterCharName = lanceur.token.get("name");
let cha = modCarac(lanceur, 'charisme');
let attMagText =
addOrigin(casterCharName, '[[' + computeArmeAtk(lanceur, '@{ATKMAG}') + ']]');
sendChat("", "[[1d6]] [[" + attMagText + "]]", function(res) {
evt.action.rolls = options.rolls || {};
let rollD6Id = 'sommeilD6';
let rolls = res[0];
let afterEvaluate = rolls.content.split(" ");
let d6RollNumber = rollNumber(afterEvaluate[0]);
let attMagRollNumber = rollNumber(afterEvaluate[1]);
let rollD6 = evt.action.rolls[rollD6Id] || rolls.inlinerolls[d6RollNumber];
evt.action.rolls[rollD6Id] = rollD6;
let nbTargetsMax = rollD6.results.total + cha;
let action = "Capacité : Sort de sommeil (max " + nbTargetsMax + " cibles)";
let display = startFramedDisplay(options.playerId, action, lanceur);
let attMag = rolls.inlinerolls[attMagRollNumber].results.total;
let targetsWithSave = [];
let targetsWithoutSave = [];
cibles.forEach(function(perso) {
if (estNonVivant(perso) || predicateAsBool(perso, 'immunite_endormi')) { //le sort de sommeil n'affecte que les créatures vivantes
addLineToFramedDisplay(display, nomPerso(perso) + " n'est pas affecté par le sommeil");
return;
}
if (predicateAsBool(perso, "liberteDAction")) {
addLineToFramedDisplay(display, nomPerso(perso) + " reste libre de ses mouvements !");
return;
}
let pv = perso.token.get('bar1_max');
if (pv > 2 * attMag) {
let line = nomPerso(perso) + " a trop de PV pour être affecté par le sort";
addLineToFramedDisplay(display, line);
} else if (pv > attMag) {
targetsWithSave.push(perso);
} else {
targetsWithoutSave.push(perso);
}
});
let ciblesSansSave;
if (evt.action.ciblesSansSave) {
ciblesSansSave = evt.action.ciblesSansSave;
nbTargetsMax -= ciblesSansSave.length;
} else {
ciblesSansSave = [];
let i, r;
if (targetsWithoutSave.length > nbTargetsMax) {
i = 0; //position to decide
while (nbTargetsMax > 0) {
r = randomInteger(nbTargetsMax) + i;
ciblesSansSave.push(targetsWithoutSave[r]);
targetsWithoutSave[r] = targetsWithoutSave[i];
i++;
nbTargetsMax--;
}
} else {
ciblesSansSave = targetsWithoutSave;
nbTargetsMax -= ciblesSansSave.length;
}
}
evt.action.ciblesSansSave = ciblesSansSave;
ciblesSansSave.forEach(function(t) {
setState(t, 'endormi', true, evt);
addLineToFramedDisplay(display, nomPerso(t) + " s'endort");
});
if (nbTargetsMax > 0 && targetsWithSave.length > 0) {
let ciblesAvecSave;
if (evt.action.ciblesAvecSave) {
ciblesAvecSave = evt.action.ciblesAvecSave;
nbTargetsMax -= ciblesAvecSave.length;
} else {
ciblesAvecSave = [];
if (targetsWithSave.length > nbTargetsMax) {
let j = 0;
while (nbTargetsMax > 0) {
let ra = randomInteger(nbTargetsMax) + j;
ciblesAvecSave.push(targetsWithSave[ra]);
targetsWithSave[ra] = targetsWithSave[j];
j++;
nbTargetsMax--;
}
} else {
ciblesAvecSave = targetsWithSave;
nbTargetsMax -= ciblesAvecSave.length;
}
}
let seuil = 10 + cha;
let tokensToProcess = ciblesAvecSave.length;
let finalize = function() {
if (tokensToProcess == 1) {
sendFramedDisplay(display);
}
tokensToProcess--;
};
evt.action.ciblesAvecSave = ciblesAvecSave;
ciblesAvecSave.forEach(function(perso) {
let testId = 'resisteSommeil_' + perso.token.id;
testCaracteristique(perso, 'SAG', seuil, testId, options, evt,
function(tr) {
let line = "Jet de résistance de " + nomPerso(perso) + ": " + tr.texte;
let sujet = onGenre(perso, 'il', 'elle');
if (tr.reussite) {
line += ">=" + seuil + ", " + sujet + " ne s'endort pas." + tr.modifiers;
} else {
setState(perso, 'endormi', true, evt);
line += "<" + seuil + ", " + sujet + " s'endort" + tr.rerolls + tr.modifiers;
}
addLineToFramedDisplay(display, line);
finalize();
});
});
} else { // all targets are without save
sendFramedDisplay(display);
}
});
}
//!cof-attaque-magique-contre-pv {selected|token_id} {target|token_id}
// deprecated
function attaqueMagiqueContrePV(msg) {
const options = parseOptions(msg);
if (options === undefined || options.cmd === undefined) return;
let cmd = options.cmd;
if (cmd.length < 3) {
error("Il faut au moins 2 arguments à !cof-attaque-magique-contre-pv", cmd);
return;
}
let attaquant = persoOfId(cmd[1], cmd[1]);
let cible = persoOfId(cmd[2], cmd[2]);
if (attaquant === undefined || cible === undefined) {
error("Arguments de !cof-attaque-magique-contre-pv incorrects", cmd);
return;
}
if (options.portee !== undefined) {
let distance = distanceCombat(attaquant.token, cible.token, options.pageId);
if (distance > options.portee) {
sendPerso(attaquant, "est trop loin de " + nomPerso(cible) +
" pour l'attaque magique");
return;
}
}
let pvMax = parseInt(cible.token.get('bar1_max'));
if (isNaN(pvMax)) {
error("Token avec des PV max qui ne sont pas un nombre", cible.token);
return;
}
const evt = {
type: 'Attaque magique',
};
addEvent(evt);
if (limiteRessources(attaquant, options, 'attaque magique', "l'attaque magique", evt)) return;
let attaquantChar = getObj('character', attaquant.charId);
if (attaquantChar === undefined) {
error("Fiche de l'attaquant introuvable");
return;
}
attaquant.name = attaquantChar.get('name'); //TODO: utile ?
let playerId = options.playerId || getPlayerIdFromMsg(msg);
let explications = [];
let bonusA = bonusDAttaque(attaquant, explications, evt);
if (bonusA === 0) bonusA = "";
else if (bonusA > 0) bonusA = " +" + bonusA;
let attMagText = addOrigin(attaquant.name, "[[" + computeArmeAtk(attaquant, '@{ATKMAG}') + bonusA + "]]");
let de = computeDice(attaquant);
let action = "Attaque magique (contre pv max)";
const display = startFramedDisplay(playerId, action, attaquant, {
perso2: cible
});
sendChat("", "[[" + de + "]] " + attMagText, function(res) {
let rolls = res[0];
let afterEvaluate = rolls.content.split(" ");
let attRollNumber = rollNumber(afterEvaluate[0]);
let attSkillNumber = rollNumber(afterEvaluate[1]);
let d20roll = rolls.inlinerolls[attRollNumber].results.total;
effetAuD20(attaquant, d20roll);
let attSkill = rolls.inlinerolls[attSkillNumber].results.total;
if (estAffaibli(attaquant) && predicateAsBool(attaquant, 'insensibleAffaibli')) attSkill -= 2;
let attackRoll = d20roll + attSkill;
let line =
nomPerso(attaquant) + " fait " +
buildinline(rolls.inlinerolls[attRollNumber]);
if (attSkill > 0) line += "+" + attSkill + " = " + attackRoll;
else if (attSkill < 0) line += attSkill + " = " + attackRoll;
addLineToFramedDisplay(display, line);
let reussi;
if (d20roll == 1) reussi = false;
else if (d20roll == 20) reussi = true;
else reussi = (attackRoll >= pvMax);
if (reussi) {
addLineToFramedDisplay(display, "Attaque réussi !");
} else {
diminueMalediction(attaquant, evt);
addLineToFramedDisplay(display, "L'attaque échoue.");
}
sendFramedDisplay(display);
}); //Fin du jet de dés pour l'attaque
}
function transeGuerison(msg) {
if (stateCOF.combat) {
sendPlayer(msg, "Pas possible de méditer en combat");
return;
}
const options = parseOptions(msg);
if (options === undefined) return;
getSelected(msg, function(selected, playerId) {
if (selected === undefined || selected.length === 0) {
sendPlayer(msg, "Pas de cible sélectionnée pour la transe de guérison", playerId);
return;
}
const evt = {
type: "Transe de guérison",
};
iterSelected(selected, function(perso) {
let token = perso.token;
if (attributeAsBool(perso, 'transeDeGuérison')) {
sendPerso(perso, "a déjà médité depuis le dernier combat");
return;
}
let bar1 = parseInt(token.get("bar1_value"));
let pvmax = parseInt(token.get("bar1_max"));
if (isNaN(bar1) || isNaN(pvmax)) return;
if (bar1 >= pvmax) {
sendPerso(perso, "n'a pas besoin de méditer");
return;
}
let sagMod = modCarac(perso, 'sagesse');
let niveau = ficheAttributeAsInt(perso, 'niveau', 1);
let soin = niveau + sagMod;
if (soin < 0) soin = 0;
if (bar1 === 0) {
if (attributeAsBool(perso, 'etatExsangue')) {
removeTokenAttr(perso, 'etatExsangue', evt, {
msg: "retrouve des couleurs"
});
}
}
bar1 += soin;
if (bar1 > pvmax) {
soin -= (bar1 - pvmax);
bar1 = pvmax;
}
updateCurrentBar(perso, 1, bar1, evt);
setTokenAttr(perso, 'transeDeGuérison', true, evt);
sendPerso(perso, "entre en méditation pendant 10 minutes et récupère " + soin + " points de vie.");
});
addEvent(evt);
}, options);
}
function raceIs(perso, race) {
if (perso.race === undefined) {
perso.race = ficheAttribute(perso, 'race', '');
perso.race = perso.race.toLowerCase();
}
return (perso.race.includes(race.toLowerCase()));
}
function estFee(perso) {
if (predicateAsBool(perso, 'fée')) return true;
if (perso.race === undefined) {
perso.race = ficheAttribute(perso, 'race', '');
perso.race = perso.race.toLowerCase();
}
if (perso.race === '') return false;
switch (perso.race) {
case 'licorne':
case 'farfadet':
case 'fee':
case 'fée':
case 'pixie':
case 'lutin':
return true;
default:
return false;
}
}
function estDemon(perso) {
if (predicateAsBool(perso, 'démon')) return true;
if (perso.race === undefined) {
perso.race = ficheAttribute(perso, 'race', '');
perso.race = perso.race.toLowerCase();
}
if (perso.race === '') return false;
switch (perso.race) {
case 'démon':
case 'demon':
case 'balor':
case 'marilith':
case 'quasit':
case 'succube':
return true;
default:
return false;
}
}
function estDraconique(perso) {
if (predicateAsBool(perso, 'dragon')) return true;
if (perso.race === undefined) {
perso.race = ficheAttribute(perso, 'race', '');
perso.race = perso.race.toLowerCase();
}
if (perso.race === '') return false;
switch (perso.race) {
case 'dragon':
case 'draconide':
case 'kobold':
return true;
default:
let mots = perso.race.split(' ');
return mots.includes('dragon');
}
}
function estMortVivant(perso) {
if (predicateAsBool(perso, 'mortVivant')) return true;
if (perso.race === undefined) {
perso.race = ficheAttribute(perso, 'race', '');
perso.race = perso.race.toLowerCase();
}
if (perso.race === '') return false;
switch (perso.race) {
case 'squelette':
case 'zombie':
case 'mort-vivant':
case 'mort vivant':
case 'momie':
case 'goule':
case 'vampire':
return true;
default:
return false;
}
}
function estGeant(perso) {
if (predicateAsBool(perso, 'géant')) return true;
if (perso.race === undefined) {
perso.race = ficheAttribute(perso, 'race', '');
perso.race = perso.race.toLowerCase();
}
if (perso.race === '') return false;
switch (perso.race) {
case 'géant':
case 'geant':
case 'ogre':
case 'troll':
case 'ettin':
case 'cyclope':
case 'yai':
return true;
default:
return false;
}
}
function estGobelin(perso) {
if (predicateAsBool(perso, 'gobelin')) return true;
if (perso.race === undefined) {
perso.race = ficheAttribute(perso, 'race', '');
perso.race = perso.race.toLowerCase();
}
if (perso.race === '') return false;
switch (perso.race) {
case 'gobelin':
case 'gobelours':
case 'hobgobelin':
case 'wikkawak':
return true;
default:
return false;
}
}
function estNonVivant(perso) {
return (predicateAsBool(perso, 'nonVivant') ||
attributeAsBool(perso, 'masqueMortuaire') ||
attributeAsBool(perso, 'masqueMortuaireAmeLiee') ||
estMortVivant(perso));
}
function estElfeNoir(perso) {
if (predicateAsBool(perso, 'elfeNoir')) return true;
if (perso.race === undefined) {
perso.race = ficheAttribute(perso, 'race', '');
perso.race = perso.race.toLowerCase();
}
if (perso.race === '') return false;
if (perso.race.includes('elf') && perso.race.includes('noir')) return true;
switch (perso.race) {
case 'drider':
case 'drow':
return true;
default:
return false;
}
}
function estElfe(perso) {
if (predicateAsBool(perso, 'elfe')) return true;
if (perso.race === undefined) {
perso.race = ficheAttribute(perso, 'race', '');
perso.race = perso.race.toLowerCase();
}
if (perso.race === '') return false;
if (perso.race.includes('elfe')) return true;
return false;
}
//Vrai pour les insectes et araignées
function estInsecte(perso) {
if (predicateAsBool(perso, 'insecte')) return true;
if (perso.profil === undefined) {
perso.profil = ficheAttribute(perso, 'profil', '');
perso.profil = perso.profil.toLowerCase();
}
if (perso.profil == 'insecte') return true;
if (perso.profil == 'araignée') return true;
if (perso.race === undefined) {
perso.race = ficheAttribute(perso, 'race', '');
perso.race = perso.race.toLowerCase();
}
if (perso.race === '') return false;
if (perso.race.includes('elf') && perso.race.includes('noir')) return true;
switch (perso.race) {
case 'ankheg':
case 'araignée':
case 'araignee':
case 'insecte':
return true;
default:
return false;
}
}
function estHumanoide(perso) {
if (predicateAsBool(perso, 'humanoide')) return true;
if (perso.race === undefined) {
perso.race = ficheAttribute(perso, 'race', '');
perso.race = perso.race.toLowerCase();
}
if (perso.race === '') return false;
switch (perso.race) {
case 'humain':
case 'nain':
case 'elfe':
case 'elfe des bois':
case 'elfe noir':
case 'drow':
case 'haut elfe':
case 'halfelin':
case 'géant':
case 'geant':
case 'ange':
case 'barghest':
case 'démon':
case 'doppleganger':
case 'dryade':
case 'gnoll':
case 'gobelin':
case 'gobelours':
case 'hobegobelin':
case 'homme-lézard':
case 'kobold':
case 'nymphe':
case 'ogre':
case 'orque':
case 'pixie':
case 'troll':
return true;
default:
return perso.race.startsWith('humain');
}
}
function estQuadrupede(perso) {
if (predicateAsBool(perso, 'quadrupede')) return true;
if (perso.race === undefined) {
perso.race = ficheAttribute(perso, 'race', '');
perso.race = perso.race.toLowerCase();
}
if (perso.race === '') return false;
switch (perso.race) {
case 'ankheg':
case 'araignée':
case 'araignee':
case 'basilic':
case 'béhir':
case 'behir':
case 'bulette':
case 'bison':
case 'centaure':
case 'cheval':
case 'chien':
case 'chimère':
case 'chimere':
case 'cockatrice':
case 'crocodile':
case 'dragon':
case 'drider':
case 'eléphant':
case 'elephant':
case 'éléphant':
case 'mammouth':
case 'griffon':
case 'hipogriffe':
case 'hippogriffe':
case 'hydre':
case 'licorne':
case 'lion':
case 'loup':
case 'worg':
case 'manticore':
case 'ours':
case 'panthere':
case 'panthère':
case 'pegase':
case 'pégase':
case 'pieuvre':
case 'rat':
case 'rhinoceros':
case 'rhinocéros':
case 'sanglier':
case 'taureau':
case 'tigre':
return true;
default:
return false;
}
}
function estAnimal(perso) {
if (predicateAsBool(perso, 'animal')) return true;
let attr = findObjs({
_type: 'attribute',
_characterid: perso.charId,
});
let attrProfile = attr.filter(function(a) {
return a.get('name').toUpperCase() == 'PROFIL';
});
if (attrProfile.length > 0) {
if (attrProfile[0].get('current').trim().toLowerCase() == 'animal')
return true;
}
let attrRace = attr.filter(function(a) {
return a.get('name').toUpperCase() == 'RACE';
});
if (attrRace.length === 0) return false;
let charRace = attrRace[0].get('current').trim().toLowerCase();
switch (charRace) {
case 'animal':
case 'aigle':
case 'araignee':
case 'araignée':
case 'basilic':
case 'bulette':
case 'bison':
case 'calmar':
case 'chauve-souris':
case 'cheval':
case 'chien':
case 'crocodile':
case 'dinosaure':
case 'éléphant':
case 'eléphant':
case 'elephant':
case 'gorille':
case 'griffon':
case 'hipogriffe':
case 'hydre':
case 'insecte':
case 'lion':
case 'loup':
case 'mammouth':
case 'manticore':
case 'ours':
case 'ours-hibou':
case 'panthère':
case 'pegase':
case 'pégase':
case 'pieuvre':
case 'rhinocéros':
case 'roc':
case 'sanglier':
case 'serpent':
case 'rat':
case 'taureau':
case 'tigre':
case 'wiverne':
return true;
default:
return false;
}
}
function estMauvais(perso) {
if (predicateAsBool(perso, 'mauvais')) return true;
if (estDemon(perso)) return true; //remplit perso.race
switch (perso.race) {
case 'squelette':
case 'zombie':
case 'élémentaire':
case 'momie':
return true;
default:
return false;
}
}
function estAussiGrandQue(perso1, perso2) {
let t1 = taillePersonnage(perso1);
let t2 = taillePersonnage(perso2);
if (t1 === undefined || t2 === undefined) return true;
return t1 >= t2;
}
//Lance les fx et les sons à la fin d'une action qui a réussi
// perso, cible et pageId sont optionnel
function effetsSpeciaux(perso, cible, options, pageId) {
if (options.fx && perso) {
pageId = pageId || perso.token.get('pageid');
if (cible) {
let p1e = {
x: perso.token.get('left'),
y: perso.token.get('top'),
};
let p2e = {
x: cible.token.get('left'),
y: cible.token.get('top'),
};
spawnFxBetweenPoints(p1e, p2e, options.fx, pageId);
} else {
spawnFx(perso.token.get('left'), perso.token.get('top'), options.fx, pageId);
}
}
if (options.son) playSound(options.son);
if (options.targetFx && cible) {
pageId = pageId || cible.token.get('pageid');
spawnFx(cible.token.get('left'), cible.token.get('top'), options.targetFx, pageId);
}
}
//!cof-soin
function soigner(msg) {
const options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd.length < 2) {
error("Il faut au moins un argument à !cof-soin", cmd);
return;
}
let soigneur = options.lanceur;
let pageId = options.pageId;
let cible;
let argSoin;
if (cmd.length > 4) {
error("Trop d'arguments à !cof-soin", cmd);
}
if (cmd.length > 2) { //cof-soin lanceur [cible] montant
if (soigneur === undefined) {
soigneur = persoOfId(cmd[1], cmd[1]);
if (soigneur === undefined) {
error("Le premier argument n'est pas un token valide", cmd[1]);
return;
}
pageId = soigneur.token.get('pageid');
}
if (cmd.length > 3) { // on a la cible en argument
cible = persoOfId(cmd[2], cmd[2], pageId);
if (cible === undefined) {
error("Le deuxième argument n'est pas un token valide: " + msg.content, cmd[2]);
return;
}
argSoin = cmd[3];
} else {
argSoin = cmd[2];
}
} else { //on a juste le montant des soins
argSoin = cmd[1];
}
if (soigneur === undefined && (options.mana || (options.portee !== undefined) || options.limiteParJour || options.limiteParCombat || options.dose || options.limiteSoinsParJour)) {
error("Il faut préciser un soigneur pour ces options d'effet", options);
return;
}
let charId;
let niveau = 1;
let rangSoin = 0;
let soins;
if (soigneur) {
charId = soigneur.charId;
niveau = ficheAttributeAsInt(soigneur, 'niveau', 1);
rangSoin = predicateAsInt(soigneur, 'voieDesSoins', 0);
}
let effet = "soins";
let nbDes = 1;
let souffleDeVie = false;
if (options.tempeteDeManaIntense) nbDes += options.tempeteDeManaIntense;
switch (argSoin) {
case 'leger':
effet += ' légers';
if (options.dose === undefined && options.limiteParJour === undefined)
options.limiteAttribut = {
nom: 'soinsLegers',
message: "ne peut plus lancer de sort de soins légers aujourd'hui",
limite: rangSoin
};
let bonusLeger = niveau + predicateAsInt(soigneur, 'voieDuGuerisseur', 0);
soins = "[[" + nbDes + (options.puissant ? "d10" : "d8");
if (attributeAsBool(soigneur, 'formeDAnge') && predicateAsInt(soigneur, 'voieDeLArchange', 1) > 1) {
soins += 'ro1';
}
soins += " +" + bonusLeger + "]]";
if (options.portee === undefined) options.portee = 0;
break;
case 'modere':
effet += ' modérés';
if (options.dose === undefined && options.limiteParJour === undefined)
options.limiteAttribut = {
nom: 'soinsModeres',
message: "ne peut plus lancer de sort de soins modéréss aujourd'hui",
limite: rangSoin
};
if (options.portee === undefined) options.portee = 0;
let bonusModere = niveau + predicateAsInt(soigneur, 'voieDuGuerisseur', 0);
soins = "[[" + (nbDes + 1) + (options.puissant ? "d10" : "d8");
if (attributeAsBool(soigneur, 'formeDAnge') && predicateAsInt(soigneur, 'voieDeLArchange', 1) > 1) {
soins += 'ro1';
}
soins += " +" + bonusModere + "]]";
break;
case 'souffleDeVie':
{
let combat = stateCOF.combat;
if (!combat) {
whisperChar(charId, " ne peut pas lancer de souffle de vie en dehors des combats");
return;
}
souffleDeVie = combat.tour;
effet = 'souffleDeVie';
if (options.portee === undefined) options.portee = 20;
soins = "[[" + (nbDes + 1) + (options.puissant ? "d10" : "d8");
if (attributeAsBool(soigneur, 'formeDAnge') && predicateAsInt(soigneur, 'voieDeLArchange', 1) > 1) {
soins += 'ro1';
}
soins += " +" + niveau + "]]";
break;
}
case 'premiersSoins':
{
let combat = stateCOF.combat;
if (!combat) {
whisperChar(charId, " ne peut pas lancer premiers soins en dehors des combats");
return;
}
souffleDeVie = -1;
effet = 'premiersSoins';
if (options.portee === undefined) options.portee = 0;
soins = "[[" + (nbDes + 1) + (options.puissant ? 'd8' : 'd6');
if (attributeAsBool(soigneur, 'formeDAnge') && predicateAsInt(soigneur, 'voieDeLArchange', 1) > 1) {
soins += 'ro1';
}
soins += " +" + modCarac(soigneur, 'sagesse') + "]]";
break;
}
case 'groupe':
if (!stateCOF.combat) {
whisperChar(charId, " ne peut pas lancer de soin de groupe en dehors des combats");
return;
}
effet += ' de groupe';
if (options.dose === undefined && options.limiteParJour === undefined)
options.limiteAttribut = {
nom: 'attributDeCombat_soinsDeGroupe',
message: " a déjà fait un soin de groupe durant ce combat",
limite: 1
};
if (options.puissant) soins = "[[1d10";
else soins = "[[" + nbDes + "d8";
if (attributeAsBool(soigneur, 'formeDAnge') && predicateAsInt(soigneur, 'voieDeLArchange', 1) > 1) {
soins += 'ro1';
}
let bonusGroupe = niveau + predicateAsInt(soigneur, 'voieDuGuerisseur', 0);
soins += " + " + bonusGroupe + "]]";
msg.content += " --alliesEnVue --self";
if (options.mana === undefined && estPJ(soigneur)) {
if (ficheAttributeAsBool(soigneur, 'option_pm', true))
options.mana = 1;
}
break;
case 'secondSouffle':
{
if (!stateCOF.combat) {
whisperChar(charId, " ne peut pas utiliser la capacité second souffle en dehors des combats");
return;
}
effet = "second souffle";
if (options.dose === undefined && options.limiteParJour === undefined)
options.limiteAttribut = {
nom: 'secondSouffleUtilise',
message: " a déjà repris son souffle durant ce combat",
limite: 1
};
soins = "[[1d10+";
let bonus = predicateAsInt(soigneur, 'secondSouffle', 0, -1);
if (bonus > 0) soins += bonus;
else soins += niveau + "+" + modCarac(soigneur, 'constitution');
soins += "]]";
cible = soigneur;
options.recuperation = true;
if (bonus == -1 || bonus > 0) { //Il y a un prédicat second souffle
//On limite les soins à ce qui a été perdu dans ce combat
const pvDebut = attributeAsInt(soigneur, 'PVsDebutCombat', 0);
let pv = parseInt(soigneur.token.get('bar1_value'));
if (isNaN(pv)) return;
if (pvDebut <= pv) {
whisperChar(charId, "Aucun PV perdu pendant ce combat, second souffle sans effet");
return;
}
options.limiteSoins = pvDebut - pv;
}
break;
}
default:
if (options.tempeteDeManaIntense) {
let firstDicePart = argSoin.match(/[1-9][0-9]*d\d+/i);
if (firstDicePart && firstDicePart.length > 0) {
let fdp = firstDicePart[0];
nbDes = parseInt(fdp) + options.tempeteDeManaIntense;
argSoin =
argSoin.replace(fdp, nbDes + fdp.substring(fdp.search(/d/i)));
} else {
argSoin = '(' + argSoin + ')*' + (1 + options.tempeteDeManaIntense);
}
}
if (soigneur && attributeAsBool(soigneur, 'formeDAnge') && predicateAsInt(soigneur, 'voieDeLArchange', 1) > 1) {
argSoin = argSoin.replace(/([1-9][0-9]*d\d+)/gi, function(all, d) {
return d + 'ro1';
});
}
soins = "[[" + argSoin + "]]";
}
let ressourceLimiteSoinsParJour;
if (soigneur && options.limiteSoinsParJour) {
ressourceLimiteSoinsParJour = effet;
if (options.limiteSoinsParJourRessource)
ressourceLimiteSoinsParJour = options.limiteSoinsParJourRessource;
ressourceLimiteSoinsParJour = "limiteParJour_Soins" + ressourceLimiteSoinsParJour;
let soinsRestantsDuJour = attributeAsInt(soigneur, ressourceLimiteSoinsParJour, options.limiteSoinsParJour);
if (soinsRestantsDuJour < 1) {
whisperChar(charId, "Plus possible de faire ces soins aujourd'hui");
return;
}
if (options.limiteSoins === undefined || options.limiteSoins > soinsRestantsDuJour) {
options.limiteSoins = soinsRestantsDuJour;
}
}
const playerId = getPlayerIdFromMsg(msg);
if (options.tempeteDeMana && soigneur) {
if (options.tempeteDeMana.cout === 0) {
//On demande de préciser les options
const optMana = {
mana: options.mana,
rang: options.rang,
portee: options.portee,
altruiste: options.altruiste,
soins: true
};
setTempeteDeMana(playerId, soigneur, msg.content, optMana);
return;
} else {
if (options.rang && options.tempeteDeMana.cout > options.rang) {
sendPerso(soigneur, "Attention, le coût de la tempête de mana (" + options.tempeteDeMana.cout + ") est supérieur au rang du sort");
}
}
}
try {
sendChat('', soins, function(res) {
soins = res[0].inlinerolls[0].results.total;
let soinTxt = buildinline(res[0].inlinerolls[0], 'normal', true);
if (soins <= 0) {
sendChar(charId, "ne réussit pas à soigner (total de soins " + soinTxt + ")", true);
return;
}
const evt = {
type: effet
};
let limiteSoinsAtteinte;
if (options.limiteSoins && soins > options.limiteSoins) {
soins = options.limiteSoins;
limiteSoinsAtteinte = true;
}
let ressourceLimiteCibleParJour;
if (options.limiteCibleParJour) {
ressourceLimiteCibleParJour = effet;
if (options.limiteCibleParJourRessource)
ressourceLimiteCibleParJour = options.limiteCibleParJourRessource;
ressourceLimiteCibleParJour = "limiteParJour_" + ressourceLimiteCibleParJour;
}
let limiteATester = true;
let soinImpossible = false;
let nbCibles;
let display;
let pvsPartages = new Set();
let iterCibles = function(callback) {
if (cible) {
nbCibles = 1;
callback(cible);
} else {
getSelected(msg, function(selected) {
nbCibles = selected.length;
if (nbCibles > 1) {
display = startFramedDisplay(playerId, effet, soigneur);
} else if (nbCibles === 0) {
sendChar(charId, "personne à soigner", true);
return;
}
iterSelected(selected, callback);
}, {
lanceur: soigneur
});
}
};
let finSoin = function() {
if (nbCibles == 1) {
if (options.messages) {
options.messages.forEach(function(message) {
if (display) addLineToFramedDisplay(display, message);
else sendChar(charId, message, true);
});
}
if (display) sendFramedDisplay(display);
if (ressourceLimiteSoinsParJour) {
whisperChar(charId, "peut encore soigner de " + attributeAsInt(soigneur, ressourceLimiteSoinsParJour, options.limiteSoinsParJour) + " PV aujourd'hui.");
}
addEvent(evt);
}
nbCibles--;
};
iterCibles(function(cible) {
if (cible.name === undefined) {
let cibleChar = getObj('character', cible.charId);
if (cibleChar === undefined) {
finSoin();
return;
}
cible.name = cibleChar.get('name');
}
if (pvsPartages.has(cible.name)) {
finSoin();
return;
}
let ciblePartagee = predicatesNamed(cible, 'PVPartagesAvec');
ciblePartagee.forEach(function(cp) {
pvsPartages.add(cp);
});
if (ressourceLimiteCibleParJour) {
let utilisations =
attributeAsInt(cible, ressourceLimiteCibleParJour, options.limiteCibleParJour);
if (utilisations === 0) {
sendPerso(cible, "ne peut plus bénéficier de " + effet + " aujourd'hui");
finSoin();
return;
}
setTokenAttr(cible, ressourceLimiteCibleParJour, utilisations - 1, evt);
}
if (soinImpossible) {
finSoin();
return;
}
let token2 = cible.token;
let nomCible = token2.get('name');
let sujet = onGenre(cible, 'il', 'elle');
let Sujet = onGenre(cible, 'Il', 'Elle');
if (options.portee !== undefined) {
if (options.puissantPortee || options.tempeteDeManaPortee) options.portee = options.portee * 2;
let distance = distanceCombat(soigneur.token, token2, pageId);
if (distance > options.portee) {
if (display)
addLineToFramedDisplay(display, "" + nomCible + " : trop loin pour le soin.");
else
sendChar(charId,
"est trop loin de " + nomCible + " pour le soigner.", true);
finSoin();
return;
}
}
if (limiteATester) {
limiteATester = false;
if (limiteRessources(soigneur, options, effet, effet, evt)) {
soinImpossible = true;
display = undefined;
finSoin();
return;
}
if (display) {
addLineToFramedDisplay(display, "Résultat des dés : " + soinTxt);
}
if (msg.content.includes(' --sacrifierPV')) { //paie autant de PV que soins
if (soigneur === undefined) {
error("Il faut préciser qui est le soigneur pour utiliser l'option --sacrifierPV", msg.content);
soinImpossible = true;
display = undefined;
finSoin();
return;
}
let pvSoigneur = parseInt(soigneur.token.get('bar1_value'));
if (isNaN(pvSoigneur) || pvSoigneur <= 0) {
sendPerso(soigneur,
"ne peut pas soigner car " + onGenre(soigneur, 'il', 'elle') + " n'a plus de PV");
soinImpossible = true;
display = undefined;
finSoin();
return;
}
if (pvSoigneur < soins) {
soins = pvSoigneur;
}
updateCurrentBar(soigneur, 1, pvSoigneur - soins, evt);
let msgSacrifice = "sacrifie " + soins + " PV" + (soins > 1 ? 's' : '');
if (pvSoigneur == soins) {
mort(soigneur, undefined, evt);
msgSacrifice += " et en meurt";
}
if (display)
addLineToFramedDisplay(display, nomPerso(soigneur) + ' ' + msgSacrifice);
else sendPerso(soigneur, msgSacrifice);
}
}
if (souffleDeVie) {
//souffleDeVie = tour courant pour le sort de souffle de vie,
// et -1 pour le sort de premiers soins
let pv = parseInt(cible.token.get('bar1_value'));
if (isNaN(pv) || pv > 0) {
let sort = (souffleDeVie > 0) ? "souffle de vie" : "premiers soins";
let m = nomPerso(cible) + " n'est pas à 0 PV, impossible d'utiliser " + sort + " pour " + onGenre(cible, 'lui', 'elle');
if (display) addLineToFramedDisplay(display, m);
else sendChar(charId, m, true);
finSoin();
return;
}
if (souffleDeVie > 0) {
let d = tokenAttribute(cible, 'a0PVDepuis');
if (d.length > 0) {
d = d[0];
let tour = parseInt(d.get('current'));
let tropTard = false;
if (!isNaN(tour)) {
if (tour < souffleDeVie - 2) tropTard = true;
if (tour == souffleDeVie - 2) {
let init = parseInt(d.get('max'));
tropTard = !isNaN(init) && init > stateCOF.combat.init;
}
}
if (tropTard) {
let m = nomPerso(cible) + " est à 0 PV depuis plus de 2 tours, impossible d'utiliser le souffle de vie pour " + onGenre(cible, 'lui', 'elle');
if (display) addLineToFramedDisplay(display, m);
else sendChar(charId, m, true);
finSoin();
return;
}
evt.deletedAttributes = evt.deletedAttributes || [];
deleteAttribute(d, evt);
if (estPJ(cible) && reglesOptionelles.dommages.val.blessures_graves.val) {
//Il faut alors annuler la perte de PR ou la blessure grave
let pr = pointsDeRecuperation(cible);
if (pr && (pr.current > 0 || !getState(cible, 'blesse'))) {
rajouterPointDeRecuperation(cible, evt, pr);
} else {
setState(cible, 'blesse', false, evt);
}
}
}
}
}
let callMax = function() {
if (display) {
addLineToFramedDisplay(display, "" + nomCible + " : pas besoin de soins.");
} else {
let maxMsg = "n'a pas besoin de ";
if (options.recuperation) {
maxMsg = "se reposer";
charId = soigneur.charId;
} else if (!soigneur || token2.id == soigneur.token.id) {
maxMsg += "se soigner";
charId = cible.charId;
} else {
maxMsg += "soigner " + nomCible;
}
sendChar(charId, maxMsg + ". " + Sujet + " est déjà au maximum de PV", true);
}
};
let extraImg = afficheOptionImage(options);
const printTrue = function(s) {
if (ressourceLimiteSoinsParJour) {
addToAttributeAsInt(soigneur, ressourceLimiteSoinsParJour, options.limiteSoinsParJour, -s, evt);
}
if (display) {
addLineToFramedDisplay(display,
"" + nomCible + " : + " + s + " PV" + extraImg);
} else {
let msgSoin;
if (!soigneur || token2.id == soigneur.token.id) {
msgSoin = 'se soigne';
charId = cible.charId;
} else {
msgSoin = 'soigne ' + nomCible;
}
msgSoin += " de ";
if (options.recuperation) msgSoin = "récupère ";
if (limiteSoinsAtteinte || s != soins)
msgSoin += s + " PV. (Le résultat du jet était " + soinTxt + ")";
else msgSoin += soinTxt + " PV.";
msgSoin += extraImg;
sendChar(charId, msgSoin, true);
}
};
let callTrueFinal = printTrue;
if (msg.content.includes(' --transfer')) { //paie avec ses PV
if (soigneur === undefined) {
error("Il faut préciser qui est le soigneur pour utiliser l'option --transfer", msg.content);
soinImpossible = true;
finSoin();
return;
}
let pvSoigneur = parseInt(soigneur.token.get('bar1_value'));
if (isNaN(pvSoigneur) || pvSoigneur <= 0) {
if (display)
addLineToFramedDisplay(display, "" + nomCible + " : plus assez de PV pour le soigner");
else
sendPerso(soigneur,
"ne peut pas soigner " + nomCible + ", " + sujet + " n'a plus de PV");
soinImpossible = true;
finSoin();
return;
}
if (pvSoigneur < soins) {
soins = pvSoigneur;
}
callTrueFinal = function(s) {
updateCurrentBar(soigneur, 1, pvSoigneur - s, evt);
if (pvSoigneur == s) mort(soigneur, undefined, evt);
printTrue(s);
};
}
effetsSpeciaux(soigneur, cible, options, pageId);
soigneToken(cible, soins, evt, callTrueFinal, callMax, options);
finSoin();
}); //fin de iterCibles
}); //fin du sendChat du jet de dés
} catch (e) {
if (soins) {
log(msg.content);
log("L'expression des soins était " + soins + ", et il y a eu une erreur durant son évaluation");
if (argSoin) {
error("L'expression des soins (" + argSoin + ") n'est pas bien formée", msg.content);
} else {
error("Erreur pendant l'évaluation de l'expression des soins. Plus d'informations dans le log", msg);
}
} else {
error("Erreur pendant les soins ", msg.content);
throw e;
}
}
}
function removeConsommables(nom, evt, attrs) {
const prefixes = new Set();
let empty = true;
attrs = attrs.filter(function(a) {
let attrName = a.get('name');
let m = consommableNomRegExp.exec(attrName);
if (!m) return true;
if (a.get('current').trim() == nom) {
prefixes.add(m[1]);
a.remove();
empty = false;
return false;
}
return true;
});
if (empty) return attrs;
let regExp = '^(';
let notFirst = false;
prefixes.forEach(function(pref) {
if (notFirst) regExp += '|';
regExp += pref;
});
regExp += ').*?$';
regExp = new RegExp(regExp);
attrs = attrs.filter(function(a) {
if (regExp.test(a.get('name'))) {
a.remove();
return false;
}
return true;
});
return attrs;
}
function ajouterConsommable(perso, nom, nb, action, evt) {
if (perso.token.get('bar1_link') === '') { //Perso non lié, on utilise un attribut
let attrName = 'dose_' + nom;
let attr = tokenAttribute(perso, attrName);
if (attr.length > 0) {
attr = attr[0];
let bd = parseInt(attr.get('current'));
if (!isNaN(bd) && bd > 0) nb += bd;
evt.attributes = evt.attributes || [];
evt.attributes.push({
attribute: attr,
current: bd,
max: attr.get('max')
});
attr.set({
current: nb,
max: action
});
} else {
setTokenAttr(perso, attrName, nb, evt, {
maxVal: action
});
}
} else { //On va mettre les consommables dans l'équipement
let attributes = findObjs({
_type: 'attribute',
_characterid: perso.charId
});
let found = attributes.find(function(attr) {
let attrName = attr.get('name');
let m = consommableNomRegExp.exec(attrName);
if (!m) return false;
if (attr.get('current').trim() != nom) return false;
let consoPrefix = m[1];
let attrEffet =
charAttribute(perso.charId, consoPrefix + 'equip_effet');
if (attrEffet.length === 0) {
attrEffet = createObj('attribute', {
characterid: perso.charId,
name: consoPrefix + 'equip_effet',
current: action
});
evt.attributes = evt.attributes || [];
evt.attributes.push({
attribute: attrEffet,
});
} else if (attrEffet[0].get('current').trim() != action) {
return false;
}
let attrQte = charAttribute(perso.charId, consoPrefix + 'equip_qte');
if (attrQte.length === 0) {
attrQte = createObj('attribute', {
characterid: perso.charId,
name: consoPrefix + 'equip_qte',
current: nb + 1,
});
evt.attributes = evt.attributes || [];
evt.attributes.push({
attribute: attrQte,
});
return true;
}
attrQte = attrQte[0];
let quantite = parseInt(attrQte.get('current'));
if (isNaN(quantite) || quantite < 1) quantite = 0;
attrQte.set('current', quantite + nb);
evt.attributes = evt.attributes || [];
evt.attributes.push({
attribute: attrQte,
current: quantite
});
return true;
});
// si le consommable n'a pas été trouvé, on le crée avec une valeur de nb
if (!found) {
let pref = 'repeating_equipement_' + generateRowID() + '_';
let attre = createObj("attribute", {
name: pref + 'equip_nom',
current: nom,
characterid: perso.charId
});
evt.attributes = evt.attributes || [];
evt.attributes.push({
attribute: attre,
});
attre = createObj('attribute', {
name: pref + 'equip_effet',
current: action,
characterid: perso.charId
});
evt.attributes.push({
attribute: attre,
});
if (nb > 1) {
let attrQte = createObj('attribute', {
characterid: perso.charId,
name: pref + 'equip_qte',
current: nb,
});
evt.attributes.push({
attribute: attrQte,
});
}
}
}
}
function parseNatureNourriciere(msg) {
const options = parseOptions(msg);
getSelected(msg, function(selected) {
iterSelected(selected, function(lanceur) {
let voieDeLaSurvie = predicateAsInt(lanceur, 'voieDeLaSurvie', 0);
if (voieDeLaSurvie < 1) {
sendPerso(lanceur, " ne connaît pas la Voie de la Survie ?");
}
doNatureNourriciere(lanceur, options);
});
}, options);
}
function doNatureNourriciere(perso, options) {
const evt = {
type: "natureNourriciere",
action: {
perso: perso,
options: options,
rolls: {}
}
};
addEvent(evt);
let voieDeLaSurvie = predicateAsInt(perso, 'voieDeLaSurvie', 0);
let trouveBaies = predicateAsBool(perso, 'natureNourriciereBaies');
if (options.rolls && options.rolls.duree) {
evt.action.rolls.duree = options.rolls.duree;
} else {
evt.action.rolls.duree = rollDePlus(6);
}
let output = "cherche des herbes. ";
if (trouveBaies) output = "cherche des baies. ";
output += "Après " + evt.action.rolls.duree.roll + " heure";
if (evt.action.rolls.duree.val > 1) output += "s";
output += ", " + onGenre(perso, "il", "elle");
const testId = 'natureNourriciere';
testCaracteristique(perso, 'SAG', 10, testId, options, evt,
function(tr) {
let post = "";
if ((tr.reussite && !trouveBaies) || (trouveBaies && !tr.reussite && tr.valeur > 7)) {
if (voieDeLaSurvie > 0) {
output += " revient avec " + voieDeLaSurvie + " plantes médicinales." + tr.modifiers;
let actionHerbes = "!cof-soin @{selected|token_id} @{selected|token_id} 1d6";
ajouterConsommable(perso, 'Plante médicinale', voieDeLaSurvie, actionHerbes, evt);
} else {
output += " revient avec de quoi soigner les blessés." + tr.modifiers;
}
} else if (tr.reussite && trouveBaies) {
let niveau = ficheAttributeAsInt(perso, 'niveau', 1);
let actionBaies = "!cof-consommer-baie " + niveau + " --limiteParJour 1 baieMagique";
let nbBaies = voieDeLaSurvie + Math.floor((tr.valeur - 10) / 2);
if (nbBaies === 0) nbBaies = 1;
output += " revient avec " + nbBaies + " baies magiques." + tr.modifiers;
ajouterConsommable(perso, 'Baie magique', nbBaies, actionBaies, evt);
} else {
output += " revient bredouille." + tr.rerolls + tr.modifiers;
}
output += "(test de SAG:" + tr.texte + ")";
output += post;
sendPerso(perso, output);
});
}
function ignorerLaDouleur(msg) {
const options = parseOptions(msg);
if (options === undefined) return;
const cmd = options.cmd;
let evtARefaire = lastEvent();
if (cmd !== undefined && cmd.length > 1) { //On relance pour un événement particulier
evtARefaire = findEvent(cmd[1]);
if (evtARefaire === undefined) {
error("L'action est trop ancienne ou a été annulée", cmd);
return;
}
}
getSelected(msg, function(selected) {
iterSelected(selected, function(chevalier) {
let token = chevalier.token;
if (attributeAsInt(chevalier, 'douleurIgnoree', 0) > 0) {
sendPerso(chevalier, "a déjà ignoré la doubleur une fois pendant ce combat");
return;
}
if (evtARefaire === undefined || evtARefaire.type === undefined || !evtARefaire.type.startsWith('Attaque')) {
sendPerso(chevalier, "s'y prend trop tard pour ignorer la douleur : la dernière action n'était pas une attaque");
return;
}
let aIgnore;
const evt = {
type: 'ignorer la douleur'
};
let PVid = token.get('bar1_link');
if (PVid === '') { //token non lié, effets seulement sur le token.
if (evtARefaire.affecte) {
let affecte = evtARefaire.affectes[token.id];
if (affecte && affecte.prev) {
let lastBar1 = affecte.prev.bar1_value;
let bar1 = parseInt(token.get('bar1_value'));
if (isNaN(lastBar1) || isNaN(bar1) || lastBar1 <= bar1) {
//On regarde la barre 2, peut-être qu'il s'agit de DM temporaires
let lastBar2 = affecte.prev.bar2_value;
let bar2 = parseInt(token.get('bar2_value'));
if (isNaN(lastBar2) || isNaN(bar2) || bar2 <= lastBar2) {
sendPerso(chevalier, "ne peut ignorer la douleur : il semble que la dernière attaque ne lui ait pas enlevé de PV");
return;
}
updateCurrentBar(chevalier, 2, lastBar2, evt);
setTokenAttr(chevalier, 'douleurIgnoree', bar2 - lastBar2, evt);
aIgnore = true;
} else {
updateCurrentBar(chevalier, 1, lastBar1, evt);
setTokenAttr(chevalier, 'douleurIgnoree', lastBar1 - bar1, evt);
aIgnore = true;
}
}
}
} else { // token lié, il faut regarder l'attribut
let attrPV = evtARefaire.attributes.find(function(attr) {
return (attr.attribute.id == PVid);
});
if (attrPV) {
let lastPV = attrPV.current;
let newPV = attrPV.attribute.get('current');
if (isNaN(lastPV) || isNaN(newPV) || lastPV <= newPV) {
sendPerso(chevalier, "ne peut ignorer la douleur : il semble que la dernière attaque ne lui ait pas enlevé de PV");
return;
}
updateCurrentBar(chevalier, 1, lastPV, evt);
setTokenAttr(chevalier, 'douleurIgnoree', lastPV - newPV, evt);
aIgnore = true;
} else { //peut-être qu'il s'agit de DM temporaires
PVid = token.get('bar2_link');
attrPV = evtARefaire.attributes.find(function(attr) {
return (attr.attribute.id == PVid);
});
if (attrPV) {
let lastDmTemp = attrPV.current;
let newDmTemp = attrPV.attribute.get('current');
if (isNaN(lastDmTemp) || isNaN(newDmTemp) || newDmTemp <= lastDmTemp) {
sendPerso(chevalier, "ne peut ignorer la douleur : il semble que la dernière attaque ne lui ait pas augmenté les DM temporaires");
return;
}
updateCurrentBar(chevalier, 2, lastDmTemp, evt);
setTokenAttr(chevalier, 'douleurIgnoree', newDmTemp - lastDmTemp, evt);
aIgnore = true;
}
}
}
if (aIgnore) {
sendPerso(chevalier, " ignore la douleur de la dernière attaque");
addEvent(evt);
} else {
sendPerso(chevalier, "ne peut ignorer la douleur : il semble que la dernière attaque ne l'ait pas affecté");
}
});
}, options);
}
function fortifiant(msg) {
const options = parseOptions(msg);
if (options === undefined) return;
const cmd = options.cmd;
if (cmd === undefined || cmd.length < 2) {
error("La fonction !cof-fortifiant attend en argument le rang dans la Voie des élixirs du créateur", cmd);
return;
}
const rang = parseInt(cmd[1]);
if (isNaN(rang) || rang < 1) {
error("Rang du fortifiant incorrect", cmd);
return;
}
const evt = {
type: 'fortifiant',
attributes: []
};
addEvent(evt);
getSelected(msg, function(selection) {
iterSelected(selection, function(beneficiaire) {
if (limiteRessources(beneficiaire, options, 'elixir_fortifiant', "boire un fortifiant", evt)) return;
const soins = rollDePlus(4, {
bonus: rang
});
sendPerso(beneficiaire, " boit un fortifiant");
soigneToken(beneficiaire, soins.val, evt, function(soinsEffectifs) {
let msgSoins = "et est soigné de ";
if (soinsEffectifs == soins.val) msgSoins += soins.roll + " PV";
else msgSoins += soinsEffectifs + " PV (le jet était " + soins.roll + ")";
sendPerso(beneficiaire, msgSoins);
});
// Finalement on met l'effet fortifie
setTokenAttr(beneficiaire, 'fortifie', rang + 1, evt);
});
}, options);
}
//Appliquer une huile instable sur l'arme de la cible
// Par défaut, c'est l'arme en main de la cible
// TODO: le faire pour les projectiles
// !cof-huile-instable @{target|token_id}
function huileInstable(msg) {
const options = parseOptions(msg);
if (options === undefined) return;
const cmd = options.cmd;
if (cmd === undefined || cmd.length < 2) {
error("La fonction !cof-huile-instable attend en argument la personne dont il faut enflammer l'arme", cmd);
return;
}
let perso = persoOfId(cmd[1], cmd[1]);
if (perso === undefined) {
sendPlayer(msg, "Cible pour appliquer l'huile instable incorrecte");
return;
}
let arme = armesEnMain(perso);
if (arme === undefined) {
sendPerso(perso, "Doit tenir son arme en main pour qu'on puisse appliquer l'huile instable");
return;
}
let playerId = getPlayerIdFromMsg(msg);
let effet = 'armeEnflammee(' + arme.label + ')';
let mEffet = messageEffetTemp.armeEnflammee;
let intel = 0;
if (options.lanceur) {
intel = modCarac(options.lanceur, 'intelligence');
}
let duree = (5 + intel) * 6;
effetTemporaire(playerId, [perso], effet, mEffet, duree, options);
}
function lancerSort(msg) {
const options = parseOptions(msg);
if (options === undefined) return;
const cmd = options.cmd;
if (cmd === undefined) return;
if (options.messages === undefined) options.messages = [];
if (cmd.length > 1) options.messages.unshift(cmd.slice(1).join(' '));
if (options.messages.length < 1) {
options.messages.push("lance un sort");
}
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
if (options.lanceur) {
selected = [{
_id: options.lanceur.token.id
}];
} else {
error("Pas de token sélectionée pour !cof-lancer-sort", cmd);
return;
}
}
const evt = {
type: "lancement de sort"
};
addEvent(evt);
if (options.lanceur) {
let lanceur = options.lanceur;
if (options.tempeteDeMana) {
if (options.tempeteDeMana.cout === 0) {
//On demande de préciser les options
let optMana = {
mana: options.mana,
dm: false,
soins: false,
duree: true,
portee: true,
rang: options.rang,
};
setTempeteDeMana(playerId, lanceur, msg.content, optMana);
return;
} else {
if (options.rang && options.tempeteDeMana.cout > options.rang) {
sendPerso(lanceur, "Attention, le coût de la tempête de mana (" + options.tempeteDeMana.cout + ") est supérieur au rang du sort");
}
}
}
if (limiteRessources(lanceur, options, undefined, "lancer un sort", evt)) return;
}
if (options.son) playSound(options.son);
let extraImg = afficheOptionImage(options);
options.messages[0] += extraImg;
iterSelected(selected, function(cible) {
if (!options.lanceur) {
if (options.tempeteDeMana) {
if (options.tempeteDeMana.cout === 0) {
//On demande de préciser les options
const optMana = {
mana: options.mana,
dm: false,
soins: false,
duree: true,
portee: true,
rang: options.rang,
};
setTempeteDeMana(playerId, cible, msg.content, optMana);
return;
} else {
if (options.rang && options.tempeteDeMana.cout > options.rang) {
sendPerso(cible, "Attention, le coût de la tempête de mana (" + options.tempeteDeMana.cout + ") est supérieur au rang du sort");
}
}
}
if (limiteRessources(cible, options, undefined, "lancer un sort", evt)) return;
}
if (options.targetFx) {
spawnFx(cible.token.get('left'), cible.token.get('top'), options.targetFx, cible.token.get('pageid'));
}
effetsSpeciaux(options.lanceur, cible, options);
options.messages.forEach(function(m) {
sendPerso(cible, m, options.secret);
});
if (options.messagesMJ) {
options.messagesMJ.forEach(function(m) {
sendChar(cible.charId, '/w gm ' + m);
});
}
});
});
}
//Est-ce encore utile ? TODO
function emulerAs(msg) {
let cmd = msg.content.split(' ');
if (cmd.length < 2) {
error("Il manque le nom du personnage pour !cof-as", msg.content);
return;
}
cmd.shift();
let nomPerso = cmd.shift();
if (nomPerso.charAt(0) == '"') {
nomPerso = nomPerso.substring(1);
let inComma = cmd.length;
while (inComma) {
nomPerso += ' ' + cmd.shift();
inComma--;
if (nomPerso.endsWith('"')) {
nomPerso = nomPerso.substr(0, nomPerso.length - 1);
inComma = 0;
}
}
}
sendChat(nomPerso, cmd.join(' '));
}
// Renvoie la durée mise à jour ou undefined si l'action n'est pas possible
function lancerMurDeForce(lanceur, playerId, duree, msg, typeMur, evt, options) {
if (options.tempeteDeMana) {
if (options.tempeteDeMana.cout === 0) {
//On demande de préciser les options
let optMana = {
mana: options.mana,
dm: false,
soins: false,
duree: true,
portee: true,
rang: options.rang,
};
setTempeteDeMana(playerId, lanceur, msg.content, optMana);
return;
} else {
if (options.rang && options.tempeteDeMana.cout > options.rang) {
sendPerso(lanceur, "Attention, le coût de la tempête de mana (" + options.tempeteDeMana.cout + ") est supérieur au rang du sort");
}
}
}
if (limiteRessources(lanceur, options, 'murDeForce', 'lancer un mur de ' + typeMur, evt)) return;
if (options.son) playSound(options.son);
whisperChar(lanceur.charId, "lance un sort de mur de " + typeMur);
if (!duree) {
switch (typeMur) {
case 'force':
duree = 5 + modCarac(lanceur, 'charisme');
break;
case 'vent':
duree = 5 + modCarac(lanceur, 'intelligence');
break;
default:
duree = 1;
}
}
return duree;
}
//!cof-mur-de-force [opt] [duree]
// opt peut être mur, noImage ou vent
// On peut changer la taille du mur avec l'option --portee.
function murDeForce(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) return;
let sphere = true;
let imageSphere = stateCOF.options.images.val.image_mur_de_force.val;
let typeMur = 'force';
if (cmd.length > 1) {
switch (cmd[1]) {
case 'mur':
sphere = false;
break;
case 'noImage':
imageSphere = undefined;
break;
case 'vent':
typeMur = 'vent';
imageSphere = stateCOF.options.images.val.image_mur_de_vent.val;
break;
default:
imageSphere = cmd[1].replace(':', ':');
}
}
let duree;
if (cmd.length > 2) {
duree = parseInt(cmd[2]);
if (isNaN(duree) || duree < 1) {
error("Le deuxième argument de !cof-mur-de-force doit être une durée", cmd);
return;
}
}
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
sendPlayer(msg, "Aucun personnage sélectionné pour lancer le mur de " + typeMur, playerId);
return;
}
const evt = {
type: "Mur de " + typeMur
};
addEvent(evt);
let token;
let pageId;
if (options.lanceur) {
duree = lancerMurDeForce(options.lanceur, playerId, duree, msg, typeMur, evt, options);
if (!duree) return;
token = options.lanceur.token;
pageId = token.get('pageid');
}
initiative(selected, evt);
iterSelected(selected, function(cible) {
if (!options.lanceur) {
let lanceur = cible;
duree = lancerMurDeForce(lanceur, playerId, duree, msg, typeMur, evt, options);
if (!duree) return;
token = lanceur.token;
pageId = token.get('pageid');
}
if (sphere) {
let scale = computeScale(pageId);
let diametre = 6;
if (typeMur == 'vent') diametre = 10;
if (options.portee) diametre = 2 * options.portee;
diametre = PIX_PER_UNIT * (diametre / scale);
if (options.puissantPortee || options.tempeteDeManaPortee) diametre += diametre;
if (options.tempeteDeManaIntense)
diametre *= (1 + options.tempeteDeManaIntense);
let imageFields = {
_pageid: pageId,
imgsrc: imageSphere,
represents: '',
left: token.get('left'),
top: token.get('top'),
width: diametre,
height: diametre,
layer: 'map',
name: "Mur de " + typeMur,
isdrawing: true,
};
let newImage = createObj('graphic', imageFields);
if (newImage) {
evt.tokens = [newImage];
toFront(newImage);
setTokenAttr(cible, 'murDeForceId', newImage.id, evt);
if (options.puissantDuree || options.tempeteDeManaDuree) duree += duree;
setAttrDuree(cible, 'murDeForce', duree, evt);
} else {
error("Impossible de créer l'image " + options.image, imageFields);
}
} else {
sendPlayerAndGM(msg, playerId, "placer l'image du mur sur la carte");
}
});
});
}
function tokensEnCombat() {
let cmp = Campaign();
let turnOrder = cmp.get('turnorder');
if (turnOrder === '') return []; // nothing in the turn order
turnOrder = JSON.parse(turnOrder);
if (turnOrder.length === 0) return [];
let tokens = [];
turnOrder.forEach(function(a) {
if (a.id == -1) return;
tokens.push({
_id: a.id
});
});
return tokens;
}
function devientCapitaine(msg) {
let cmd = msg.content.split(' ');
cmd = cmd.filter(function(c) {
return c !== '';
});
if (cmd.length < 2) {
error("La fonction !cof-capitaine attend en argument l'id du capitaine", cmd);
return;
}
const evt = {
type: 'Capitaine'
};
let remove;
let capitaine;
let nomCapitaine;
let bonus = 2;
let titre = 'capitaine';
if (cmd[1] == '--aucun') {
remove = true;
} else {
capitaine = persoOfId(cmd[1], cmd[1]);
if (capitaine === undefined) {
error("Le premier argument de !cof-capitaine doit être un token", cmd[1]);
return;
}
setState(capitaine, 'chef', true, evt);
nomCapitaine = nomPerso(capitaine);
if (cmd.length > 2 && !cmd[2].startsWith('--')) {
bonus = parseInt(cmd[2]);
if (isNaN(bonus) || bonus < 0) {
error("Le bonus de capitaine (second argument) doit être un nombre positif", cmd);
return;
}
if (bonus === 0) remove = true;
if (bonus > 2) titre = 'commandant';
}
}
getSelected(msg, function(selected) {
if (selected.length === 0) {
error("Pas de token sélectionné pour !cof-capitaine");
return;
}
iterSelected(selected, function(perso) {
let token = perso.token;
if (remove) {
removeCharAttr(perso.charId, 'capitaine', evt);
removeCharAttr(perso.charId, 'capitaineActif', evt);
sendChat('COF', "/w GM " + nomPerso(perso) + " n'a plus de capitaine");
} else {
if (token.id == capitaine.token.id) return;
setTokenAttr(perso, 'capitaine', idName(capitaine), evt, {
maxVal: bonus,
charAttr: true
});
sendChat('COF', "/w GM " + nomCapitaine + " est " + onGenre(capitaine, "le ", "la ") + titre + " de " + nomPerso(perso));
}
});
addEvent(evt);
});
}
function distribuerBaies(msg) {
if (msg.selected === undefined || msg.selected.length != 1) {
error("Pour utiliser !cof-distribuer-baies, il faut sélectionner un token", msg);
return;
}
let druide = persoOfId(msg.selected[0]._id);
if (druide === undefined) {
error("Erreur de sélection dans !cof-distribuer-baies", msg.selected);
return;
}
let niveau = ficheAttributeAsInt(druide, 'niveau', 1);
const evt = {
type: "Distribution de baies magiques"
};
let action = "Distribue des baies";
let mangerBaie = "!cof-consommer-baie " + niveau + " --limiteParJour 1 baieMagique";
getSelected(msg, function(selected, playerId) {
const display = startFramedDisplay(playerId, action, druide);
iterSelected(selected, function(perso) {
let nom = nomPerso(perso);
ajouterConsommable(perso, 'Baie magique', 1, mangerBaie, evt);
let line = nom + " reçoit une baie";
if (perso.token.id == druide.token.id)
line = nom + " en garde une pour " + onGenre(druide, "lui", "elle");
addLineToFramedDisplay(display, line);
});
addEvent(evt);
sendFramedDisplay(display);
}, {
lanceur: druide
}); //fin du getSelected
}
//!cof-consommer-baie niveau
function consommerBaie(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd.length < 2) {
error("Il faut un argument à !cof-consommer-baie", cmd);
return;
}
let baie = parseInt(cmd[1]);
if (isNaN(baie) || baie < 0) {
error("L'argument de !cof-consommer-baie doit être un nombre positif", cmd);
return;
}
getSelected(msg, function(selected, playerId) {
if (selected === undefined) {
sendPlayer(msg, "Pas de token sélectionné pour !cof-consommer-baie", playerId);
return;
}
const evt = {
type: "consommer une baie"
};
addEvent(evt);
iterSelected(selected, function(perso) {
if (limiteRessources(perso, options, 'baieMagique', "a déjà mangé une baie aujourd'hui. Pas d'effet.", evt)) return;
let soins = rollDePlus(6, {
bonus: baie
});
soigneToken(perso, soins.val, evt, function(soinsEffectifs) {
let msgSoins =
"mange une baie magique. " +
onGenre(perso, "Il est rassasié", "Elle est rassasiée") +
" et récupère ";
if (soinsEffectifs == soins.val) msgSoins += soins.roll + " points de vie";
else msgSoins += soinsEffectifs + " PV (le jet était " + soins.roll + ")";
sendPerso(perso, msgSoins);
},
function() {
sendPerso(perso, "mange une baie magique. " + onGenre(perso, "Il", "Elle") + " se sent rassasié" + onGenre(perso, '', 'e') + '.');
});
});
}); //fin de getSelected
}
function replaceInline(msg) {
if (msg.inlinerolls) {
msg.content = _.chain(msg.inlinerolls)
.reduce(function(m, v, k) {
m['$[[' + k + ']]'] = v.results.total || 0;
return m;
}, {})
.reduce(function(m, v, k) {
return m.replace(k, v);
}, msg.content)
.value();
}
}
/* Quand on protège un allié, on stocke l'idName dans un attribut 'attributDeComat_protegerUnAllie', et pour ce token, on met un
* attribut 'protegePar_nom' où nom est le nom du token protecteur, et qui contient l'idName du protecteur
* Ces attributs disparaissent à la fin des combats */
function protegerUnAllie(msg) {
let args = msg.content.split(' ');
if (args.length < 3) {
error("Pas assez d'arguments pour !cof-proteger-un-allie: " + msg.content, args);
return;
}
const protecteur = persoOfId(args[1], args[1]);
if (protecteur === undefined) {
error("Le premier argument n'est pas un token valide", args[1]);
return;
}
const nameProtecteur = nomPerso(protecteur);
let tokenProtecteur = protecteur.token;
let pageId = tokenProtecteur.get('pageid');
let target = persoOfId(args[2], args[2], pageId);
if (target === undefined) {
error("Le deuxième argument n'est pas un token valide: " + msg.content, args[2]);
return;
}
let tokenTarget = target.token;
if (tokenTarget.id == tokenProtecteur.id) {
sendPerso(protecteur, "ne peut pas se protéger i" + onGenre(protecteur, 'lui', 'elle') + "-même");
return;
}
const nameTarget = nomPerso(target);
const evt = {
type: "Protéger un allié"
};
let attrsProtecteur = tokenAttribute(protecteur, 'attributDeCombat_protegerUnAllie');
let protegePar = 'protegePar_' + nameProtecteur;
let other;
if (attrsProtecteur.length > 0) { //On protège déjà quelqu'un
let previousTarget =
persoOfIdName(attrsProtecteur[0].get('current'), pageId);
if (previousTarget) {
if (previousTarget.token.id == tokenTarget.id) {
sendPerso(protecteur, "protège déjà " + nameTarget);
return;
}
if (predicateAsBool(protecteur, 'attributDeCombat_protegerUnAllieAvance')) {
let previousTarget2 =
persoOfIdName(attrsProtecteur[0].get('max'), pageId);
if (previousTarget2) {
if (previousTarget2.token.id == tokenTarget.id) {
sendPerso(protecteur, "protège déjà " + nameTarget);
return;
}
removeTokenAttr(previousTarget2, protegePar, evt, {
msg: "n'est plus protégé par " + nameProtecteur
});
} else {
other = attrsProtecteur[0].get('current');
}
}
removeTokenAttr(previousTarget, protegePar, evt, {
msg: "n'est plus protégé par " + nameProtecteur
});
}
}
let distTargetProtecteur = distanceCombat(target.token, protecteur.token, pageId);
if (distTargetProtecteur > 0) {
sendPerso(protecteur, "est trop loin de " +
nameTarget + " pour le protéger");
return;
}
if (ficheAttributeAsInt(protecteur, 'defbouclieron', 0) === 0) {
let sujet = onGenre(protecteur, 'il', 'elle');
sendPerso(protecteur, "ne porte pas son bouclier, " + sujet +
" ne peut pas proteger " + nameTarget);
return;
}
let opt = {
maxVal: ''
};
if (other === undefined) {
setTokenAttr(protecteur, 'attributDeCombat_protegerUnAllie', idName(target), evt, opt);
} else {
opt.maxVal = idName(target);
setTokenAttr(protecteur, 'attributDeCombat_protegerUnAllie', other, evt, opt);
}
setTokenAttr(target, protegePar, idName(protecteur), evt);
sendPerso(protecteur, "protège " + nameTarget);
addEvent(evt);
}
function actionDefensive(msg) {
let combat = stateCOF.combat;
if (!combat) {
sendPlayer(msg, "Il faut entrer en combat pour se défendre");
return;
}
let cmd = msg.content.split(' ');
let def = 2; //pour une défense simple
let defMsg = "préfère se défendre pendant ce tour";
if (cmd.length > 1) {
switch (cmd[1]) {
case 'totale':
def = 4;
defMsg = "se consacre entièrement à sa défense pendant ce tour";
break;
case 'simple':
def = 2;
break;
default:
error("Argument de !cof-action-defensive non reconnu", cmd);
}
}
const evt = {
type: "action défensive"
};
getSelected(msg, function(selected) {
initiative(selected, evt);
iterSelected(selected, function(perso) {
setTokenAttr(perso, 'defenseTotale', def, evt, {
msg: defMsg,
maxVal: combat.tour
});
});
addEvent(evt);
});
}
function strangulation(msg) {
const args = msg.content.split(' ');
if (args.length < 3) {
error("Pas assez d'arguments pour !cof-strangulation: " + msg.content, args);
return;
}
const necromancien = persoOfId(args[1], args[1]);
if (necromancien === undefined) {
error("Le premier argument n'est pas un token", args[1]);
return;
}
let pageId = necromancien.token.get('pageid');
const target = persoOfId(args[2], args[2], pageId);
if (target === undefined) {
error("Le deuxième argument n'est pas un token valide: " + msg.content, args[2]);
return;
}
let name2 = nomPerso(target);
if (!attributeAsBool(target, 'strangulation')) {
sendPerso(necromancien, "ne peut pas maintenir la strangulation. Il faut (re)lancer le sort");
return;
}
const evt = {
type: "Strangulation"
};
let dureeStrang = tokenAttribute(target, 'dureeStrangulation');
let nouvelleDuree = 1;
if (dureeStrang.length > 0) {
nouvelleDuree = parseInt(dureeStrang[0].get('current'));
if (isNaN(nouvelleDuree)) {
log("Durée de strangulation n'est pas un nombre");
log(dureeStrang);
nouvelleDuree = 1;
} else nouvelleDuree++;
}
setTokenAttr(target, 'dureeStrangulation', nouvelleDuree, evt, {
maxVal: true
});
let deStrang = 6;
if (msg.content.includes(' --puissant')) deStrang = 8;
let dmgExpr = "[[1d" + deStrang + " ";
let modInt = modCarac(necromancien, 'intelligence');
if (modInt > 0) dmgExpr += "+" + modInt;
else if (modInt < 0) dmgExpr += modInt;
dmgExpr += "]]";
sendChat('', dmgExpr, function(res) {
const dmg = {
type: 'magique',
total: res[0].inlinerolls[0].results.total,
display: buildinline(res[0].inlinerolls[0], 'normal', true),
};
dealDamage(target, dmg, [], evt, false, {
attaquant: necromancien
}, undefined,
function(dmgDisplay, dmg) {
sendPerso(necromancien, "maintient sa strangulation sur " + name2 + ". Dommages : " + dmgDisplay);
addEvent(evt);
});
});
}
function ombreMortelle(msg) {
const options = parseOptions(msg);
if (options === undefined) return;
const cmd = options.cmd;
if (cmd === undefined || cmd.length < 4) {
error("Pas assez d'arguments pour " + cmd[0], cmd);
return;
}
const lanceur = persoOfId(cmd[1], cmd[1]);
if (lanceur === undefined) {
error("Le premier argument n'est pas un token valide", cmd[1]);
return;
}
const pageId = options.pageId || lanceur.token.get('pageid');
const cible = persoOfId(cmd[2], cmd[2], pageId);
if (cible === undefined) {
error("La cible n'est pas un token valide", cmd[2]);
return;
}
let duree = parseInt(cmd[3]);
if (isNaN(duree) || duree <= 0) {
error("La durée doit être un nombre positif", cmd);
return;
}
let image = options.image || stateCOF.options.images.val.image_ombre.val;
if (options.portee !== undefined) {
let distance = distanceCombat(lanceur.token, cible.token, pageId);
if (distance > options.portee) {
sendPerso(lanceur, "est trop loind de " + nomPerso(cible) +
" pour animer son ombre");
return;
}
}
const evt = {
type: "Ombre mortelle"
};
let msgRes = "invoquer une ombre mortelle";
if (limiteRessources(lanceur, options, "Ombre_mortelle", msgRes, evt)) return;
copieToken(cible, image, stateCOF.options.images.val.image_ombre.val, "Ombre de " + nomPerso(cible), 'ombreMortelle', duree, pageId, evt);
let msgOmbre = "anime l'ombre de " + cible.tokName + ". Celle-ci commence à attaquer " + cible.tokName + " !";
if (options.secret) whisperChar(lanceur.charId, msgOmbre);
else sendPerso(lanceur, msgOmbre);
addEvent(evt);
}
//renvoie l'attribut de l'effet temporaire créé
function copieToken(cible, image1, image2, nom, effet, duree, pageId, evt) {
let pv = parseInt(cible.token.get('bar1_value'));
if (isNaN(pv)) {
error("Token avec des PV qui ne sont pas un nombre", cible.token);
return;
}
if (pv > 1) pv = Math.floor(pv / 2);
let pvMax = parseInt(cible.token.get('bar1_max'));
if (isNaN(pvMax)) {
error("Token avec des PV max qui ne sont pas un nombre", cible.token);
return;
}
if (pvMax > 1) pvMax = Math.floor(pvMax / 2);
let tokenFields = {
_pageid: pageId,
imgsrc: image1,
represents: cible.charId,
left: cible.token.get('left') + 60,
top: cible.token.get('top'),
width: cible.token.get('width'),
height: cible.token.get('height'),
rotation: cible.token.get('rotation'),
layer: 'objects',
name: nom,
bar1_value: pv,
bar1_max: pvMax,
bar2_value: cible.token.get('bar2_value'),
bar2_max: cible.token.get('bar2_max'),
bar3_value: cible.token.get('bar3_value'),
bar3_max: cible.token.get('bar3_max'),
showname: true,
showplayers_name: true,
showplayers_bar1: cible.token.get('showplayers_bar1'),
};
let newToken;
if (image1) newToken = createObj('graphic', tokenFields);
if (newToken === undefined) {
tokenFields.imgsrc = normalizeTokenImg(cible.token.get('imgsrc'));
newToken = createObj('graphic', tokenFields);
if (newToken === undefined) {
log(tokenFields.imgsrc);
if (image2 && image2 != image1) {
tokenFields.imgsrc = image2;
newToken = createObj('graphic', tokenFields);
}
if (newToken === undefined) {
error("L'image du token sélectionné n'a pas été uploadé, et l'image par défaut n'est pas correcte. Impossible de créer un token.", tokenFields);
return;
}
}
}
evt.tokens = evt.tokens || [];
evt.tokens.push(newToken);
let perso = {
token: newToken,
charId: cible.charId
};
let attr = setAttrDuree(perso, effet, duree, evt);
initPerso(perso, evt);
return attr;
}
//retourne true si le joueur est effectivement déplacé
function movePlayerToPage(pid, oldPageId, newPageId) {
if (getObj('player', pid) === undefined) return;
const c = Campaign();
let playerPages = c.get('playerspecificpages');
const playersMainPage = c.get('playerpageid');
if (!playerPages) playerPages = {};
if ((playerPages[pid] && playerPages[pid] == oldPageId)) {
if (playersMainPage == newPageId) {
c.set('playerspecificpages', false);
if (_.size(playerPages) > 1) {
delete playerPages[pid];
c.set('playerspecificpages', playerPages);
}
} else {
playerPages[pid] = newPageId;
c.set('playerspecificpages', false);
c.set('playerspecificpages', playerPages);
}
} else if ((!playerPages[pid] && playersMainPage == oldPageId)) {
playerPages[pid] = newPageId;
let allPlayers = findObjs({
_type: 'player'
});
let allOnNewPage = allPlayers.every(function(p) {
if (playerIsGM(p.id)) return true;
return playerPages[p.id] == newPageId;
});
c.set('playerspecificpages', false);
if (allOnNewPage) {
Campaign().set('playerpageid', newPageId);
} else {
c.set('playerspecificpages', playerPages);
}
}
}
function findEsc(escaliers, escName, i) {
let fullEscName = escName + labelsEscalier[i];
let sortieEscalier = escaliers.find(function(esc) {
return esc.get('name') == fullEscName;
});
if (sortieEscalier === undefined && i > 0) return findEsc(escName, i - 1);
return sortieEscalier;
}
//Attention : ne tient pas compte de la rotation !
function intersection(pos1, size1, pos2, size2) {
if (pos1 == pos2) return true;
if (pos1 < pos2) return ((pos1 + size1 / 2) >= pos2 - size2 / 2);
return ((pos2 + size2 / 2) >= pos1 - size1 / 2);
}
const labelsEscalier = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"];
function escalier(msg) {
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
sendPlayer(msg, "Pas de sélection de token pour !cof-escalier", playerId);
log("!cof-escalier requiert de sélectionner des tokens");
return;
}
let pageId = getObj('graphic', selected[0]._id).get('pageid');
let escaliers = findObjs({
_type: 'graphic',
_pageid: pageId,
layer: 'gmlayer'
});
if (escaliers.length === 0) {
sendPlayer(msg, "Pas de token dans le layer GM", playerId);
return;
}
let tmaps; //Les passages entre les maps.
let versLeHaut = true;
let loop = true;
if (msg.content) {
if (msg.content.includes(' bas')) {
versLeHaut = false;
loop = false;
} else if (msg.content.includes(' haut')) {
versLeHaut = true;
loop = false;
}
}
iterSelected(selected, function(perso) {
let token = perso.token;
let posX = token.get('left');
let sizeX = token.get('width');
let posY = token.get('top');
let sizeY = token.get('height');
let sortieEscalier;
escaliers.forEach(function(esc) {
if (sortieEscalier) return;
if (intersection(posX, sizeX, esc.get('left'), esc.get('width')) &&
intersection(posY, sizeY, esc.get('top'), esc.get('height'))) {
let escName; //Contiendra le nom de l'escalier vers lequel aller
//On regarde d'abord le gmnote
let gmNotes = esc.get('gmnotes');
try {
gmNotes = _.unescape(decodeURIComponent(gmNotes)).replace(' ', ' ');
gmNotes = linesOfNote(gmNotes);
gmNotes.find(function(l) {
if (versLeHaut) {
if (l.startsWith('monte:')) {
escName = l.substring(6);
return true;
}
if (l.startsWith('monter:')) {
escName = l.substring(7);
return true;
}
if (l.startsWith('bas:')) {
escName = l.substring(4);
return true;
}
return false;
} else {
if (l.startsWith('descend:')) {
escName = l.substring(8);
return true;
}
if (l.startsWith('descendre:')) {
escName = l.substring(10);
return true;
}
if (l.startsWith('haut:')) {
escName = l.substring(5);
return true;
}
return false;
}
return false;
});
} catch (uriError) {
log("Erreur de décodage URI dans la note GM de " + esc.get('name') + " : " + gmNotes);
}
let i; //index de label si on n'utilise pas gmnote
if (escName === undefined) {
//Si on n'a pas trouvé, on regarde le nom
escName = esc.get('name');
let l = escName.length;
if (l > 1) {
let label = escName.substr(l - 1, 1);
escName = escName.substr(0, l - 1);
i = labelsEscalier.indexOf(label);
if (versLeHaut) {
if (i == 11) {
if (loop) escName += labelsEscalier[0];
} else escName += labelsEscalier[i + 1];
} else {
if (i === 0) {
if (loop) escName += labelsEscalier[11];
} else escName += labelsEscalier[i - 1];
}
}
}
if (!escName) return;
//Ensuite on cherche l'escalier de nom escName
let escs = escaliers;
if (escName.startsWith('tmap_')) {
if (!tmaps) {
tmaps = findObjs({
_type: 'graphic',
layer: 'gmlayer'
});
tmaps = tmaps.filter(function(e) {
return e.get('name').startsWith('tmap_');
});
}
escs = tmaps;
}
sortieEscalier = escs.find(function(esc2) {
return esc2.get('name') == escName;
});
if (sortieEscalier === undefined && i !== undefined && loop) {
if (i > 0) { //sortie par le plus petit
escName = escName.substr(-1) + 'A';
sortieEscalier = escs.find(function(esc2) {
return esc2.get('name') == escName;
});
} else {
sortieEscalier = findEsc(escs, escName.substr(-1), 10);
}
}
}
});
if (sortieEscalier) {
let left = sortieEscalier.get('left');
let top = sortieEscalier.get('top');
let newPageId = sortieEscalier.get('pageid');
//Déplacement du token
if (newPageId == pageId) {
token.set('left', left);
token.set('top', top);
} else {
//On change de carte, il faut donc copier le token
let tokenObj = JSON.parse(JSON.stringify(token));
tokenObj._pageid = newPageId;
//On met la taille du token à jour en fonction des échelles des cartes.
let ratio = computeScale(pageId) / computeScale(newPageId);
if (ratio < 0.9 || ratio > 1.1) {
if (ratio < 0.25) ratio = 0.25;
else if (ratio > 4) ratio = 4;
tokenObj.width *= ratio;
tokenObj.height *= ratio;
}
tokenObj.imgsrc = normalizeTokenImg(tokenObj.imgsrc);
tokenObj.left = left;
tokenObj.top = top;
let newToken = createObj('graphic', tokenObj);
if (newToken === undefined) {
error("Impossible de copier le token, et donc de faire le changement de carte", tokenObj);
return;
}
}
//On déplace ensuite le joueur.
let character = getObj('character', perso.charId);
if (character === undefined) return;
let charControlledby = character.get('controlledby');
if (charControlledby === '') {
//Seul le MJ contrôle le personnage
let players = findObjs({
_type: 'player',
online: true
});
let gm = players.find(function(p) {
return playerIsGM(p.id);
});
if (gm) {
if (newPageId != pageId) movePlayerToPage(gm.id, pageId, newPageId);
sendPing(left, top, newPageId, gm.id, true, gm.id);
}
} else {
charControlledby.split(",").forEach(function(pid) {
if (newPageId != pageId) movePlayerToPage(pid, pageId, newPageId);
sendPing(left, top, newPageId, pid, true, pid);
});
}
//Enfin, on efface le token de départ si on a changé de page
if (newPageId != pageId) token.remove();
return;
}
let err = nomPerso(perso) + " n'est pas sur un escalier";
if (!loop) {
if (versLeHaut) err += " qui monte";
else err += " qui descend";
}
sendPlayer(msg, err, playerId);
});
}); //fin getSelected
}
function defautDansLaCuirasse(msg) {
let args = msg.content.split(' ');
if (args.length < 3) {
error("Pas assez d'arguments pour !cof-defaut-dans-la-cuirasse", args);
return;
}
let tireur = persoOfId(args[1], args[1]);
if (tireur === undefined) {
error("Le premier argument n'est pas un token valide", args[1]);
return;
}
let pageId = tireur.token.get('pageid');
let cible = persoOfId(args[2], args[2], pageId);
if (cible === undefined) {
error("La cible n'est pas un token valide", args[2]);
return;
}
const evt = {
type: "Défaut dans la cuirasse"
};
setTokenAttr(cible, 'defautDansLaCuirasse_' + nomPerso(tireur), 2, evt);
sendPerso(tireur, "passe le tour à analyser les points faibles de " + nomPerso(cible));
addEvent(evt);
}
function postureDeCombat(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 1) {
error("Impossible de trouve la commande !", msg.content);
return;
}
if (cmd.length < 4) {
error("Pas assez d'arguments pour !cof-posture-de-combat", cmd);
return;
}
let bonus = parseInt(cmd[1]);
let attrDebuf = cmd[2];
if (attrDebuf != 'DEF' && attrDebuf != 'ATT' && attrDebuf != 'DM') {
error("L'attribut à débuffer pour la posture de combat est incorrect", cmd);
return;
}
let attrBuf = cmd[3];
if (attrBuf != 'DEF' && attrBuf != 'ATT' && attrBuf != 'DM') {
error("L'attribut à augmenter pour la posture de combat est incorrect", cmd);
return;
}
getSelected(msg, function(selected, playerId) {
if (isNaN(bonus) || bonus < 1) {
sendPlayer(msg, "choisir un bonus positif (pas " + cmd[1] + ") pour sa posture de combat", playerId);
return;
}
iterSelected(selected, function(guerrier) {
let rang = predicateAsInt(guerrier, "voieDuSoldat", 0);
if (rang > 0 && rang < bonus) {
sendPerso(guerrier, "ne peut choisir qu'un bonus inférieur à " + rang + " pour sa posture de combat");
return;
}
const evt = {
type: "Posture de combat"
};
if (limiteRessources(guerrier, options, 'postureDeCombat', "prendre une posture de combat", evt)) return;
if (attrBuf == attrDebuf) {
sendPerso(guerrier, "prend une posture de combat neutre");
removeTokenAttr(guerrier, 'postureDeCombat', evt);
addEvent(evt);
return;
}
msg = "prend une posture ";
switch (attrBuf) {
case 'DEF':
msg += "défensive";
break;
case 'ATT':
msg += "offensive";
break;
case 'DM':
msg += "puissante";
break;
default:
}
msg += " mais ";
switch (attrDebuf) {
case 'DEF':
msg += "risquée";
break;
case 'ATT':
msg += "moins précise";
break;
case 'DM':
msg += "moins puissante";
break;
default:
}
setTokenAttr(guerrier, 'postureDeCombat', bonus, evt, {
msg: msg,
maxVal: attrDebuf + "_" + attrBuf
});
addEvent(evt);
});
});
}
function attaqueAOutrance(msg) {
let args = msg.content.split(' ');
if (args.length < 2) {
error("Pas assez d'arguments pour !cof-attaque-a-outrance", args);
return;
}
let bonus = parseInt(args[1]);
if (bonus != 0 && bonus != 2 && bonus != 5) {
error("Le malus de DEF ne peut être que 0, 2 ou 5", args);
return;
}
getSelected(msg, function(selected) {
iterSelected(selected, function(guerrier) {
const evt = {
type: "Attaque à outrance"
};
if (bonus === 0) {
sendPerso(guerrier, "n'attaque plus à outrance");
removeTokenAttr(guerrier, 'attaqueAOutrance', evt);
addEvent(evt);
return;
}
msg = "attaque à outrance ";
switch (bonus) {
case 2:
msg += "(-2 DEF, +1D6 DM)";
break;
case 5:
msg += "(-5 DEF, +2D6 DM)";
break;
default:
}
setTokenAttr(guerrier, 'attaqueAOutrance', bonus, evt, {
msg: msg,
});
addEvent(evt);
});
});
}
function parseTourDeForce(msg) { // Deprecated
const options = parseOptions(msg);
const cmd = options.cmd;
if (cmd < 2) {
error("Il manque un argument à !cof-tour-de-force", cmd);
return;
}
let seuil = parseInt(cmd[1]);
getSelected(msg, function(selected, playerId) {
iterSelected(selected, function(barbare) {
if (isNaN(seuil)) {
sendPlayer(msg, "le seuil de difficulté du tour de force doit être un nombre", playerId);
return;
}
doTourDeForce(barbare, seuil, options);
});
});
}
function doTourDeForce(perso, seuil, options) { // Deprecated
let evt = {
type: "tourDeForce",
action: {
perso: perso,
seuil: seuil,
options: options
}
};
addEvent(evt);
let action = "Capacité : Tour de force";
let display = startFramedDisplay(options.playerId, action, perso);
let testId = 'tourDeForce';
options.bonus = 10;
testCaracteristique(perso, 'FOR', seuil, testId, options, evt,
function(tr) {
addLineToFramedDisplay(display, " Jet de force difficulté " + seuil);
let smsg = nomPerso(perso) + " fait " + tr.texte;
if (tr.reussite) {
smsg += " => réussite";
} else {
smsg += " => échec" + tr.rerolls;
}
addLineToFramedDisplay(display, smsg);
let d4 = options.rolls.tourDeForceDmg || rollDePlus(4);
evt.action.rolls.tourDeForceDmg = d4;
let r = {
total: d4.val,
type: 'normal',
display: d4.roll
};
let explications = [];
perso.ignoreTouteRD = true;
dealDamage(perso, r, [], evt, false, {}, explications,
function(dmgDisplay, dmg) {
let dmgMsg = "mais cela lui coûte " + dmgDisplay + " PV";
addLineToFramedDisplay(display, dmgMsg);
explications.forEach(function(expl) {
addLineToFramedDisplay(display, expl, 80);
});
sendFramedDisplay(display);
});
});
}
function removePreDmg(options, perso, champ, newVal) {
if (options.preDmg) {
if (options.preDmg[perso.token.id]) {
if (champ && options.preDmg[perso.token.id][champ]) {
if (newVal) options.preDmg[perso.token.id][champ] = newVal;
else delete options.preDmg[perso.token.id][champ];
}
if (champ === undefined || _.isEmpty(options.preDmg[perso.token.id]))
delete options.preDmg[perso.token.id];
}
if (_.isEmpty(options.preDmg)) delete options.preDmg;
}
}
//!cof-encaisser-un-coup, avec la personne qui encaisse sélectionnée
function doEncaisserUnCoup(msg) {
const optionsEncaisser = parseOptions(msg);
if (optionsEncaisser === undefined) return;
let cmd = optionsEncaisser.cmd;
let evt = lastEvent();
if (cmd !== undefined && cmd.length > 1) { //On relance pour un événement particulier
evt = findEvent(cmd[1]);
if (evt === undefined) {
error("L'action est trop ancienne ou a été annulée", cmd);
return;
}
}
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
error("Personne n'est sélectionné pour encaisser un coup", msg);
return;
}
if (evt === undefined) {
sendChat('', "Historique d'actions vide, pas d'action trouvée pour encaisser un coup");
return;
}
if (evt.type != 'Attaque' || evt.succes === false) {
sendChat('', "la dernière action n'est pas une attaque réussie, trop tard pour encaisser le coup d'une action précédente");
return;
}
let action = evt.action;
if (action.options.distance) {
sendChat('', "Impossible d'encaisser le dernier coup, ce n'était pas une attaque au contact");
return;
}
let toProceed;
iterSelected(selected, function(chevalier) {
if (!attributeAsBool(chevalier, 'encaisserUnCoup')) {
sendPerso(chevalier, "n'est pas placé " + eForFemale(chevalier) + " pour encaisser un coup");
return;
}
if (!peutController(msg, chevalier)) {
sendPlayer(msg, "pas le droit d'utiliser ce bouton", playerId);
return;
}
let cible = action.cibles.find(function(target) {
return (target.token.id === chevalier.token.id);
});
if (cible === undefined) {
sendPerso(chevalier, "n'est pas la cible de la dernière attaque");
return;
}
action.choices = action.choices || {};
action.choices[chevalier.token.id] = action.choices[chevalier.token.id] || {};
action.choices[chevalier.token.id].encaisserUnCoup = true;
toProceed = true;
}); //fin iterSelected
if (toProceed) {
let options = action.currentOptions || {};
options.rolls = action.rolls;
options.choices = action.choices;
resolvePreDmgOptions(action.attaquant, action.ciblesTouchees, action.echecCritique, action.attackLabel, action.weaponStats, action.attackd20roll, action.display, options, evt, action.explications, action.pageId, action.cibles);
}
}); //fin getSelected
}
function appliquerEncaisserUnCoup(cible, options, evt) {
removeTokenAttr(cible, 'encaisserUnCoup', evt);
cible.extraRD = defenseArmure(cible);
removePreDmg(options, cible, "encaisserUnCoup");
}
//!cof-devier-les-coups, avec la personne qui encaisse sélectionnée
function doDevierLesCoups(msg) {
let optionsDevier = parseOptions(msg);
if (optionsDevier === undefined) return;
let cmd = optionsDevier.cmd;
let evt = lastEvent();
if (cmd !== undefined && cmd.length > 1) { //On relance pour un événement particulier
evt = findEvent(cmd[1]);
if (evt === undefined) {
error("L'action est trop ancienne ou a été annulée", cmd);
return;
}
}
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
error("Personne n'est sélectionné pour dévier les coups", msg);
return;
}
if (evt === undefined) {
sendChat('', "Historique d'actions vide, pas d'action trouvée pour dévier les coups");
return;
}
if (evt.type != 'Attaque' || evt.succes === false) {
sendChat('', "la dernière action n'est pas une attaque réussie, trop tard pour dévier les coups d'une action précédente");
return;
}
const action = evt.action;
if (action.options.distance) {
sendChat('', "Impossible d'encaisser le dernier coup, ce n'était pas une attaque au contact");
return;
}
let toProceed;
iterSelected(selected, function(perso) {
let testDevierCoups = testLimiteUtilisationsCapa(perso, 'devierLesCoups', 'tour', "ne peut plus dévier les coups à ce tour-ci");
if (testDevierCoups === undefined) return;
if (!peutController(msg, perso)) {
sendPlayer(msg, "pas le droit d'utiliser ce bouton", playerId);
return;
}
let cible = action.cibles.find(function(target) {
return (target.token.id === perso.token.id);
});
if (cible === undefined) {
sendPerso(perso, "n'est pas la cible de la dernière attaque");
return;
}
action.choices = action.choices || {};
action.choices[perso.token.id] = action.choices[perso.token.id] || {};
action.choices[perso.token.id].devierLesCoups = testDevierCoups;
toProceed = true;
}); //fin iterSelected
if (toProceed) {
let options = action.currentOptions || {};
options.rolls = action.rolls;
options.choices = action.choices;
resolvePreDmgOptions(action.attaquant, action.ciblesTouchees, action.echecCritique, action.attackLabel, action.weaponStats, action.attackd20roll, action.display, options, evt, action.explications, action.pageId, action.cibles);
}
}); //fin getSelected
}
function appliquerDevierLesCoups(cible, test, options, evt) {
utiliseCapacite(cible, test, evt);
cible.extraRDBouclier = defenseBouclier(cible);
removePreDmg(options, cible, 'devierLesCoups');
}
//!cof-parade-projectiles
function doParadeProjectiles(msg) {
const optionsParade = parseOptions(msg);
if (optionsParade === undefined) return;
let cmd = optionsParade.cmd;
let evt = lastEvent();
if (cmd !== undefined && cmd.length > 1) { //On relance pour un événement particulier
evt = findEvent(cmd[1]);
if (evt === undefined) {
error("L'action est trop ancienne ou a été annulée", cmd);
return;
}
}
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
error("Personne n'est sélectionné pour parer le projectile", msg);
return;
}
if (evt === undefined) {
sendChat('', "Historique d'actions vide, pas d'action trouvée pour parer le projectile");
return;
}
if (evt.type != 'Attaque' || evt.succes === false) {
sendChat('', "la dernière action n'est pas une attaque réussie, trop tard pour parer le projectile d'une action précédente");
return;
}
let action = evt.action;
if (!action.options.distance) {
sendChat('', "Impossible de parer le projectile, ce n'était pas une attaque à distance");
return;
}
if (action.options.poudre) {
sendChat('', "Impossible de parer le projectile, il s'agit d'une arme à poudre");
return;
}
if (action.cibles.length > 1) {
sendChat('', "Impossible de parer un projectile qui touche plusieurs cibles");
return;
}
let toProceed;
iterSelected(selected, function(moine) {
let testParadeProjectiles = testLimiteUtilisationsCapa(moine, 'paradeDeProjectiles', 'tour', "ne peut plus parer de projectiles");
if (testParadeProjectiles === undefined) {
return;
}
if (!peutController(msg, moine)) {
sendPlayer(msg, "pas le droit d'utiliser ce bouton", playerId);
return;
}
let cible = action.cibles.find(function(target) {
return (target.token.id === moine.token.id);
});
if (cible === undefined) {
sendPerso(moine, "n'est pas la cible de la dernière attaque");
return;
}
action.choices = action.choices || {};
action.choices[moine.token.id] = action.choices[moine.token.id] || {};
action.choices[moine.token.id].paradeDeProjectiles = testParadeProjectiles;
toProceed = true;
}); //fin iterSelected
if (toProceed) {
let options = action.currentOptions || {};
options.rolls = action.rolls;
options.choices = action.choices;
resolvePreDmgOptions(action.attaquant, action.ciblesTouchees, action.echecCritique, action.attackLabel, action.weaponStats, action.attackd20roll, action.display, options, evt, action.explications, action.pageId, action.cibles);
}
}); //fin getSelected
}
function appliquerParadeProjectiles(cible, test, options, evt) {
utiliseCapacite(cible, test, evt);
options.preDmgAnnule = true;
removePreDmg(options, cible);
}
// asynchrone : on fait les jets du personnage en opposition
// options :
// - annule : si l'évitement réussi, annule pour tout le monde ?
// - arme : utiliser les bonus de l'arme en main
// - armeGauche : utiliser les bonus de l'arme en main gauche
// - attrAsBool : si on utilise un attribut, on le lit comme un booléen et non comme un nombre
// - bonusAttaque : bonus au jet d'attaque
// - bouclier: utiliser les bonus de bouclier
// - condition : une fonction qui prend en argument un perso. Si le résultat est false, l'évitement est impossible
// - critiqueDevientNormal : transforme un critique en normal
// - predicat : on utilise un prédicat et non un attribut. Peut être 'tour' ou 'combat'
// - protecteur: c'est un protecteur qui protège la cible (pas compatible avec predicate
function evitementGenerique(msg, verbe, attributeName, actionName, tente, msgDejaFait, carac, typeAttaque, msgReussite, opt) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 1) {
error("Impossible de trouve la commande !", msg.content);
return;
}
if (cmd.length < 2) {
error("Pas assez d'arguments pour " + cmd[0], cmd);
return;
}
let evt;
let chance;
if (cmd.length > 2) { //On relance pour un événement particulier
evt = findEvent(cmd[2]);
if (evt === undefined) {
error("L'action est trop ancienne ou a été annulée", cmd);
return;
}
chance = cmd.length > 3 && cmd[3] == 'chance';
} else {
evt = lastEvent();
}
if (evt === undefined) {
sendChat('', "Historique d'actions vide, pas d'action trouvée pour esquiver");
return;
}
if (evt.type != 'Attaque') {
sendChat('', "la dernière action n'est pas une attaque réussie, trop tard pour " + verbe + " l'attaque précédente");
return;
}
const perso = persoOfId(cmd[1]);
if (perso === undefined) {
error("Le premier argument de " + cmd[0] + " n'est pas un token de personnage", cmd);
return;
}
let action = evt.action;
let optionsAttaque = action.currentOptions;
let pageId = action.pageId;
let cible = action.cibles.find(function(target) {
return (target.token.id === perso.token.id);
});
if (cible === undefined) {
sendPerso(perso, "n'est pas la cible de la dernière attaque");
return;
}
let attributAVerifier = attributeName;
let persoAttribut = perso;
if (opt.protecteur) { // c'est un personnage tiers qui protège la cible
let protecteurAttrs = tokenAttribute(perso, attributeName + 'Valeur');
if (protecteurAttrs.length < 1) {
error("Erreur interne dans le bouton de protection, protecteur introuvable", cmd);
return;
}
persoAttribut = persoOfId(protecteurAttrs[0].get("current"));
attributAVerifier = attributeName + "Actif";
if (persoAttribut === undefined) {
error("Erreur interne dans le bouton de protection, protecteur introuvable", cmd);
return;
}
opt.protecteur = persoAttribut;
}
if (!peutController(msg, persoAttribut)) {
sendPlayer(msg, "pas le droit d'utiliser ce bouton");
return;
}
if (opt && opt.condition && !opt.condition(perso)) {
resolvePreDmgOptions(action.attaquant, action.ciblesTouchees, action.echecCritique, action.attackLabel, action.weaponStats, action.attackd20roll, action.display, optionsAttaque, evt, action.explications, pageId, action.cibles);
}
let jetAdversaire = cible.attackRoll;
if (jetAdversaire === undefined) {
error("Impossible de trouver le jet de l'attaquant", cible);
return;
}
let attribut;
let testPredicat;
if (opt && opt.predicat) {
testPredicat = testLimiteUtilisationsCapa(perso, attributeName, opt.predicat, msgDejaFait, "ne sait pas faire " + actionName);
if (testPredicat === undefined) return;
testPredicat.perso = perso;
} else if (attributAVerifier) {
attribut = tokenAttribute(persoAttribut, attributAVerifier);
if (attribut.length === 0) {
sendPerso(persoAttribut, "ne sait pas faire " + actionName);
return;
}
attribut = attribut[0];
if (opt && opt.attrAsBool) {
if (!attribut.get('current')) {
sendPerso(persoAttribut, msgDejaFait);
return;
}
} else {
let curAttribut = parseInt(attribut.get('current'));
if (isNaN(curAttribut)) {
error("Resource pour " + actionName + " mal formée", attribut);
return;
}
if (curAttribut < 1) {
sendPerso(persoAttribut, msgDejaFait);
return;
}
}
}
optionsAttaque.choices = action.choices || {};
optionsAttaque.choices[perso.token.id] = optionsAttaque.choices[perso.token.id] || {};
optionsAttaque.choices[perso.token.id].evitementGenerique =
optionsAttaque.choices[perso.token.id].evitementGenerique || [];
optionsAttaque.choices[perso.token.id].evitementGenerique.push({
jetAdversaire,
attribut,
testPredicat,
opt,
attributeName,
tente,
carac,
typeAttaque,
msgReussite
});
optionsAttaque.rolls = action.rolls;
resolvePreDmgOptions(action.attaquant, action.ciblesTouchees, action.echecCritique, action.attackLabel, action.weaponStats, action.attackd20roll, action.display, optionsAttaque, evt, action.explications, action.pageId, action.cibles);
}
function appliquerEvitementGenerique(cible, evitementGen, pageId, options, evt, callback) {
let action = evt.action;
let lanceur = cible;
let {
jetAdversaire,
attribut,
testPredicat,
opt,
attributeName,
tente,
carac,
typeAttaque,
msgReussite
} = evitementGen;
if (opt && opt.protecteur) {
lanceur = opt.protecteur;
}
if (attribut) {
if (opt && opt.attrAsBool) {
evt.deletedAttributes = evt.deletedAttributes || [];
evt.deletedAttributes.push(attribut);
attribut.remove();
} else {
let curAttribut = parseInt(attribut.get('current'));
evt.attributes = evt.attributes || [];
evt.attributes.push({
attribute: attribut,
current: curAttribut
});
attribut.set('current', curAttribut - 1);
}
} else if (testPredicat) {
utiliseCapacite(testPredicat.perso, testPredicat, evt);
} else if (attributeName !== undefined) { //ni attribut ni prédicat
error("On n'a ni attribut ni prédicat pour un évitement générique", evitementGen);
return;
}
let optDice;
if (typeAttaque == 'esquive') {
let nbDe = nbreDeTestCarac(carac, lanceur);
optDice = {
carac,
nbDe
};
}
let attackRollExpr = "[[" + computeDice(lanceur, optDice) + "cs20cf1]]";
sendChat('', attackRollExpr, function(res) {
let testId = attributeName + "_" + lanceur.token.id;
options.rolls = options.rolls || {};
let attackRoll = options.rolls[testId] || res[0].inlinerolls[0];
attackRoll.token = lanceur.token;
evt.action.rolls = evt.action.rolls || {};
evt.action.rolls[testId] = attackRoll;
let d20roll = attackRoll.results.total;
effetAuD20(lanceur, d20roll);
let msg = buildinline(attackRoll);
let attBonus = ficheAttributeAsInt(lanceur, 'niveau', 1);
if (estAffaibli(lanceur) && predicateAsBool(lanceur, 'insensibleAffaibli')) attBonus -= 2;
switch (typeAttaque) {
case 'distance':
attBonus += ficheAttributeAsInt(lanceur, 'ATKTIR_DIV', 0);
if (persoArran(lanceur)) {
attBonus += ficheAttributeAsInt(lanceur, 'mod_atktir', 0);
}
attBonus += modCarac(lanceur, carac);
break;
case 'magique':
attBonus += ficheAttributeAsInt(lanceur, 'ATKMAG_DIV', 0);
if (persoArran(lanceur)) {
attBonus += ficheAttributeAsInt(lanceur, 'mod_atkmag', 0);
attBonus += modCarac(lanceur, 'intelligence');
} else {
attBonus += modCarac(lanceur, carac);
}
attBonus += predicateAsInt(lanceur, 'bonusAttaqueMagique', 0);
break;
case 'contact':
attBonus += ficheAttributeAsInt(lanceur, 'ATKCAC_DIV', 0);
attBonus += modCarac(lanceur, carac);
break;
case 'esquive':
attBonus += predicateAsInt(lanceur, 'reflexesFelins', 0);
attBonus += predicateAsInt(lanceur, 'esquiveVoleur', 0);
attBonus += predicateAsInt(lanceur, 'esquive', 0);
attBonus += modCarac(lanceur, carac);
break;
default:
}
let weaponStats;
if (opt && opt.arme && lanceur.arme) {
weaponStats = lanceur.arme;
if (lanceur.arme.attSkillDiv) attBonus += weaponStats.attSkillDiv;
}
if (opt && opt.armeGauche && lanceur.armeGauche) {
weaponStats = lanceur.armeGauche;
if (lanceur.armeGauche.attSkillDiv) attBonus += weaponStats.attSkillDiv;
}
if (opt && opt.bouclier && ficheAttributeAsInt(lanceur, 'defbouclieron', 0)) {
attBonus += predicateAsInt(lanceur, 'bonusAttaqueBouclier', 0);
}
let totalEvitement = d20roll + attBonus;
if (attBonus > 0) msg += "+" + attBonus;
else if (attBonus < 0) msg += attBonus;
if (options.chanceRollId && options.chanceRollId[testId]) {
totalEvitement += options.chanceRollId[testId];
msg += "+" + options.chanceRollId[testId];
}
const optionsEvitement = {
displayName: true,
pasDeDmg: true
};
parseWeaponStatsOptions(lanceur, action.attaquant, weaponStats, undefined, optionsEvitement);
let attEvBonus =
bonusAttaqueA(lanceur, weaponStats, evt, cible.messages, optionsEvitement);
let bad = bonusAttaqueD(lanceur, action.attaquant, 0, pageId, evt, cible.messages, optionsEvitement);
attEvBonus += bad;
if (opt && opt.bonusAttaque) attEvBonus += opt.bonusAttaque;
if (attEvBonus > 0) msg += "+" + attEvBonus;
else if (attEvBonus < 0) msg += attEvBonus;
msg = nomPerso(lanceur) + " tente " + tente + ". " +
onGenre(lanceur, "Il", "Elle") + " fait " + msg;
let generalMsg = '';
if (totalEvitement < jetAdversaire) {
msg += " => Raté";
let pc = pointsDeChance(lanceur);
if (attackRoll.results.total != 1 && pc > 0) {
generalMsg += '
' +
boutonSimple("!cof-bouton-chance " + evt.id + " " + testId, "Chance") +
" (reste " + pc + " PC)";
}
if (stateCOF.combat && attributeAsBool(lanceur, 'runeForgesort_énergie') &&
attributeAsInt(lanceur, 'limiteParCombat_runeForgesort_énergie', 1) > 0 &&
(carac == 'force' || carac == 'constitution' || carac == 'dexterite')) {
generalMsg += '
' + boutonSimple("!cof-bouton-rune-energie " + evt.id + " " + testId, "Rune d'énergie");
}
if (stateCOF.combat && capaciteDisponible(lanceur, 'petitVeinard', 'combat')) {
generalMsg += '
' + boutonSimple("!cof-bouton-petit-veinard " + evt.id + " " + testId, "Petit veinard");
}
if (generalMsg === '') { //Ne retirer l'option que si aucun reroll possible
removePreDmg(options, cible, attributeName);
} else { //Sinon cacher le bouton mais laisser l'option reroll
removePreDmg(options, cible, attributeName, 'reroll');
}
} else { //Évitement réussi
if (opt && cible.critique && (opt.critiqueDevientNormal || (opt.critiqueAnnuleCritique && d20roll != 20))) {
cible.critique = false;
msg += " => Réussi, l'attaque fait des dégâts normaux";
removePreDmg(options, cible, attributeName);
} else {
cible.touche = false;
if (opt.annule) {
delete options.preDmg;
options.preDmgAnnule = true;
generalMsg += " => Réussi, " + msgReussite;
} else {
action.ciblesTouchees = action.ciblesTouchees.filter(function(c) {
return c.token.id != cible.token.id;
});
removePreDmg(options, cible);
msg += " => Réussi, " + msgReussite;
}
}
}
callback(msg, generalMsg);
});
}
//!cof-absorber-coup-au-bouclier id [evtid] [chance]
function absorberCoupAuBouclier(msg) {
let condition = function(guerrier) {
if (ficheAttributeAsInt(guerrier, 'defbouclieron', 0) != 1) {
sendPerso(guerrier, "ne porte pas son bouclier, il ne peut pas aborber de coup");
return false;
}
return true;
};
evitementGenerique(msg, "absorber un coup au bouclier", 'absorberUnCoup',
"d'absorption de coup au bouclier", "d'absorber un coup au bouclier",
" a déjà essayé d'absorber un coup au bouclier ce tour", 'force', 'contact', "le coup est absorbé !", {
attrAsBool: true,
bouclier: true,
condition,
});
}
//!cof-absorber-sort-au-bouclier id [evtid] [chance]
function absorberSortAuBouclier(msg) {
const condition = function(guerrier) {
if (ficheAttributeAsInt(guerrier, 'defbouclieron', 0) != 1) {
sendPerso(guerrier, "ne porte pas son bouclier, il ne peut pas aborber un sort");
return false;
}
return true;
};
evitementGenerique(msg, "absorber un sort au bouclier", 'absorberUnSort',
"d'absorption de sort au bouclier", "d'absorber un sort au bouclier",
" a déjà essayé d'absorber un sort au bouclier ce tour", 'sagesse', 'magique', "le sort est absorbé !", {
attrAsBool: true,
bouclier: true,
condition: condition
});
}
// asynchrone : on fait les jets du barbare en opposition
//!cof-resister-a-la-magie id [evtid] [chance]
function resisterALaMagie(msg) {
evitementGenerique(msg, 'résister à la magie', 'resistanceALaMagieBarbare',
'résistance à la magie', "de résister à la magie",
" a déjà essayé de résister à la magie ce tour",
'sagesse', 'magique', "il résiste à la magie !", {
predicat: 'tour'
});
}
// asynchrone : on fait les jets du magicien protecteur en opposition
//!cof-cercle-protection id [evtid] [chance]
function cercleDeProtection(msg) {
evitementGenerique(msg, 'activer le cercle de protection', 'cercleDeProtection',
'activation du cercle de protection', "de bloquer le sort avec le Cercle de Protection",
" a déjà active le Cercle de Protection ce tour",
'intelligence', 'magique', "le sort est annulé !", {
protecteur: true,
annule: true
});
}
// asynchrone : on fait les jets du barde en opposition
//!cof-esquive-acrobatique id [evtid] [chance]
function doEsquiveAcrobatique(msg) {
evitementGenerique(msg, 'esquiver', 'esquiveAcrobatique',
'esquive acrobatique', "une esquive acrobatique", " a déjà fait une esquive acrobatique ce tour", 'dexterite', 'distance', "l'attaque est esquivée !", {
critiqueDevientNormal: true,
predicat: 'tour'
});
}
function doEsquiveDeLaMagie(msg) {
evitementGenerique(msg, 'esquiver', undefined,
"esquive de la magie", "d'esquiver la magie", " a déjà fait esquivé la magie", 'dexterite', 'esquive', "l'attaque est esquivée !", {});
}
function doEsquiveMagistrale(msg) {
evitementGenerique(msg, 'esquiver', 'paradeMagistrale',
'esquive acrobatique', "une esquive acrobatique", " a déjà fait une parade magistrale ce tour", 'dexterite', 'distance', "l'attaque est esquivée !", {
bonusAttaque: -5,
critiqueDevientNormal: true,
predicat: 'tour'
});
}
function doParadeMagistrale(msg) {
evitementGenerique(msg, 'parer', 'paradeMagistrale',
'parade magistrale', "une parade magistrale", " a déjà fait une parade magistrale ce tour", 'dexterite', 'distance', "l'attaque est parée !", {
arme: true,
critiqueDevientNormal: true,
predicat: 'tour'
});
}
//!cof-parade-au-bouclier
function doParadeAuBouclier(msg) {
evitementGenerique(msg, 'parer', 'paradeAuBouclier',
'parade au bouclier', "une parade au bouclier", " a déjà fait une parade au bouclier ce tour", 'force', 'contact', "l'attaque est parée !", {
armeGauche: true,
critiqueAnnuleCritique: true,
predicat: 'tour'
});
}
//!cof-chair-a-canon id1 id2 [evt_id]
// id1 est l'id du pnj récurrent
// id2 est l'id du token qui se met devant l'attaque
function doChairACanon(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 3) {
error("pas assez d'argumennts pour !cof-chair-a-canon", cmd);
return;
}
let evtARefaire;
if (cmd.length > 3) { //On relance pour un événement particulier
evtARefaire = findEvent(cmd[3]);
} else {
evtARefaire = lastEvent();
}
if (evtARefaire === undefined) {
error("L'action est trop ancienne ou a été annulée", cmd);
return;
}
let pnjRec = persoOfId(cmd[1]);
if (pnjRec === undefined) {
error("Le premier argument de !cof-chair-a-canon n'est pas un token de personnage", cmd);
return;
}
if (!peutController(msg, pnjRec)) {
sendPlayer(msg, "pas le droit d'utiliser ce bouton");
return;
}
let testChairACanon = testLimiteUtilisationsCapa(pnjRec, 'chairACanon', 'tour', "a déjà utilisé un sous-fifre ce tour", "ne sait pas utiliser ses sous-fifres pour se défendre");
if (testChairACanon === undefined) {
return;
}
let sousFifre = persoOfId(cmd[2]);
if (sousFifre === undefined) {
error("Le second argument de !cof-chair-a-canon n'est pas un token de personnage", cmd);
return;
}
if (evtARefaire === undefined) {
sendChat('', "Historique d'actions vide, pas d'action trouvée pour la chair à canon");
return;
}
if (evtARefaire.type != 'Attaque') {
sendChat('', "la dernière action n'est pas une attaque");
return;
}
let attaque = evtARefaire.action;
if (attaque === undefined) {
error("Attaque sans action", evtARefaire);
return;
}
let originalTarget;
attaque.cibles = attaque.cibles.filter(function(c) {
if (originalTarget) return true;
if (c.token.id != cmd[1]) return true;
originalTarget = c;
return false;
});
if (originalTarget === undefined) {
error("Impossible de trouver " + nomPerso(pnjRec) + " parmi les cibles de l'attaque", attaque);
return;
}
sousFifre.rollDmg = originalTarget.rollDmg;
sousFifre.chairACanon = true;
attaque.cibles.push(sousFifre);
let optionsRedo = attaque.options;
optionsRedo.rolls = attaque.rolls;
let evt = {
type: "chair à canon"
};
utiliseCapacite(pnjRec, testChairACanon, evt);
undoEvent(evtARefaire);
addEvent(evt);
removePreDmg(attaque.options, originalTarget);
redoEvent(evtARefaire, attaque);
}
// modifie res et le retourne (au cas où il ne serait pas donné)
function listRollResults(roll, res) {
res = res || [];
switch (roll.type) {
case 'V': //top-level des rolls
if (roll.rolls === undefined) break;
roll.rolls.forEach(function(r) {
listRollResults(r, res);
});
return res;
case 'R': //jet simple
if (roll.results === undefined) break;
roll.results.forEach(function(r) {
if (r.v) res.push(r.v);
else if (r.d) res.push(r.d);
else log("Type de résultat de dé inconnu " + r);
});
return res;
case 'M':
case 'L':
return res;
case 'G':
if (roll.rolls === undefined) break;
roll.rolls.forEach(function(ra) {
ra.forEach(function(r) {
listRollResults(r, res);
});
});
return res;
default:
log("tag inconnu");
}
error("Structure de roll inconnue", roll);
return res;
}
//category est un tableau de string, le premier élément étant la catégorie
//principale, le suivant la sous-catégorie, etc
//value peut être un nombre, un tableau de nombres, ou un inline roll
function addStatistics(playerId, category, value) {
if (stateCOF.statistiques === undefined) return;
let stat = stateCOF.statistiques;
if (playerId) {
const player = getObj('player', playerId);
if (player) {
//On utilise l'id roll20 qui semble persistante
const pid = player.get('d20userid');
stat[pid] = stat[pid] || {};
stat = stat[pid];
}
}
if (category) {
category.forEach(function(cat) {
stat[cat] = stat[cat] || {};
stat = stat[cat];
});
}
if (!Array.isArray(value)) {
if (value.results) value = listRollResults(value.results);
else value = [value];
}
value.forEach(function(v) {
if (isNaN(v)) {
error("statistique sur une valeur qui n'est pas un nombre", value);
return;
}
if (typeof v != 'number') v = parseInt(v);
if (stat.total) stat.total += v;
else stat.total = v;
if (stat.nombre) stat.nombre++;
else stat.nombre = 1;
});
}
function displayStatCategory(stats, indent, categoryName, accum) {
const res = {
nombre: 0,
total: 0,
};
if (stats.nombre) { //on peut afficher des résultats
res.nombre = stats.nombre;
res.total = stats.total;
}
let nindent = indent + " ";
let nAccum = [];
for (const category in stats) {
if (category == 'total' || category == 'nombre') break;
let catRes = displayStatCategory(stats[category], nindent, category, nAccum);
res.nombre += catRes.nombre;
res.total += catRes.total;
}
let msg = "aucun jet cellecté";
if (res.nombre > 0) {
let moyenne = res.total / res.nombre;
msg = res.nombre + " jet" + ((res.nombre > 1) ? "s" : "") + ", moyenne " + moyenne;
}
if (nAccum.length > 0) msg = indent + categoryName + " (" + msg + ") :";
else msg = indent + categoryName + " : " + msg;
accum.push(msg);
nAccum.forEach(function(m) {
accum.push(m);
});
return res;
}
function displayStatistics(msg) {
let stats = stateCOF.statistiques;
const display = startFramedDisplay(getPlayerIdFromMsg(msg), "Statistiques");
if (stats === undefined) {
stats = stateCOF.statistiquesEnPause;
if (stats)
addLineToFramedDisplay(display, "Statistiques en pause");
else {
addLineToFramedDisplay(display, "Aucune statistique collectée");
sendFramedDisplay(display);
return;
}
}
let tot = {
total: 0,
nombre: 0
};
let players = findObjs({
type: 'player'
});
let findPlayer = function(pid) {
return players.find(function(p) {
return (p.get('d20userid') == pid);
});
};
let addMessages = function(mv) {
mv.forEach(function(m) {
addLineToFramedDisplay(display, m);
});
};
for (const category in stats) {
//first, check if the category is a player id
let pl = findPlayer(category);
let catName = category;
if (pl) catName = pl.get('displayname');
let accum = [];
let catRes = displayStatCategory(stats[category], "", catName, accum);
addMessages(accum);
tot.total += catRes.total;
tot.nombre += catRes.nombre;
}
addLineToFramedDisplay(display, tot.nombre + " jets au total, dont la somme fait " + tot.total);
sendFramedDisplay(display);
}
function parseDestructionDesMortsVivants(msg) {
const options = parseOptions(msg);
if (options === undefined) return;
let args = options.cmd;
if (args === undefined || args.length < 2) {
error("Il faut au moins un argument à !cof-destruction-des-morts-vivants", args);
return;
}
args.shift();
let dm = args.join(' ');
dm = dm.replace(/%/g, '%');
dm = dm.replace(/\)/g, ')');
dm = dm.replace(/\?/g, '?');
dm = dm.replace(/@/g, '@');
dm = dm.replace(/\[/g, '[');
dm = dm.replace(/\]/g, ']');
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
sendPlayer(msg, "Il faut sélectionner le lanceur de la destruction des morts-vivants", playerId);
return;
}
if (selected.length > 1) {
sendPlayer(msg, "Ne sélectionner qu'un token à la fois pour lancer la destruction des mort-vivants.", playerId);
return;
}
let playerName = msg.who;
if (playerIsGM(playerId)) playerName = 'GM';
iterSelected(selected, function(lanceur) {
if (options.tempeteDeMana) {
if (options.tempeteDeMana.cout === 0) {
//On demande de préciser les options
const optMana = {
mana: options.mana,
dm: true,
soins: false,
duree: false,
rang: options.rang,
};
setTempeteDeMana(playerId, lanceur, msg.content, optMana);
return;
} else {
if (options.rang && options.tempeteDeMana.cout > options.rang) {
sendPlayerAndGM(msg, playerId, "Attention, le coût de la tempête de mana (" + options.tempeteDeMana.cout + ") est supérieur au rang du sort");
}
}
}
if (options.tempeteDeManaIntense) {
let findNbDes = dm.match(/^([0-9]+)d/);
if (findNbDes && findNbDes.length > 1) {
let nbDes = parseInt(findNbDes[1]);
dm = dm.replace(findNbDes[0], (nbDes + options.tempeteDeManaIntense) + 'd');
} else {
log("Pas réussi à trouver le nombre de dés dans " + dm);
}
} else if (options.puissant) {
let findValDes = dm.match(/^([0-9]+d)([0-9]+)/);
if (findValDes && findValDes.length > 2) {
let valDes = parseInt(findValDes[2]) + 2;
dm = dm.replace(findValDes[0], findValDes[1] + valDes);
} else {
log("Pas réussi à trouver le nombre de faces des dés dans " + dm);
}
}
doDestructionDesMortsVivants(lanceur, playerName, dm, options);
});
});
}
function doDestructionDesMortsVivants(lanceur, playerName, dm, options) {
let playerId = options.playerId;
let evt = {
type: "destructionMortsVivants",
action: {
lanceur: lanceur,
playerName: playerName,
playerId: playerId,
dm: dm,
options: options
}
};
addEvent(evt);
if (limiteRessources(lanceur, options, 'destructionDesMortsVivants', "lancer une destruction des mort-vivants", evt)) return;
let display = startFramedDisplay(playerId,
"Sort : destruction des morts-vivants", lanceur);
let testId = 'destructionDesMortsVivants_' + lanceur.token.id;
let difficulte = 13;
let malusRepetition;
if (options.malusRepetition) {
malusRepetition = attributeAsInt(lanceur, 'limiteParCombat_malusDestructionDesMortsVivants', 0);
difficulte += malusRepetition;
malusRepetition += options.malusRepetition;
}
testCaracteristique(lanceur, 'SAG', difficulte, testId, options, evt,
function(testRes) {
let msgJet = "Jet de SAG : " + testRes.texte;
if (testRes.reussite) {
addLineToFramedDisplay(display, msgJet + " ≥ " + difficulte + testRes.modifiers);
if (malusRepetition)
setTokenAttr(lanceur, 'limiteParCombat_malusDestructionDesMortsVivants', malusRepetition, evt);
let cibles = [];
let page = getObj("page", options.pageId);
let murs = getWalls(page, options.pageId);
let pt;
if (murs) {
pt = {
x: lanceur.token.get('left'),
y: lanceur.token.get('top')
};
}
let tokensEnVue = findObjs({
_type: 'graphic',
_pageid: options.pageId,
_subtype: 'token',
layer: 'objects'
});
tokensEnVue.forEach(function(obj) {
if (obj.id == lanceur.token.id) return;
let objCharId = obj.get('represents');
if (objCharId === '') return;
if (obj.get('bar1_max') == 0) return; // jshint ignore:line
let objChar = getObj('character', objCharId);
if (objChar === undefined) return;
if (murs) {
if (obstaclePresent(obj.get('left'), obj.get('top'), pt, murs)) return;
}
let cible = {
charId: objCharId,
token: obj
};
if (!estMortVivant(cible)) return;
if (getState(cible, 'mort')) return;
if (predicateAsBool(cible, 'immunite_destruction')) {
addLineToFramedDisplay(display, nomPerso(cible) + " ne semble pas affecté par la destruction des morts-vivants");
return;
}
cibles.push(cible);
});
let nbCibles = cibles.length;
if (nbCibles === 0) {
addLineToFramedDisplay(display, "Aucun mort-vivant en vue");
sendFramedDisplay(display);
return;
}
let expl = [];
entrerEnCombat(lanceur, cibles, expl, evt);
expl.forEach(function(e) {
addLineToFramedDisplay(display, e, 80, false);
});
let optionsDM = {
sortilege: true,
lanceur: lanceur,
aoe: true,
evt: evt
};
dm = dm.trim();
evt.action.cibles = cibles;
let finalDisplay = function() {
nbCibles--;
if (nbCibles < 1) {
sendFramedDisplay(display);
}
};
cibles.forEach(function(perso) {
let name = nomPerso(perso);
let explications = [];
perso.attaquant = lanceur;
try {
sendChat('', '[[' + dm + ']]', function(resDmg) {
let dmg = {
type: 'magique',
value: dm,
roll: resDmg[0]
};
let afterEvaluateDmg = dmg.roll.content.split(' ');
let dmgRollNumber = rollNumber(afterEvaluateDmg[0]);
dmg.total = dmg.roll.inlinerolls[dmgRollNumber].results.total;
dmg.display = buildinline(dmg.roll.inlinerolls[dmgRollNumber], dmg.type, optionsDM.magique);
dealDamage(perso, dmg, [], evt, false, optionsDM, explications, function(dmgDisplay, dmgFinal) {
addLineToFramedDisplay(display,
name + " reçoit " + dmgDisplay + " DM");
explications.forEach(function(e) {
addLineToFramedDisplay(display, e, 80, false);
});
finalDisplay();
});
}); //fin du jet de dés
} catch (rollError) {
error("Jet " + dm + " mal formé", dm);
}
}); //fin forEach
} else {
addLineToFramedDisplay(display, msgJet + " < " + difficulte);
let msgRate = nomPerso(lanceur) + " ne réussit pas à invoquer son dieu." + testRes.rerolls + testRes.modifiers;
addLineToFramedDisplay(display, msgRate);
sendFramedDisplay(display);
}
});
}
//!cof-enduire-poison label type dm save
//si label = munition_nom, alors on enduit des munitions et non une arme.
function parseEnduireDePoison(msg) {
const options = parseOptions(msg);
let optArgs = msg.content.split(' --');
let cmd = options.cmd;
optArgs.shift();
if (cmd.length < 5) {
error("Usage : !cof-enduire-poison L type force save", cmd);
return;
}
let label = cmd[1];
let typePoison = cmd[2];
if (typePoison != 'rapide' && typePoison != 'affaiblissant') {
error("Les seuls type de poison gérés sont rapide et affaiblissant, mais pas encore " + typePoison, cmd);
}
let nomMunition;
let estMunition = label.startsWith('munition_');
if (estMunition) nomMunition = label.substring(9);
let forcePoison = cmd[3];
let savePoison = parseInt(cmd[4]);
if (isNaN(savePoison)) {
error("Le dernier argument non optionnel doit être la difficulté du test de CON", cmd);
return;
}
let testINT = 14;
optArgs.forEach(function(arg) {
cmd = arg.split(' ');
switch (cmd[0]) {
case 'testINT':
if (cmd.length < 2) {
error("Il faut un argument à --testINT", cmd);
return;
}
testINT = parseInt(cmd[1]);
if (isNaN(testINT)) {
error("Argument de --testINT invalide", cmd);
testINT = 14;
}
return;
}
}); //fin du traitement des options
getSelected(msg, function(selected, playerId) {
iterSelected(selected, function(perso) {
let labelArme = label;
if (!estMunition) {
if (labelArme == -1) {
let arme = armesEnMain(perso);
if (arme) {
labelArme = arme.label;
} else {
sendPerso(perso, "n'a pas d'arme en main");
return;
}
} else if (labelArme == -2) {
armesEnMain(perso);
if (perso.armeGauche) {
labelArme = perso.armeGauche.label;
} else {
sendPerso(perso, "n'a pas d'arme en main gauche");
return;
}
}
}
let attribut = 'enduitDePoison_' + labelArme;
let armeEnduite;
let attr = tokenAttribute(perso, attribut);
let infosAdditionelles = savePoison;
let arme;
if (!estMunition) {
arme = getWeaponStats(perso, labelArme);
if (arme === undefined) {
error("Pas d'arme de label " + labelArme, cmd);
return;
}
if (arme.sortilege) {
sendPerso(perso, "imossible d'enduire un sortilège de poison", true);
return;
}
if (arme.typeDegats == 'contondant') {
sendPerso(perso, arme.name + " fait des dégâts contondants. Est-il vraiment possible de l'empoisonner ?", true);
}
if (arme.armeDeJet) estMunition = true;
}
if (estMunition) {
let munitionsCourantes;
let maxMunitions;
if (arme) {
armeEnduite = arme.name;
munitionsCourantes = arme.nbArmesDeJet;
maxMunitions = arme.nbArmesDeJetMax;
} else {
let munitions = listAllMunitions(perso);
let m = munitions[nomMunition];
if (m) {
let typeMunition = fieldAsString(m, 'typemunition', 'Flèche');
armeEnduite = fieldAsString(m, 'nommunition', typeMunition);
munitionsCourantes = fieldAsInt(m, 'qtemunition', 1);
maxMunitions = fieldAsInt(m, 'qtemunition_max', 1);
} else { //ancienne variante, obsolète depuis mars 2023
armeEnduite = nomMunition.replace(/_/g, ' ');
let attrQte = tokenAttribute(perso, labelArme);
if (attrQte.length === 0) {
sendPerso(perso, "n'a pas de munition nommée " + nomMunition);
return;
}
attrQte = attrQte[0];
munitionsCourantes = parseInt(attrQte.get('current'));
maxMunitions = parseInt(attrQte.get('max'));
if (isNaN(munitionsCourantes) || isNaN(maxMunitions)) {
error("Attribut de munitions mal formé", attrQte);
return;
}
}
}
if (munitionsCourantes === 0) {
sendPlayer(msg, "Plus de munition " + nomMunition, playerId);
return;
}
let dejaEnduits = 0;
if (attr.length > 0) {
let infos = attr[0].get('max');
let indexInfos = infos.indexOf(' ');
if (indexInfos < 1) {
error("Attribut de poison rapide de munition mal formé (il faudrait la difficulté du save + le nombre de munitions empoisonnées)", infos);
return;
}
let oldSave = parseInt(infos.substring(0, indexInfos));
dejaEnduits = parseInt(infos.substring(indexInfos + 1));
if (isNaN(dejaEnduits)) dejaEnduits = 0;
if (dejaEnduits > 0 &&
(attr[0].get('current') != typePoison + ' ' + forcePoison ||
oldSave != savePoison)) {
sendPlayer(msg, "Il y a déjà du poison de type " + attr[0].get('current') + "et de save " + oldSave + " sur les munitions " + armeEnduite + ". Le script ne sait pas gérer différents poisons sur les mêmes munitions.", playerId);
return;
}
}
infosAdditionelles = savePoison + ' ' + (dejaEnduits + 1);
if (dejaEnduits >= maxMunitions) {
sendPlayer(msg, "Toutes les munitions " + armeEnduite + " sont déjà enduites de poison", playerId);
return;
}
} else {
armeEnduite = getAttackName(labelArme, perso);
if (armeEnduite === undefined) {
error(perso.tokNname + " n'a pas d'arme associée au label " + labelArme, cmd);
return;
}
if (attributeAsBool(perso, attribut)) {
sendPlayer(msg, armeEnduite + " est déjà enduit de poison.", playerId);
return;
}
}
if (predicateAsBool(perso, 'connaissanceDuPoison')) {
//Pas besoin de test
const evt = {
type: 'enduireDePoison'
};
addEvent(evt);
if (limiteRessources(perso, options, 'enduirePoison', 'enduire de poison', evt)) return;
setTokenAttr(perso, attribut, typePoison + ' ' + forcePoison, evt, {
maxVal: infosAdditionelles
});
sendPlayer(msg, armeEnduite + " est maintenant enduit de poison", playerId);
return;
}
doEnduireDePoison(perso, armeEnduite, savePoison, typePoison, forcePoison, attribut, testINT, infosAdditionelles, options);
});
});
}
function doEnduireDePoison(perso, armeEnduite, savePoison, typePoison, forcePoison, attribut, testINT, infosAdditionelles, options) {
const evt = {
type: 'enduireDePoison',
action: {
perso,
armeEnduite,
savePoison,
typePoison,
forcePoison,
attribut,
testINT,
infosAdditionelles,
options
}
};
addEvent(evt);
if (limiteRessources(perso, options, 'enduirePoison', 'enduire de poison', evt)) return;
const display = startFramedDisplay(options.playerId, "Essaie d'enduire " + armeEnduite + " de poison", perso);
//Test d'INT pour savoir si l'action réussit.
let testId = 'enduireDePoison';
testCaracteristique(perso, 'INT', testINT, testId, options, evt,
function(tr) {
let jet = "Jet d'INT : " + tr.texte;
if (tr.echecCritique) { //échec critique
jet += " Échec critique !" + tr.rerolls + tr.modifiers;
addLineToFramedDisplay(display, jet);
addLineToFramedDisplay(display, nomPerso(perso) + " s'empoisonne.");
sendChat('', "[[" + forcePoison + "]]", function(res) {
let dmgRoll;
if (options.rolls && options.rolls.enduireSelfDmg) {
dmgRoll = options.rolls.enduireSelfDmg;
} else {
dmgRoll = res[0].inlinerolls[0];
}
evt.action.rolls.enduireSelfDmg = dmgRoll;
if (typePoison == 'rapide') {
let r = {
total: dmgRoll.results.total,
type: 'poison',
display: buildinline(dmgRoll, 'poison')
};
options.partialSave = {
carac: 'CON',
seuil: savePoison
};
let explications = [];
dealDamage(perso, r, [], evt, false, options, explications,
function(dmgDisplay, dmg) {
explications.forEach(function(e) {
addLineToFramedDisplay(display, e);
});
addLineToFramedDisplay(display, nomPerso(perso) + " subit " + dmgDisplay + " DM");
sendFramedDisplay(display);
}); //fin de dmg dus à l'échec critique
}
}); //fin du jet de dmg
} else if (tr.reussite) {
jet += " ≥ " + testINT + tr.modifiers;
addLineToFramedDisplay(display, jet);
setTokenAttr(perso, attribut, typePoison + ' ' + forcePoison, evt, {
maxVal: infosAdditionelles
});
addLineToFramedDisplay(display, armeEnduite + " est maintenant enduit de poison");
sendFramedDisplay(display);
} else { //echec normal au jet d'INT
jet += " < " + testINT + " : échec" + tr.rerolls + tr.modifiers;
addLineToFramedDisplay(display, jet);
sendFramedDisplay(display);
}
}); //fin du test de carac
}
const listeElixirs = [{
nom: 'Élixir fortifiant',
attrName: 'fortifiant',
action: "!cof-fortifiant $rang",
rang: 1
}, {
nom: 'Élixir de feu grégeois',
attrName: 'feu_grégeois',
action: "!cof-attack @{selected|token_id} @{target|token_id} Feu Grégeois --auto --dm $rangd6 --feu --psave DEX [[$base_save+@{selected|INT}]] --disque 3 --portee 10 --targetFx burst-fire",
rang: 2
}, {
nom: 'Élixir de guérison',
attrName: 'élixir_de_guérison',
action: "!cof-soin 3d6+$INT",
rang: 3
}, {
nom: "Élixir d'agrandissement",
attrName: "potion_d_agrandissement",
action: "!cof-effet-temp agrandissement [[5+$INT]]",
rang: 4
}, {
nom: "Élixir de forme gazeuse",
attrName: "potion_de_forme_gazeuse",
action: "!cof-effet-temp formeGazeuse [[1d4+$INT]]",
rang: 4
}, {
nom: "Élixir de protection contre les éléments",
attrName: "potion_de_protection_contre_les_éléments",
action: "!cof-effet-temp protectionContreLesElements [[5+$INT]] --valeur $rang",
rang: 4
}, {
nom: "Élixir d'armure de mage",
attrName: "potion_d_armure_de_mage",
action: "!cof-effet-combat armureDuMage",
rang: 4
}, {
nom: "Élixir de chute ralentie",
attrName: "potion_de_chute_ralentie",
action: "est léger comme une plume.",
rang: 4
}, {
nom: "Élixir d'invisibilité",
attrName: "potion_d_invisibilité",
action: "!cof-set-state invisible true --message se rend invisible ([[1d6+$INT]] minutes)",
rang: 5
}, {
nom: "Élixir de vol",
attrName: "potion_de_vol",
action: "se met à voler",
rang: 5
}, {
nom: "Élixir de respiration aquatique",
attrName: "potion_de_respiration_aquatique",
action: "peut respirer sous l'eau",
rang: 5
}, {
nom: "Élixir de flou",
attrName: "potion_de_flou",
action: "!cof-effet-temp flou [[1d4+$INT]]",
rang: 5
}, {
nom: "Élixir de hâte",
attrName: "potion_de_hâte",
action: "!cof-effet-temp hate [[1d6+$INT]]",
rang: 5
},
//Le élixirs pour les terres d'Arran
{
nom: 'Huile instable',
attrName: 'huileInstable',
action: "!cof-huile-instable @{target|token_id}",
rang: 3,
arran: true
}, {
nom: 'Élixir de guérison',
attrName: 'élixirGuérison',
action: "!cof-soin 3d6+$INT",
rang: 4,
arran: true
}, {
nom: "Élixir de peau d'écorce",
attrName: 'peauEcorce',
action: "!cof-effet-temp peauDEcorce [[5+$SAG]] --valeur 4",
rang: 5,
arran: true
}, {
nom: "Élixir d'image décalée",
attrName: 'imageDecalee',
action: "!cof-effet-temp imageDecalee [[5+$SAG]]",
rang: 5,
arran: true
}, {
nom: "Élixir de protection contre les éléments",
attrName: 'protectionContreElements',
action: "!cof-effet-temp protectionContreLesElements [[5+$SAG]] --valeur 5",
rang: 5,
arran: true
},
];
function exilirInconnu(elixir, perso, voieDesElixirs) {
if (elixir.rang > voieDesElixirs) return true;
if (elixir.rang < 3) return false;
if (persoArran(perso)) return !elixir.arran;
return elixir.arran;
}
const consommableNomRegExp = new RegExp(/^(repeating_equipement_.*_)equip_nom/);
const consommableQuantiteRegExp = new RegExp(/^(repeating_equipement_.*_)equip_qte/);
const consommableEffetRegExp = new RegExp(/^(repeating_equipement_.*_)equip_effet/);
function listeConsommables(msg) {
getSelected(msg, function(selected, playerId) {
iterSelected(selected, function(perso) {
if (perso.token.get('bar1_link') === '') {
error("La liste de consommables n'est pas au point pour les tokens non liés", perso);
return;
}
let display = startFramedDisplay(playerId, 'Liste de vos consommables :', perso, {
chuchote: true
});
let attributes = findObjs({
_type: 'attribute',
_characterid: perso.charId
});
let consommables = {}; //map id -> nom, quantite, effet, attr
attributes.forEach(function(attr) {
let attrName = attr.get('name').trim();
let m = consommableNomRegExp.exec(attrName);
if (m) {
let consoPrefix = m[1];
consommables[consoPrefix] = consommables[consoPrefix] || {};
consommables[consoPrefix].nom = attr.get('current');
return;
}
m = consommableQuantiteRegExp.exec(attrName);
if (m) {
let consoPrefix = m[1];
consommables[consoPrefix] = consommables[consoPrefix] || {};
consommables[consoPrefix].quantite = parseInt(attr.get('current'));
consommables[consoPrefix].attr = attr;
return;
}
m = consommableEffetRegExp.exec(attrName);
if (m) {
let consoPrefix = m[1];
consommables[consoPrefix] = consommables[consoPrefix] || {};
consommables[consoPrefix].effet = attr.get('current');
return;
}
//Consommables dans des attributs utilisateurs
if (!(attrName.startsWith('dose_') || attrName.startsWith('consommable_') || attrName.startsWith('elixir_'))) return;
let consName;
if (attrName.startsWith("elixir_")) {
let typeElixir = listeElixirs.find(function(i) {
return "elixir_" + i.attrName == attrName;
});
if (typeElixir !== undefined) {
consName = typeElixir.nom;
}
}
if (consName === undefined) {
consName = attrName.substring(attrName.indexOf('_') + 1);
consName = consName.replace(/_/g, ' ');
}
let quantite = parseInt(attr.get('current'));
if (isNaN(quantite) || quantite === 0) return;
let action = attr.get('max').trim();
while (consommables[attrName]) {
attrName += randomInteger(1000);
}
consommables[attrName] = {
nom: consName,
quantite: quantite,
effet: action,
attr: attr,
};
}); //fin de la boucle sur les attributs
let aConsommable;
_.each(consommables, function(c, prefix) {
if (c.effet === undefined || c.effet === '' || c.nom === undefined || c.nom === '') return;
//La quantité est de 1 par défaut sur la fiche
if (c.quantite === undefined) {
c.quantite = 1;
c.attr = createObj('attribute', {
_characterid: perso.charId,
name: prefix + 'equip_qte',
current: 1
});
} else if (isNaN(c.quantite) || c.quantite < 1) {
return;
}
aConsommable = true;
let ligne = c.quantite + ' ';
ligne += bouton(c.effet, c.nom, perso, {
ressource: c.attr
});
// Pictos : https://wiki.roll20.net/CSS_Wizardry#Pictos
let overlay = ' title="Cliquez pour échanger"';
ligne += boutonSimple('!cof-echange-consommable ' + perso.token.id + ' @{target|token_id} ' + c.attr.id, 'r', overlay);
addLineToFramedDisplay(display, ligne);
}); //fin de la boucle sur les onsommables
if (aConsommable)
addLineToFramedDisplay(display, 'Cliquez sur le consommable pour l\'utiliser ou sur r pour l\'échanger avec un autre personnage.');
else
addLineToFramedDisplay(display, "Vous n'avez aucun consommable
");
sendFramedDisplay(display);
});
}); //fin du getSelected
}
// !cof-utilise-consommable tok_id attr_id [msg]
// utilisation d'un consommable sans action en jeu
function utiliseConsommable(msg) {
let cmd = msg.content.split(' ');
if (cmd.length < 3) {
error("Erreur interne d'utilisation de consommables", cmd);
return;
}
cmd.shift();
let perso = persoOfId(cmd[0]);
if (perso === undefined) {
log("Propriétaire perdu");
sendChat('COF', "Plus possible d'utiliser cette action. Réafficher les consommables.");
return;
}
// Vérifie les droits d'utiliser le consommable
if (msg.selected && msg.selected.length == 1) {
let utilisateur = persoOfId(msg.selected[0]._id);
if (utilisateur === undefined) {
sendChat('COF', "Le token sélectionné n'est pas valide");
return;
}
let d = distanceCombat(perso.token, utilisateur.token);
if (d > 0) {
sendPerso(utilisateur, "est trop loin de " + nomPerso(perso) + " pour utiliser ses objets");
return;
}
perso = utilisateur;
} else {
//On regarde si le joueur contrôle le token
if (!peutController(msg, perso)) {
sendPlayer(msg, "Pas les droits pour ça");
return;
}
}
//on récupère l'attribut à utiliser
cmd.shift();
let attr = getObj('attribute', cmd[0]);
if (attr === undefined) {
log("Attribut a changé/perdu");
log(msg.content);
sendChat('COF', "Plus possible d'utiliser cette action. Veuillez réafficher les consommables.");
return;
}
//Nom du consommable (pour affichage)
let consName;
let quantite = parseInt(attr.get('current'));
const evt = {
type: "Utilisation de consommable",
attributes: [{
attribute: attr,
current: quantite,
}]
};
let attrName = attr.get('name').trim();
//On regarde si c'est un consommable sur la fiche
let m = consommableQuantiteRegExp.exec(attrName);
if (m) {
let consoPrefix = m[1];
let attrConsName = charAttribute(perso.charId, consoPrefix + 'equip_nom');
if (attrConsName.length === 0) {
error("Impossible de trouver le nom du consommable", attr);
return;
}
consName = attrConsName[0].get('current').trim();
} else {
consName = attrName.substring(attrName.indexOf('_') + 1);
consName = consName.replace(/_/g, ' ').trim();
}
if (isNaN(quantite) || quantite < 1) {
attr.set('current', 0);
whisperChar(perso.charId + "Vous ne disposez plus de " + consName);
return;
}
if (cmd.length > 1) {
cmd.shift();
sendPerso(perso, cmd.join(' '));
}
attr.set('current', quantite - 1);
addEvent(evt);
}
//!cof-echange-consommable tid1 tid2 attrid
function echangeConsommable(msg) {
let cmd = msg.content.split(' ');
if (cmd.length < 4) {
error("Erreur interne de consommables", cmd);
return;
}
if (cmd[1] == cmd[2]) {
sendChat('COF', "Échange avec soi-même, sans effet");
return;
}
//perso1 = token avec qui va échanger le consommable
let perso1 = persoOfId(cmd[1]);
if (perso1 === undefined) {
log("Propriétaire perdu");
sendChat('COF', "Plus possible d'utiliser cette action. Réafficher les consommables.");
return;
}
//perso2 = token avec lequel on va faire l'échange
let perso2 = persoOfId(cmd[2]);
if (perso2 === undefined) {
log("Destinataire perdu");
sendChat('COF', "Erreur concernant le destinataire. Veuillez réessayer.");
return;
}
//On regarde si le joueur contrôle le token
if (!peutController(msg, perso1)) {
sendPlayer(msg, "Pas les droits pour ça");
return;
}
//on récupère l'attribut à échanger de perso1
let attr1 = getObj('attribute', cmd[3]);
if (attr1 === undefined) {
log("Attribut a changé/perdu");
log(cmd);
sendChat('COF', "Plus possible d'utiliser cette action. Veuillez réafficher les consommables.");
return;
}
let consName;
let quantite1 = parseInt(attr1.get('current'));
const evt = {
type: "Échange de consommable",
attributes: [{
attribute: attr1,
current: quantite1,
}]
};
let effet;
let attrName = attr1.get('name').trim();
//On regarde si c'est un consommable sur la fiche
let m1 = consommableQuantiteRegExp.exec(attrName);
if (m1) {
let consoPrefix = m1[1];
let attrConsName = charAttribute(perso1.charId, consoPrefix + 'equip_nom');
let attrEffet = charAttribute(perso1.charId, consoPrefix + 'equip_effet');
if (attrConsName.length === 0 || attrEffet.length === 0) {
error("Impossible de trouver le nom ou l'effet du consommable", attr1);
return;
}
consName = attrConsName[0].get('current').trim();
effet = attrEffet[0].get('current').trim();
} else {
consName = attrName.substring(attrName.indexOf('_') + 1);
consName = consName.replace(/_/g, ' ').trim();
effet = attr1.get('max').trim();
}
if (isNaN(quantite1) || quantite1 < 1) {
attr1.set('current', 0);
whisperChar(perso1.charId, "Vous ne disposez plus de " + consName);
return;
}
// on baisse la valeur de 1 du consommable qu'on s'apprête à échanger
quantite1--;
attr1.set('current', quantite1);
// ajout du consommable dans perso2 :
let attributes = findObjs({
_type: 'attribute',
_characterid: perso2.charId
});
let quantite2 = 0;
// on recherche si le consommable existe chez perso2
let found = attributes.find(function(attr2) {
let attrName2 = attr2.get('name');
let m2 = consommableNomRegExp.exec(attrName2);
if (m2) {
if (consName != attr2.get('current').trim()) return false;
let consoPrefix2 = m2[1];
let attrEffet2 =
charAttribute(perso2.charId, consoPrefix2 + 'equip_effet');
if (attrEffet2.length === 0) {
attrEffet2 = createObj('attribute', {
characterid: perso2.charId,
name: consoPrefix2 + 'equip_effet',
current: effet
});
evt.attributes.push({
attribute: attrEffet2,
});
} else if (attrEffet2[0].get('current').trim() != effet) {
error("Échange dangereux : pas le même effet pour le consommable selon le personnage \n" +
"Effet chez " + nomPerso(perso1) + " : " + effet + "\n" +
"Effet chez " + nomPerso(perso2) + " : " + attrEffet2[0].get('current'), attr2.get('name'));
return false;
}
let attrQte2 = charAttribute(perso2.charId, consoPrefix2 + 'equip_qte');
if (attrQte2.length === 0) {
quantite2 = 1;
attrQte2 = createObj('attribute', {
characterid: perso2.charId,
name: consoPrefix2 + 'equip_qte',
current: 2
});
evt.attributes.push({
attribute: attrQte2,
});
return true;
}
attrQte2 = attrQte2[0];
quantite2 = parseInt(attrQte2.get('current'));
if (isNaN(quantite2) || quantite2 < 1) quantite2 = 0;
attrQte2.set('current', quantite2 + 1);
evt.attributes.push({
attribute: attrQte2,
current: quantite2
});
return true;
} else if (!m1 && attrName == attrName2.trim()) {
if (attr2.get('max').trim() != effet) {
error("Échange dangereux : pas le même effet pour le consommable selon le personnage \n" +
"Effet chez " + nomPerso(perso1) + " : " + effet + "\n" +
"Effet chez " + nomPerso(perso2) + " : " + attr2.get('max'), attr2);
return false;
}
quantite2 = parseInt(attr2.get('current'));
if (isNaN(quantite2) || quantite2 < 1) quantite2 = 0;
attr2.set('current', quantite2 + 1);
evt.attributes.push({
attribute: attr2,
current: quantite2,
max: effet
});
return true;
}
return false;
});
// si le consommable n'a pas été trouvé, on le crée avec une valeur de 1.
if (!found) {
if (m1) {
let pref = 'repeating_equipement_' + generateRowID() + '_';
let attre = createObj("attribute", {
name: pref + 'equip_nom',
current: consName,
characterid: perso2.charId
});
evt.attributes.push({
attribute: attre,
});
attre = createObj("attribute", {
name: pref + 'equip_effet',
current: effet,
characterid: perso2.charId
});
evt.attributes.push({
attribute: attre,
});
} else {
let attr2 = createObj("attribute", {
name: attrName,
current: 1,
max: effet,
characterid: perso2.charId
});
evt.attributes.push({
attribute: attr2,
});
}
}
quantite2++;
// on envoie un petit message précisant la résultante de l'action.
sendChat('COF', "Echange entre " + nomPerso(perso1) + " et " + nomPerso(perso2) + " terminée.");
whisperChar(perso1.charId, " Il vous reste " + quantite1 + " " + consName + ".");
whisperChar(perso2.charId, " Vous possédez désormais " + quantite2 + " " + consName + ".");
// le MJ est notifié :
sendChat('COF', "/w GM " + nomPerso(perso1) + " vient de donner 1 " + consName + " à " + nomPerso(perso2) + ".");
addEvent(evt);
}
function parseProvocation(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Problème de parse options", msg.content);
return;
}
if (cmd.length < 3) {
error("La commande !cof-provocation requiert 2 arguments", cmd);
return;
}
let voleur = persoOfId(cmd[1]);
if (voleur === undefined) {
error("Le premier argument de !cof-provocation n'est pas un token valide");
return;
}
let cible = persoOfId(cmd[2]);
if (cible === undefined) {
error("Le deuxième argument de !cof-provocation n'est pas un token valide");
return;
}
if (cmd.length > 3 && cmd[3] == 'raillerie') {
if (predicateAsBool(cible, 'raillerieImpossible')) {
sendPerso(cible, "ne semble pas comprendre les railleries de " + nomPerso(voleur));
return;
}
options.raillerie = true;
options.bonusAttrs = options.bonusAttrs || [];
options.bonusAttrs.push('resistanceRaillerie');
}
doProvocation(voleur, cible, options);
}
function doProvocation(voleur, cible, options) {
let evt = {
type: 'provocation',
action: {
titre: "Provocation",
voleur: voleur,
cible: cible,
options: options,
}
};
addEvent(evt);
let nomVoleur = nomPerso(voleur);
let nomCible = nomPerso(cible);
let titre = 'Provocation';
let action = 'provocation';
if (options.raillerie) {
titre = 'Raillerie';
action = 'raillerie';
}
if (limiteRessources(voleur, options, action, action, evt)) return;
const display =
startFramedDisplay(options.playerId, titre, voleur, {
perso2: cible
});
let explications = [];
const rollId = 'provocation_' + cible.token.id;
testOppose(rollId, voleur, 'CHA', options, cible, 'INT',
options, explications, evt,
function(res, crit, rt1, rt2) {
explications.forEach(function(l) {
addLineToFramedDisplay(display, l);
});
let reussite;
switch (res) {
case 0: //en cas d'égalité, on considère que la provocation est réussie
diminueMalediction(cible, evt);
switch (crit) {
case -1:
reussite = "Sur un malentendu, la " + action + " réussit...";
if (options.raillerie) setAttrDuree(cible, 'enerve', 1, evt);
break;
case 0:
case 1:
reussite = "La " + action + " réussit tout juste.";
if (options.raillerie) {
setAttrDuree(cible, 'enerve', 1, evt);
setTokenAttr(cible, 'resistanceRaillerie',
attributeAsInt(cible, 'resistanceRaillerie', 0) + 2, evt);
}
}
break;
case 1:
switch (crit) {
case -1:
reussite = nomCible + " marche complètement, il attaque " + nomVoleur;
if (options.raillerie) {
setAttrDuree(cible, 'enerve', 1, evt);
removeTokenAttr(cible, 'resistanceRaillerie', evt);
}
break;
case 0:
if (options.raillerie) {
setAttrDuree(cible, 'enerve', 1, evt);
reussite = nomVoleur + " a réussi à bien énerver " + nomCible;
setTokenAttr(cible, 'resistanceRaillerie',
attributeAsInt(cible, 'resistanceRaillerie', 0) + 1, evt);
} else reussite = "La provocation réussit.";
break;
case 1:
reussite = "La provocation est une réussite critique !";
setAttrDuree(cible, 'enerve', 1, evt);
setTokenAttr(cible, 'resistanceRaillerie',
attributeAsInt(cible, 'resistanceRaillerie', 0) - 1, evt);
}
break;
case 2:
switch (crit) {
case -1:
reussite = "Échec critique de la " + action + " !";
if (options.raillerie) {
setTokenAttr(cible, 'resistanceRaillerie',
attributeAsInt(cible, 'resistanceRaillerie', 0) + 5, evt);
}
break;
case 0:
if (options.raillerie) {
setTokenAttr(cible, 'resistanceRaillerie',
attributeAsInt(cible, 'resistanceRaillerie', 0) + 1, evt);
}
reussite = "La provocation échoue";
break;
case 1:
if (options.raillerie) {
setTokenAttr(cible, 'resistanceRaillerie',
attributeAsInt(cible, 'resistanceRaillerie', 0) + 10, evt);
}
reussite = nomCible + " voit clair dans le jeu de " + nomVoleur + ". La provocation échoue.";
}
}
addLineToFramedDisplay(display, reussite);
sendFramedDisplay(display);
}); //Fin du test opposé
}
function enSelle(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 3) {
error("Il faut 2 arguments pour !cof-en-selle", msg.content);
return;
}
cmd.shift();
const cavalier = persoOfId(cmd[0]);
if (cavalier === undefined) {
error("Premier argument de !cof-en-selle incorrect", cmd);
return;
}
const tokenC = cavalier.token;
const pageId = tokenC.get('pageid');
let evt = {
type: 'En selle'
};
addEvent(evt);
let attrMonteSur = tokenAttribute(cavalier, 'monteSur');
if (attrMonteSur.length > 0) {
//Alors le cavalier va descendre de sa monture
attrMonteSur = attrMonteSur[0];
const monture = persoOfIdName(attrMonteSur.get('current'), pageId, true);
evt.deletedAttributes = evt.deletedAttributes || [];
evt.deletedAttributes.push(attrMonteSur);
attrMonteSur.remove();
if (monture === undefined) {
sendPerso(cavalier, "descend de sa monture");
} else {
sendPerso(cavalier, "descend de " + nomPerso(monture));
removeTokenAttr(monture, 'estMontePar', evt);
removeTokenAttr(monture, 'positionSurMonture', evt);
}
return;
}
cmd.shift();
let nomMonture = cmd.join(' ');
let monture = persoOfId(nomMonture, nomMonture, pageId);
if (monture === undefined || !predicateAsBool(monture, 'monture')) {
sendPerso(cavalier, "ne peut pas monter là-dessus");
log(nomMonture);
return;
}
const tokenM = monture.token;
nomMonture = tokenM.get('name');
if (attributeAsBool(monture, 'estMontePar')) {
//Vérifie si le cavalier existe bien sur cette page.
let cavalierBis;
let estMontePar = tokenAttribute(monture, 'estMontePar');
estMontePar.forEach(function(emp) {
if (cavalierBis) return;
cavalierBis = persoOfIdName(emp.get('current'), pageId);
if (cavalierBis === undefined) emp.remove();
});
if (cavalierBis) {
sendPerso(cavalier, "ne peut monter sur " + nomMonture + " car " + onGenre(monture, 'il', 'elle') + " a déjà un cavalier, " + nomPerso(cavalierBis));
return;
}
}
if (distanceCombat(tokenC, tokenM, pageId) > 0) {
sendPerso(cavalier, "est trop loin de " + nomMonture);
return;
}
setTokenAttr(cavalier, 'monteSur', idName(monture), evt, {
msg: " monte sur " + nomMonture,
});
setTokenAttr(monture, 'estMontePar', idName(cavalier), evt);
setTokenAttr(monture, 'positionSurMonture', tokenC.get('left') - tokenM.get('left'), evt, {
maxVal: tokenC.get('top') - tokenM.get('top')
});
setTokenAttr(monture, 'directionSurMonture', tokenC.get('rotation') - tokenM.get('rotation'), evt);
if (stateCOF.combat) {
updateInit(monture.token, evt);
if (stateCOF.options.affichage.val.init_dynamique.val) {
setTokenInitAura(monture);
}
}
}
function rangVoieDesElixirs(perso, noMsg) {
let arran = persoArran(perso);
let voieDesElixirs;
if (arran) {
voieDesElixirs = predicateAsInt(perso, 'voieDeLAlchimie', 0);
if (voieDesElixirs === 0)
voieDesElixirs = predicateAsInt(perso, 'voieDesElixirs', 0);
} else voieDesElixirs = predicateAsInt(perso, 'voieDesElixirs', 0);
if (voieDesElixirs < 1 && !noMsg) {
if (arran)
sendPerso(perso, "ne connaît pas la Voie de l'Alchimie");
else
sendPerso(perso, "ne connaît pas la Voie des Élixirs");
return;
}
return voieDesElixirs;
}
//!cof-creer-elixir token_id elixir
//on peut remplacer token_id par character_id
function creerElixir(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 3) {
error("Pas assez d'arguments pour !cof-creer-elixir", msg.content);
return;
}
let forgesort = persoOfId(cmd[1], cmd[1], options.pageId);
if (forgesort === undefined) {
if (msg.selected && msg.selected.length == 1) {
forgesort = persoOfId(msg.selected[0]._id);
}
if (forgesort === undefined) {
let c = getObj('character', cmd[1]);
if (c === undefined) {
error("Impossible de savoir qui crée l'élixir", cmd);
return;
}
forgesort = {
charId: cmd[1]
};
}
}
let voieDesElixirs = rangVoieDesElixirs(forgesort);
if (!voieDesElixirs) return;
let elixir = listeElixirs.find(function(i) {
if (exilirInconnu(i, forgesort, voieDesElixirs)) return false;
return i.attrName == cmd[2];
});
if (elixir === undefined) { //Version perso des élixirs
let altElixirs = findObjs({
_type: 'attribute',
_characterid: forgesort.charId
});
altElixirs.find(function(attr) {
let attrName = attr.get('name');
if (!attrName.startsWith('Elixir ')) return false;
let rang = parseInt(attrName.substring(7));
if (isNaN(rang) || rang > 3) return false;
let nomElixir = attr.get('current');
if (nomElixir != cmd[2]) return false;
elixir = {
nom: nomElixir,
attrName: nomElixir,
action: attr.get('max'),
rang: rang
};
return true;
});
if (elixir === undefined) {
error(nomPerso(forgesort) + " est incapable de créer " + cmd[2], cmd);
return;
}
}
const evt = {
type: "Création d'élixir"
};
addEvent(evt);
let arran = persoArran(forgesort);
//Dépense de mana
if (!arran && reglesOptionelles.mana.val.elixirs_sorts.val && ficheAttributeAsBool(forgesort, 'option_pm', true)) {
if (reglesOptionelles.mana.val.mana_totale.val) {
switch (elixir.rang) {
case 1:
options.mana = 1;
break;
case 2:
options.mana = 3;
break;
case 3:
options.mana = 6;
break;
case 4:
options.mana = 10;
break;
case 5:
options.mana = 15;
break;
}
} else if (elixir.rang > 2) {
options.mana = elixir.rang - 2;
}
}
let elixirsACreer = tokenAttribute(forgesort, 'elixirsACreer');
if (elixirsACreer.length === 0) {
error(nomPerso(forgesort) + " ne peut créer d'élixirs " + cmd[2], cmd);
return;
}
elixirsACreer = elixirsACreer[0];
let extraFortifiants = toInt(elixirsACreer.get('max'), 0);
let extra = extraFortifiants > 0 && elixir.rang == 1;
if (!extra) options.decrAttribute = elixirsACreer.id;
if (limiteRessources(forgesort, options, 'elixirsACreer', 'élixirs à créer', evt)) return;
if (extra) {
evt.attributes = evt.attributes || [];
evt.attributes.push({
attribute: elixirsACreer,
current: elixirsACreer.get('current'),
max: extraFortifiants
});
elixirsACreer.set('max', extraFortifiants - 1);
}
let attrName = 'elixir_' + elixir.attrName;
let message = "crée un " + elixir.nom;
let attr = tokenAttribute(forgesort, attrName);
if (attr.length === 0) {
let rang = voieDesElixirs;
let base_save = 10;
if (predicateAsBool(forgesort, 'boutefeu') &&
elixir.action.includes('--feu')) {
rang = rang + 1;
base_save = base_save + 2;
}
let action = elixir.action.replace(/\$rang/g, rang);
action = action.replace(/\$base_save/g, base_save);
action = action.replace(/\$INT/g, modCarac(forgesort, 'intelligence'));
action = action.replace(/\$SAG/g, modCarac(forgesort, 'sagesse'));
setTokenAttr(forgesort, attrName, 1, evt, {
msg: message,
maxVal: action
});
} else {
let nb = parseInt(attr[0].get('current'));
if (isNaN(nb) || nb < 1) nb = 0;
setTokenAttr(forgesort, attrName, nb + 1, evt, {
msg: message
});
}
}
function gestionElixir(msg) {
getSelected(msg, function(selected, playerId) {
iterSelected(selected, function(forgesort) {
let voieDesElixirs = rangVoieDesElixirs(forgesort);
if (!voieDesElixirs) return;
let elixirsACreer = voieDesElixirs * 2;
let fortifiantExtra = 0;
let attrElixirs = tokenAttribute(forgesort, 'elixirsACreer');
if (attrElixirs.length === 0) {
//TODO: ajouter un evenement pour pouvoir faire un undo
let opt = {};
if (predicateAsBool(forgesort, 'fortifiantAvance')) {
opt.maxVal = 2;
fortifiantExtra = 2;
}
attrElixirs = setTokenAttr(forgesort, 'elixirsACreer', elixirsACreer, {}, opt);
} else {
attrElixirs = attrElixirs[0];
elixirsACreer = toInt(attrElixirs.get('current'), 0);
fortifiantExtra = toInt(attrElixirs.get('max'), 0);
}
let titre;
if (elixirsACreer < 1) {
if (fortifiantExtra < 1)
titre = "Impossible de créer un autre élixir aujourd'hui";
else {
titre = "Encore " + fortifiantExtra + " fortifiant";
if (fortifiantExtra > 1) titre += 's';
titre += "à créer";
}
} else {
titre = "Encore " + elixirsACreer + " élixir";
if (elixirsACreer > 1) titre += 's';
if (fortifiantExtra > 0) {
titre += " et " + fortifiantExtra + " fortifiant";
if (fortifiantExtra > 1) titre += 's';
}
titre += " à créer";
}
let display = startFramedDisplay(playerId, titre, forgesort, {
chuchote: true
});
listeElixirs.forEach(function(elixir) {
if (exilirInconnu(elixir, forgesort, voieDesElixirs)) return;
if (elixir.rang < 4) {
//Il est possible de changer l'élixir par défaut
let altElixir = charAttribute(forgesort.charId, 'Elixir ' + elixir.rang);
if (altElixir.length > 0) {
elixir.nom = altElixir[0].get('current');
elixir.attrName = altElixir[0].get('current');
elixir.action = altElixir[0].get('max');
}
}
let nbElixirs = 0;
let attr = tokenAttribute(forgesort, 'elixir_' + elixir.attrName);
if (attr.length > 0) {
attr = attr[0];
nbElixirs = parseInt(attr.get('current'));
if (isNaN(nbElixirs) || nbElixirs < 0) nbElixirs = 0;
}
let nomElixir = elixir.nom;
let options = '';
let action;
if (elixirsACreer > 0 || (elixir.rang == 1 && fortifiantExtra > 0)) {
action = "!cof-creer-elixir ";
if (forgesort.token) action += forgesort.token.id;
else action += forgesort.charId;
action += ' ' + elixir.attrName;
options += bouton(action, nbElixirs, forgesort) + ' ';
} else {
options = nbElixirs + ' ';
}
if (nbElixirs > 0) {
let rang = voieDesElixirs;
let base_save = 10;
if (predicateAsBool(forgesort, 'boutefeu') &&
elixir.action.includes('--feu')) {
rang = rang + 1;
base_save = base_save + 2;
}
action = elixir.action;
action = action.replace(/\$rang/g, rang);
action = action.replace(/\$base_save/g, base_save);
action = action.replace(/\$INT/g, modCarac(forgesort, 'intelligence'));
action = action.replace(/\$SAG/g, modCarac(forgesort, 'sagesse'));
options += bouton(action, nomElixir, forgesort, {
ressource: attr
});
} else {
options += nomElixir;
}
addLineToFramedDisplay(display, options);
});
sendFramedDisplay(display);
});
}); //Fin du getSelected
}
function persoOfCharId(charId, pageId, errMsg) {
let token;
let tokensPersonnage =
findObjs({
_type: 'graphic',
_subtype: 'token',
represents: charId,
_pageid: pageId
});
if (tokensPersonnage.length === 0) {
tokensPersonnage =
findObjs({
_type: 'graphic',
_subtype: 'token',
represents: charId,
});
if (tokensPersonnage.length === 0) {
error("Impossible de trouver le token du personnage " + charId + " " + errMsg);
return;
}
if (tokensPersonnage.length > 1) {
let pageIds = characterPageIds(charId);
token = tokensPersonnage.find(function(t) {
return pageIds.has(t.get('pageid'));
});
}
} else if (tokensPersonnage.length > 1) {
log("Attention, plus d'un token pour le personnage " + charId + " " + errMsg);
}
if (token === undefined) token = tokensPersonnage[0];
return {
token,
charId
};
}
function proposerRenouveauElixirs(evt, attrs, options) {
let attrsNamed = allAttributesNamed(attrs, 'elixir');
if (attrsNamed.length === 0) return attrs;
// Trouver les forgesorts avec des élixirs sur eux
let forgesorts = {};
attrsNamed.forEach(function(attr) {
// Check de l'existence d'un créateur
let charId = attr.get('characterid');
let personnage = persoOfCharId(charId, options.pageId, "avec un élixir");
if (personnage === undefined) personnage = {
charId
};
let voieDesElixirs = rangVoieDesElixirs(personnage, true);
//TODO: réfléchir à une solution pour le renouveau des élixirs échangés
if (voieDesElixirs > 0) {
let elixirsDuForgesort = forgesorts[charId];
if (elixirsDuForgesort === undefined) {
elixirsDuForgesort = {
forgesort: personnage,
voieDesElixirs: voieDesElixirs,
elixirsParRang: {}
};
}
// Check de l'élixir à renouveler
let nomElixir = attr.get('name');
let typeElixir = listeElixirs.find(function(i) {
if (i.rang > voieDesElixirs) return false;
return "elixir_" + i.attrName == nomElixir;
});
if (typeElixir === undefined) {
error("Impossible de trouver l'élixir à renouveler");
return;
}
// Check des doses
let doses = attr.get("current");
if (isNaN(doses)) {
error("Erreur interne : élixir mal formé");
return;
}
if (doses > 0) {
// Tout est ok, création de l'item
let elixirArenouveler = {
typeElixir: typeElixir,
doses: doses
};
let elixirsParRang = elixirsDuForgesort.elixirsParRang;
if (elixirsParRang[typeElixir.rang] === undefined) {
elixirsParRang[typeElixir.rang] = [elixirArenouveler];
} else elixirsParRang[typeElixir.rang].push(elixirArenouveler);
forgesorts[charId] = elixirsDuForgesort;
}
}
});
// Display par personnage
for (const [forgesortCharId, elixirsDuForgesort] of Object.entries(forgesorts)) {
// Init du display pour le personnage
let displayOpt = {
chuchote: true
};
let allPlayers = getPlayerIds({
charId: forgesortCharId
});
let playerId;
if (allPlayers === undefined || allPlayers.length < 1) {
displayOpt.chuchote = 'gm';
} else {
playerId = allPlayers[0];
}
let forgesort = elixirsDuForgesort.forgesort;
let opt = {};
if (predicateAsBool(forgesort, 'fortifiantAvance')) {
opt.maxVal = 2;
}
setTokenAttr(forgesort, 'elixirsACreer', elixirsDuForgesort.voieDesElixirs * 2, evt, opt);
let display = startFramedDisplay(allPlayers[0], "Renouveler les élixirs", forgesort, displayOpt);
let actionToutRenouveler = "";
// Boucle par rang de rune
for (const rang in elixirsDuForgesort.elixirsParRang) {
let elixirsDeRang = elixirsDuForgesort.elixirsParRang[rang];
if (elixirsDeRang === undefined || elixirsDeRang.length < 1) continue;
addLineToFramedDisplay(display, "Elixirs de rang " + rang, undefined, true);
let actionTout = '';
let ligneBoutons = '';
// Boucle par élixir de ce rang à renouveler
for (const i in elixirsDeRang) {
let elixir = elixirsDeRang[i];
// Boucle par dose
for (let j = 0; j < elixir.doses; j++) {
let action = "!cof-creer-elixir ";
if (forgesort.token) action += forgesort.token.id;
else action += forgesort.charId;
action += ' ' + elixir.typeElixir.attrName;
actionTout += action + "\n";
actionToutRenouveler += action + "\n";
let nomElixirComplet = elixir.typeElixir.nom;
ligneBoutons += bouton(action, nomElixirComplet.replace("Elixir de ", "").replace("Elixir d'", ""), forgesort);
}
}
ligneBoutons += bouton(actionTout, "Tout", forgesort, {
buttonStyle: "background-color: blue;"
});
addLineToFramedDisplay(display, ligneBoutons, undefined, true);
}
let boutonToutRenouveler =
bouton(actionToutRenouveler, "Tout renouveler", forgesort, {
buttonStyle: "background-color: green;"
});
addLineToFramedDisplay(display, boutonToutRenouveler, undefined, true);
sendFramedDisplay(display);
}
return removeAllAttributes("elixir", evt, attrs);
}
function listeRunes(rang) {
let liste = [];
if (rang < 2) return liste;
liste.push({
nom: "Rune d'énergie",
action: "!cof-rune-energie",
attrName: "runeForgesort_énergie",
rang: 2
});
if (rang < 3) return liste;
liste.push({
nom: "Rune de protection",
action: "!cof-rune-protection",
attrName: "runeForgesort_protection",
rang: 3
});
if (rang < 4) return liste;
liste.push({
nom: "Rune de puissance",
action: "!cof-rune-puissance",
attrName: "runeForgesort_puissance",
rang: 4
});
return liste;
}
function gestionRunes(msg) {
getSelected(msg, function(selected, playerId) {
const player = getObj('player', playerId);
if (player === undefined) {
error("Impossible de trouver le joueur", playerId);
return;
}
iterSelected(selected, function(forgesort) {
let voieDesRunes = predicateAsInt(forgesort, 'voieDesRunes', 0);
if (voieDesRunes < 1) {
sendPerso(forgesort, "ne connaît pas la Voie des Runes.");
return;
} else if (voieDesRunes < 2) {
sendPerso(forgesort, "ne peut écrire que des Runes de défense.");
return;
}
let titre = "Création de runes";
let display = startFramedDisplay(playerId, titre, forgesort, {
chuchote: true
});
listeRunes(voieDesRunes).forEach(function(rune) {
let action = "!cof-creer-rune " + forgesort.token.id + " @{target|token_id} " + rune.rang;
if (rune.rang === 4) action += " ?{Numéro de l'arme de la cible?}";
let options = bouton(action, rune.nom, forgesort);
addLineToFramedDisplay(display, options);
});
sendFramedDisplay(display);
});
}); //Fin du getSelected
}
//!cof-creer-rune token_id rune
function creerRune(msg) {
const options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 4) {
error("Pas assez d'arguments pour !cof-creer-runes", msg.content);
return;
}
let forgesort = persoOfId(cmd[1], cmd[1], options.pageId);
if (forgesort === undefined) {
if (msg.selected && msg.selected.length == 1) {
forgesort = persoOfId(msg.selected[0]._id);
}
if (forgesort === undefined) {
error("Impossible de savoir qui crée la rune", cmd);
return;
}
}
let target = persoOfId(cmd[2], cmd[2], options.pageId);
if (target === undefined) {
error("Impossible de savoir à qui octroyer la rune", cmd);
return;
}
let voieDesRunes = predicateAsInt(forgesort, 'voieDesRunes', 0);
if (voieDesRunes < 1) {
sendPerso(forgesort, "ne connaît pas la Voie des Runes");
return;
} else if (voieDesRunes < 2) {
sendPerso(forgesort, "ne peut écrire que des Runes de défense.");
return;
}
let rune = listeRunes(voieDesRunes).find(function(i) {
return i.rang == cmd[3];
});
if (rune === undefined) {
error(nomPerso(forgesort) + " est incapable de créer " + cmd[3], cmd);
return;
}
let labelArme;
if (rune.rang == 4) {
if (cmd.length < 5) {
error("La rune de puissance nécessite de choisir un label d'arme.");
return;
}
labelArme = parseInt(cmd[4]);
}
const evt = {
type: "Création de rune"
};
addEvent(evt);
if (ficheAttributeAsBool(forgesort, 'option_pm', true)) {
if (reglesOptionelles.mana.val.mana_totale.val) {
switch (rune.rang) {
case 2:
options.mana = 3;
break;
case 3:
options.mana = 6;
break;
case 4:
options.mana = 10;
break;
}
} else if (rune.rang > 2) {
options.mana = rune.rang - 2;
}
}
let attrName = rune.attrName;
let message = "reçoit ";
let typeRune;
switch (rune.rang) {
case 2:
typeRune = "une rune d'énergie";
break;
case 3:
typeRune = "une rune de protection";
break;
case 4:
typeRune = "une rune de puissance sur son ";
let arme = getWeaponStats(target, labelArme);
if (arme) typeRune += arme.name;
else typeRune += "arme " + labelArme;
attrName += "(" + labelArme + ")";
break;
}
message += typeRune;
if (attributeAsInt(target, attrName, 0) > 0) {
error("La cible possède déjà une rune " + typeRune, cmd);
return;
}
if (options.mana !== undefined && limiteRessources(forgesort, options, undefined, "créer " + typeRune, evt)) return;
setTokenAttr(target, attrName, 1, evt, {
msg: message,
maxVal: forgesort.charId
});
if (rune.rang === 3 && reglesOptionelles.dommages.val.max_rune_protection.val) {
setTokenAttr(target, "runeProtectionMax", voieDesRunes * 10, evt, {
maxVal: forgesort.charId
});
}
}
//TODO: passer pageId en argument au lieu de prendre la page des joueurs
function proposerRenouveauRunes(evt, attrs, options) {
let attrsNamed = allAttributesNamed(attrs, 'runeForgesort');
if (attrsNamed.length === 0) return attrs;
// Filtrer par Forgesort, dans l'éventualité qu'il y en ait plusieurs actifs
let forgesorts = {};
attrsNamed.forEach(function(attr) {
// Check de l'existence d'un créateur
let forgesortId = attr.get('max');
if (forgesortId === undefined) {
error("Impossible de retrouver le créateur de la rune : " + attr);
return;
}
let runesDuForgesort = forgesorts[forgesortId];
if (runesDuForgesort === undefined) {
// Check de l'existence d'un token présent pour le créateur
let forgesort = persoOfCharId(forgesortId, options.pageId, "ayant créé une rune");
if (forgesort === undefined) {
attr.remove();
return;
}
// Check du perso voie des Runes
let voieDesRunes = predicateAsInt(forgesort, 'voieDesRunes', 0);
if (voieDesRunes < 1) {
sendPerso(forgesort, "ne connaît pas la Voie des Runes");
return;
} else if (voieDesRunes < 2) {
sendPerso(forgesort, "ne peut écrire que des Runes de défense.");
return;
}
runesDuForgesort = {
forgesort: forgesort,
voieDesRunes: voieDesRunes,
runesParRang: {}
};
}
// Check de la présence d'un token pour la cible
let targetCharId = attr.get('characterid');
let target = persoOfCharId(targetCharId, options.pageId, "ayant une rune");
if (target === undefined) return;
// Check de la rune à renouveler
let runeName = attr.get('name');
let typeRune =
listeRunes(runesDuForgesort.voieDesRunes).find(function(i) {
return i.attrName == runeName.split("(")[0];
});
if (typeRune === undefined) {
error("Impossible de trouver la rune à renouveler");
return;
}
// Tout est ok, création de l'item
let runeARenouveler = {
target: target,
typeRune: typeRune,
runeName: runeName
};
let runesParRang = runesDuForgesort.runesParRang;
if (runesParRang[typeRune.rang] === undefined) {
runesParRang[typeRune.rang] = [runeARenouveler];
} else runesParRang[typeRune.rang].push(runeARenouveler);
forgesorts[forgesortId] = runesDuForgesort;
});
// Display par personnage
for (const [forgesortCharId, runesDuForgesort] of Object.entries(forgesorts)) {
// Init du desplay pour le personnage
let displayOpt = {
chuchote: true
};
let allPlayers = getPlayerIds({
charId: forgesortCharId
});
let playerId;
if (allPlayers === undefined || allPlayers.length < 1) {
displayOpt.chuchote = 'gm';
} else {
playerId = allPlayers[0];
}
let forgesort = runesDuForgesort.forgesort;
let display = startFramedDisplay(allPlayers[0], "Renouveler les runes", forgesort, displayOpt);
let actionToutRenouveler = "";
// Boucle par rang de rune
for (const rang in runesDuForgesort.runesParRang) {
let runesDeRang = runesDuForgesort.runesParRang[rang];
if (runesDeRang === undefined || runesDeRang.length < 1) continue;
addLineToFramedDisplay(display, runesDeRang[0].typeRune.nom, undefined, true);
let actionTout = "";
let ligneBoutons = "";
// Boucle par rune de ce rang à renouveler
for (const i in runesDeRang) {
let rune = runesDeRang[i];
let action =
"!cof-creer-rune " + forgesort.token.id + " " + rune.target.token.id + " " + rang;
if (rang == 4) {
let runeName = rune.runeName;
action += " " + runeName.substring(runeName.indexOf("(") + 1, runeName.indexOf(")"));
}
actionTout += action + "\n";
actionToutRenouveler += action + "\n";
ligneBoutons += bouton(action, nomPerso(rune.target), forgesort);
}
ligneBoutons += bouton(actionTout, "Tout", forgesort, {
buttonStyle: "background-color: blue;"
});
addLineToFramedDisplay(display, ligneBoutons, undefined, true);
}
let boutonToutRenouveler =
bouton(actionToutRenouveler, "Tout renouveler", forgesort, {
buttonStyle: "background-color: green;"
});
addLineToFramedDisplay(display, boutonToutRenouveler, undefined, true);
sendFramedDisplay(display);
}
return removeAllAttributes("runeForgesort", evt, attrs);
}
function parseRageDuBerserk(msg) {
let typeRage = 'rage';
if (msg.content.includes(' --furie')) typeRage = 'furie';
getSelected(msg, function(selection, playerId) {
if (selection.length === 0) {
sendPlayer(msg, "Pas de token sélectionné pour la rage", playerId);
return;
}
const options = parseOptions(msg);
if (options === undefined) return;
if (options.son) playSound(options.son);
let persos = [];
iterSelected(selection, function(perso) {
persos.push(perso);
});
doRageDuBerserk(persos, typeRage, options);
});
}
function doRageDuBerserk(persos, typeRage, options) {
const evt = {
type: "rage",
action: {
persos: persos,
typeRage: typeRage,
options: options
}
};
addEvent(evt);
persos.forEach(function(perso) {
let attrRage = tokenAttribute(perso, 'rageDuBerserk');
if (attrRage.length > 0) {
attrRage = attrRage[0];
typeRage = attrRage.get('current');
let difficulte = 13;
if (typeRage == 'furie') difficulte = 16;
//Jet de sagesse difficulté 13 pou 16 pour sortir de cet état
let display = startFramedDisplay(options.playerId, "Essaie de calmer sa " + typeRage, perso);
let testId = 'rageDuBerserk_' + perso.token.id;
testCaracteristique(perso, 'SAG', difficulte, testId, options, evt,
function(tr) {
addLineToFramedDisplay(display, "Résultat du jet de SAG : " + tr.texte);
if (tr.reussite) {
addLineToFramedDisplay(display, "C'est réussi, " + nomPerso(perso) + " se calme." + tr.modifiers);
removeTokenAttr(perso, 'rageDuBerserk', evt);
} else {
let msgRate = "C'est raté, " + nomPerso(perso) + " reste enragé" + tr.rerolls + tr.modifiers;
addLineToFramedDisplay(display, msgRate);
}
sendFramedDisplay(display);
});
} else {
//Le barbare passe en rage
if (limiteRessources(perso, options, 'rageDuBerserk', "entrer en rage du berserk", evt)) {
return;
}
if (!stateCOF.combat) {
initiative([{
_id: perso.token.id
}], evt);
}
setTokenAttr(perso, 'rageDuBerserk', typeRage, evt, {
msg: "entre dans une " + typeRage + " berserk !"
});
}
});
}
//!cof-arme-secrete @{selected|token_id} @{target|token_id}
function parseArmeSecrete(msg) {
const options = parseOptions(msg);
let cmd = msg.content.split(' ');
if (cmd.length < 3) {
error("Il faut deux arguments à !cof-arme-secrete", cmd);
return;
}
let barde = persoOfId(cmd[1]);
let cible = persoOfId(cmd[2]);
if (barde === undefined || cible === undefined) {
error("Token non valide pour l'arme secrète", cmd);
return;
}
if (attributeAsInt(barde, 'armeSecreteBardeUtilisee')) {
sendPerso(barde, "a déjà utilisé son arme secrète durant ce combat");
return;
}
doArmeSecrete(barde, cible, options);
}
function doArmeSecrete(perso, cible, options) {
const evt = {
type: 'armeSecrete',
action: {
perso: perso,
cible: cible,
options: options
}
};
addEvent(evt);
if (!stateCOF.combat) {
initiative([{
_id: perso.token.id
}, {
_id: cible.token.id
}], evt);
}
setTokenAttr(perso, 'armeSecreteBardeUtilisee', true, evt);
let intCible = ficheAttributeAsInt(cible, 'intelligence', 10);
let testId = 'armeSecreteBarde';
testCaracteristique(perso, 'CHA', intCible, testId, options, evt, function(tr) {
let display = startFramedDisplay(options.playerId,
"Arme secrète", perso, {
perso2: cible
});
let line = "Jet de CHA : " + tr.texte;
if (tr.reussite) {
line += " ≥ " + intCible + tr.modifiers;
addLineToFramedDisplay(display, line);
addLineToFramedDisplay(display, nomPerso(cible) + " est complètement déstabilisé");
setAttrDuree(cible, 'armeSecreteBarde', 1, evt);
} else {
line += " < " + intCible + tr.rerolls + tr.modifiers;
addLineToFramedDisplay(display, line);
addLineToFramedDisplay(display, nomPerso(cible) + " reste insensible au charme de " + nomPerso(perso));
}
sendFramedDisplay(display);
}); //fin testCarac
}
function nouveauNomDePerso(nom) {
let characters = findObjs({
_type: 'character'
});
characters = characters.map(function(c) {
return c.get('name');
});
let trouve = characters.indexOf(nom);
if (trouve < 0) return nom;
let n = 2;
while (1) {
let nomP = nom + ' ' + n;
trouve = characters.indexOf(nomP);
if (trouve < 0) return nomP;
n++;
}
}
//Crée un nouveau personnage (de type PNJ par défaut)
//spec contient les charactéristiques, attributs et abilities
// - attributesFiche contient les attributs définis dans la fiche
// nom_attribut: valeur
// - pv (permet d'être indépendant de PJ ou PNJ)
// - attaques, liste d'attaques, chacune avec (nom, atk, dmnbde, dmde, dm,...)
// - attributes autres attributs (name, current, max)
// - abilities (name, action), toujours rajoutées à la liste d'actions
// - actions (titre, code), ajoutées aux listes d'actions
function createCharacter(nom, playerId, avatar, token, spec, evt, createur) {
let res = createObj('character', {
name: nom,
avatar: avatar,
controlledby: playerId
});
if (!res) return;
let charId = res.id;
if (token) {
token.set('represents', charId);
}
let attrs = findObjs({
_type: 'attribute',
_characterid: charId,
});
let attrVersion =
attrs.find(function(a) {
return a.get('name').toLowerCase() == 'version';
});
if (!attrVersion) {
createObj('attribute', {
_characterid: charId,
name: 'version',
current: versionFiche
});
}
attrVersion =
attrs.find(function(a) {
return a.get('name').toLowerCase() == 'scriptVersion';
});
if (!attrVersion) {
createObj('attribute', {
_characterid: charId,
name: 'scriptVersion',
current: true,
max: stateCOF.version
});
}
let pnj = true;
if (spec.attributesFiche) {
if (spec.attributesFiche.type_personnage == 'PJ') pnj = false;
for (let attrName in spec.attributesFiche) {
/*jshint loopfunc: true */
let attr =
attrs.filter(function(a) {
return a.get('name') == attrName;
});
if (attr.length === 0) {
createObj('attribute', {
_characterid: charId,
name: attrName,
current: spec.attributesFiche[attrName]
});
} else {
attr[0].set('current', spec.attributesFiche[attrName]);
}
}
}
if (pnj &&
(!spec.attributesFiche || spec.attributesFiche.type_personnage === undefined)) {
createObj('attribute', {
_characterid: charId,
name: 'type_personnage',
current: 'PNJ'
});
}
if (pnj) {
createObj('attribute', {
_characterid: charId,
name: 'tab',
current: 'carac. pnj'
});
}
if (spec.pv) {
let pvAttr = attrs.filter(function(a) {
return a.get('name').toUpperCase() == 'PV';
});
if (pvAttr.length === 0) {
pvAttr = createObj('attribute', {
_characterid: charId,
name: 'PV',
current: spec.pv,
max: spec.pv
});
} else {
pvAttr = pvAttr[0];
pvAttr.set('current', spec.pv);
pvAttr.set('max', spec.pv);
}
if (pnj) {
pvAttr = attrs.filter(function(a) {
return a.get('name').toLowerCase() == 'pnj_pv';
});
if (pvAttr.length === 0) {
pvAttr = createObj('attribute', {
_characterid: charId,
name: 'pnj_pv',
current: spec.pv,
max: spec.pv
});
} else {
pvAttr = pvAttr[0];
pvAttr.set('current', spec.pv);
pvAttr.set('max', spec.pv);
}
}
if (token) {
token.set('bar1_link', pvAttr.id);
token.set('bar1_value', spec.pv);
token.set('bar1_max', spec.pv);
}
}
if (spec.attaques) {
let maxAttackLabel = 0;
let prefix_attaques = 'repeating_armes_';
if (pnj) prefix_attaques = 'repeating_pnjatk_';
spec.attaques.forEach(function(att) {
let id = generateRowID();
let pref = prefix_attaques + id + '_arme';
_.forEach(att, function(value, field) {
createObj('attribute', {
_characterid: charId,
name: pref + field,
current: value
});
});
maxAttackLabel++;
createObj('attribute', {
_characterid: charId,
name: pref + 'label',
current: maxAttackLabel
});
});
createObj('attribute', {
_characterid: charId,
name: 'max_attack_label',
current: maxAttackLabel
});
}
if (spec.attributes) {
spec.attributes.forEach(function(a) {
a._characterid = charId;
let attr = createObj('attribute', a);
if (createur && a.lie) {
addEffetTemporaireLie(createur, attr, evt);
}
});
}
if (spec.abilities) {
spec.abilities.forEach(function(a) {
a._characterid = charId;
a.istokenaction = true;
createObj('ability', a);
});
}
if (spec.actions) {
let rang = 0;
spec.actions.forEach(function(a) {
rang++;
let pref = 'repeating_actions_' + generateRowID() + '_';
createObj('attribute', {
name: pref + 'actionoptflag',
current: 'off',
characterid: charId,
});
createObj('attribute', {
name: pref + 'actionrang',
current: rang,
characterid: charId,
});
createObj('attribute', {
name: pref + 'actiontitre',
current: a.titre,
characterid: charId,
});
if (a.code) {
createObj('attribute', {
name: pref + 'actioncode',
current: a.code,
characterid: charId,
});
}
});
if (rang > 0) {
createObj('attribute', {
name: 'maxrangaction',
current: rang,
characterid: charId
});
}
}
createObj('attribute', {
name: 'montrerarmeenmain',
current: 0,
characterid: charId
});
if (token) setDefaultTokenForCharacter(res, token);
return res;
}
//!cof-animer-arbre lanceur-id target-id [rang]
function animerUnArbre(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 3) {
error("cof-animer-arbre attend 2 arguments", msg.content);
return;
}
let druide = persoOfId(cmd[1], cmd[1], options.pageId);
if (druide === undefined) {
error("Le premier argument de !cof-animer-arbre n'est pas un token valie", cmd);
return;
}
let tokenArbre = getObj('graphic', cmd[2]);
if (tokenArbre === undefined) {
error("Le deuxième argument de !cof-animer-arbre n'est pas un token", cmd);
return;
}
if (tokenArbre.get('represents') !== '') {
sendPerso(druide, "ne peut pas animer " + tokenArbre.get('name'));
return;
}
if (options.portee !== undefined) {
let dist = distanceCombat(druide.token, tokenArbre, options.pageId);
if (dist > options.portee) {
sendPerso(druide, "est trop loin de l'arbre");
return;
}
}
let rang = predicateAsInt(druide, 'voieDesVegetaux', 3);
if (cmd.length > 3) { //Le rang est spécifié en argument optionnel
let cmd3 = parseInt(cmd[3]);
if (isNaN(cmd3) || cmd3 < 1) {
error("Le rang n'est pas un nombre valie. On utilise " + rang + " à la place", cmd);
} else rang = cmd3;
}
const evt = {
type: "Animation d'un arbre"
};
addEvent(evt);
if (limiteRessources(druide, options, 'animerUnArbre', 'animer un arbre', evt)) return;
if (!stateCOF.combat) {
initPerso(druide, evt);
}
let niveau = ficheAttributeAsInt(druide, 'niveau', 1);
let nomArbre = nouveauNomDePerso('Arbre animé');
let avatar = "https://s3.amazonaws.com/files.d20.io/images/42323556/6qxlm965aFhBXGoYFy5fqg/thumb.png?1510582137";
let specArbre = {
pv: rang * 10,
attributesFiche: {
type_personnage: 'PNJ',
niveau: niveau,
force: 18,
pnj_for: 4,
dexterite: 7,
pnj_dex: -2,
constitution: 20,
pnj_con: 5,
intelligence: 8,
pnj_int: -1,
sagesse: 14,
pnj_sag: 2,
charisme: 6,
pnj_cha: -2,
DEFDIV: 5,
pnj_def: 13,
pnj_init: 7,
RDS: '10/feu_hache',
race: 'arbre',
taille: 'grand'
},
attaques: [{
nom: 'Branches',
atk: niveau,
dmnbde: 1,
dmde: 6,
dm: 3,
typedegats: 'contondant',
}],
attributes: [{
name: 'arbreAnime',
current: niveau,
max: getInit(),
lie: options.mana !== undefined
}]
};
let charArbre = createCharacter(nomArbre, options.playerId, avatar, tokenArbre, specArbre, evt, druide);
evt.characters = [charArbre];
sendChar(charArbre.id, "commence à s'animer", true);
initiative([{
_id: tokenArbre.id
}], evt);
// Ajout de l'arbre animé aux alliés du Druide
let alliesDruide = alliesParPerso[druide.charId] || new Set();
alliesDruide.add(charArbre.id);
alliesParPerso[druide.charId] = alliesDruide;
}
//!cof-rune-protection
function runeProtection(msg) {
if (!stateCOF.combat) {
sendPlayer(msg, "On ne peut utiliser les runes de protection qu'en combat");
return;
}
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
let evt = lastEvent();
if (cmd !== undefined && cmd.length > 1) { //On relance pour un événement particulier
evt = findEvent(cmd[1]);
if (evt === undefined) {
error("L'action est trop ancienne ou a été annulée", cmd);
return;
}
}
if (evt.type != 'Attaque') {
sendChat('', "la dernière action n'est pas une attaque réussie, trop tard pour absorber l'attaque précédente");
return;
}
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
error("Personne n'est sélectionné pour utiliser une rune", msg);
return;
}
let action = evt.action;
iterSelected(selected, function(perso) {
if (!peutController(msg, perso)) {
sendPlayer(msg, "pas le droit d'utiliser ce bouton", playerId);
return;
}
let cible = action.cibles.find(function(target) {
return (target.token.id === perso.token.id);
});
if (cible === undefined) {
sendPerso(perso, "n'est pas la cible de la dernière attaque");
return;
}
if (!attributeAsBool(perso, 'runeForgesort_protection')) {
sendPerso(perso, "n'a pas de rune de protection");
return;
}
if (attributeAsInt(perso, 'limiteParCombat_runeForgesort_protection', 1) < 1) {
sendPerso(perso, "a déjà utilisé sa rune de protection durant ce combat");
return;
}
action.choices = action.choices || {};
action.choices[perso.token.id] = action.choices[perso.token.id] || {};
action.choices[perso.token.id].runeForgesort_protection = true;
}); //fin iterSelected
let options = action.currentOptions || {};
options.rolls = action.rolls;
options.choices = action.choices;
resolvePreDmgOptions(action.attaquant, action.ciblesTouchees, action.echecCritique, action.attackLabel, action.weaponStats, action.attackd20roll, action.display, options, evt, action.explications, action.pageId, action.cibles);
}); //fin getSelected
}
function appliquerRuneDeProtection(cible, options, evt) {
if (reglesOptionelles.dommages.val.max_rune_protection.val) {
cible.utiliseRuneProtectionMax = attributeAsInt(cible, 'runeProtectionMax', 30);
} else {
cible.utiliseRuneProtection = true;
}
removePreDmg(options, cible);
}
//!cof-delivrance @{selected|token_id} @{target|token_id}
function delivrance(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 2) {
error("cof-delivrance attend un argument", msg.content);
return;
}
let pretre = persoOfId(cmd[1], cmd[1], options.pageId);
if (pretre === undefined) {
error("Le premier argument de !cof-delivrance n'est pas un token valide", msg.content);
return;
}
let c;
if (cmd.length > 2) c = persoOfId(cmd[2], cmd[2], options.pageId);
let cibles = [c];
if (c === undefined) {
cibles = [];
getSelected(msg, function(selected, playerId) {
iterSelected(selected, function(cible) {
cibles.push(cible);
});
});
if (cibles.length < 1) {
error("Le deuxième argument de !cof-delivrance n'est pas un token valide", msg.content);
return;
}
}
if (options.portee !== undefined) {
cibles = cibles.filter(function(cible) {
let dist = distanceCombat(pretre.token, cible.token, options.pageId);
if (dist > options.portee) {
sendPerso(pretre, "est trop loin de " + nomPerso(cible));
return false;
}
return true;
});
}
if (cibles.length < 1) return;
const evt = {
type: "Délivrance",
deletedAttributes: []
};
addEvent(evt);
if (limiteRessources(pretre, options, 'délivrance', 'délivrance', evt)) return;
cibles.forEach(function(cible) {
let display = startFramedDisplay(getPlayerIdFromMsg(msg), 'Délivrance', pretre, {
perso2: cible
});
let printEffet = function(message) {
addLineToFramedDisplay(display, nomPerso(cible) + ' ' + message);
};
let optFin = {
print: printEffet,
pageId: options.pageId
};
_.each(messageEffetTemp, function(effet, nomEffet) {
if (effet.prejudiciable) {
//Attention, ne fonctionne pas avec les effets génériques
let attr = tokenAttribute(cible, nomEffet);
if (attr.length > 0)
finDEffet(attr[0], nomEffet, attr[0].get('name'), cible.charId, evt, optFin);
}
});
_.each(messageEffetCombat, function(effet, nomEffet) {
if (effet.prejudiciable) {
let attr = tokenAttribute(cible, nomEffet);
if (attr.length > 0) {
printEffet(messageFin(cible, effet));
evt.deletedAttributes.push(attr[0]);
attr[0].remove();
}
}
});
_.each(messageEffetIndetermine, function(effet, nomEffet) {
if (effet.prejudiciable) {
let attr = tokenAttribute(cible, nomEffet);
if (attr.length > 0) {
printEffet(messageFin(cible, effet));
evt.deletedAttributes.push(attr[0]);
attr[0].remove();
}
}
});
if (attributeAsBool(cible, 'malediction')) {
printEffet("n'est plus maudite");
removeTokenAttr(cible, 'malediction', evt);
}
if (attributeAsBool(cible, 'pointsDeSang')) {
printEffet("n'a plus de point de sang.");
removeTokenAttr(cible, 'pointsDeSang', evt);
}
//On enlève les états préjudiciables
if (getState(cible, 'aveugle')) {
printEffet("retrouve la vue");
setState(cible, 'aveugle', false, evt);
}
if (getState(cible, 'affaibli')) {
printEffet("retrouve des forces");
setState(cible, 'affaibli', false, evt);
}
if (getState(cible, 'etourdi')) {
printEffet("retrouve ses esprits");
setState(cible, 'etourdi', false, evt);
}
if (getState(cible, 'paralyse')) {
printEffet("peut à nouveau bouger");
setState(cible, 'paralyse', false, evt);
}
if (getState(cible, 'ralenti')) {
printEffet("retrouve une vitesse normale");
setState(cible, 'ralenti', false, evt);
}
if (getState(cible, 'endormi')) {
printEffet("se réveille");
setState(cible, 'endormi', false, evt);
}
if (getState(cible, 'apeure')) {
printEffet("reprend courage");
setState(cible, 'apeure', false, evt);
}
//Régénération d'une carac affaiblie de 1d4, si il y en a.
if (attributeAsInt(cible, 'affaiblissementdesagesse', 0) > 0) {
let d4 = rollDePlus(4);
diminueAffaiblissement(cible, 'sagesse', d4.val, evt);
printEffet("récupère " + d4.roll + " points de sagesse");
} else if (attributeAsInt(cible, 'affaiblissementdecharisme', 0) > 0) {
let d4 = rollDePlus(4);
diminueAffaiblissement(cible, 'charisme', d4.val, evt);
printEffet("récupère " + d4.roll + " points de charisme");
} else if (attributeAsInt(cible, 'affaiblissementdeintelligence', 0) > 0) {
let d4 = rollDePlus(4);
diminueAffaiblissement(cible, 'intelligence', d4.val, evt);
printEffet("récupère " + d4.roll + " points d'intelligence");
}
if (attributeAsInt(cible, 'affaiblissementdeconstitution', 0) > 0) {
let d4 = rollDePlus(4);
diminueAffaiblissement(cible, 'constitution', d4.val, evt);
printEffet("récupère " + d4.roll + " points de constitution");
} else if (attributeAsInt(cible, 'affaiblissementdeforce', 0) > 0) {
let d4 = rollDePlus(4);
diminueAffaiblissement(cible, 'force', d4.val, evt);
printEffet("récupère " + d4.roll + " points de force");
} else if (attributeAsInt(cible, 'affaiblissementdedexterite', 0) > 0) {
let d4 = rollDePlus(4);
diminueAffaiblissement(cible, 'dexterite', d4.val, evt);
printEffet("récupère " + d4.roll + " points de dextérité");
}
sendFramedDisplay(display);
});
}
function guerisonPerso(perso, evt, lanceur) {
let msgSoin;
if (lanceur) {
if (lanceur.token.id == perso.token.id) {
msgSoin = 'se soigne';
} else {
msgSoin = 'soigne ' + nomPerso(perso);
}
} else {
msgSoin = 'récupère';
}
msgSoin += ' de toutes les blessures subies';
if (lanceur) sendPerso(lanceur, msgSoin);
else sendPerso(perso, msgSoin);
if (getState(perso, 'blesse')) {
setState(perso, 'blesse', false, evt);
} else { //On peut bien faire récupérer un PR
let d = rajouterPointDeRecuperation(perso, evt);
if (d) sendPerso(perso, "récupère un point de récupération");
}
let soins = perso.token.get('bar1_max') - perso.token.get('bar1_value');
if (isNaN(soins)) {
updateCurrentBar(perso, 1, perso.token.get('bar1_max'), evt);
return;
}
//Les affaiblissements de caractéristiques
allCaracs.forEach(function(carac) {
let malus = attributeAsInt(perso, 'affaiblissementde' + carac, 0);
if (malus > 0) {
diminueAffaiblissement(perso, carac, malus, evt, malus);
sendPerso(perso, "récupère " + malus + " points " + deCarac(carac));
}
});
//La putréfaction des momies
if (attributeAsBool(perso, 'putrefaction')) {
finDEffetDeNom(perso, 'putrefaction', evt);
}
if (soins <= 0) {
//Rien d'autre à faire (le script ne gère pas encore le reste)
return;
}
soigneToken(perso, soins, evt);
}
//!cof-guerison @{selected|token_id} @{target|token_id}
function guerison(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 2) {
error("cof-guerison attend le lanceur en argument", msg.content);
return;
}
let lanceur = persoOfId(cmd[1], cmd[1], options.pageId);
if (lanceur === undefined) {
error("Le premier argument de !cof-guerison n'est pas un token valide", msg.content);
return;
}
let c;
if (cmd.length > 2) c = persoOfId(cmd[2], cmd[2], options.pageId);
let cibles = [c];
if (c === undefined) {
cibles = [];
getSelected(msg, function(selected, playerId) {
iterSelected(selected, function(cible) {
cibles.push(cible);
});
});
if (cibles.length < 1) {
if (cmd.length < 3) {
error("cof-guerison attend le lanceur et la cible en argument", msg.content);
} else {
error("Le deuxième argument de !cof-guerison n'est pas un token valide", msg.content);
}
return;
}
}
if (options.dose === undefined && options.decrAttribute === undefined) {
options.limiteParJour = {
val: 1
};
}
if (options.portee !== undefined) {
cibles = cibles.filter(function(cible) {
const dist = distanceCombat(lanceur.token, cible.token, options.pageId);
if (dist > options.portee) {
sendPerso(lanceur, "est trop loin de " + nomPerso(cible));
return false;
}
return true;
});
}
if (cibles.length < 1) return;
const evt = {
type: "Guérison",
};
addEvent(evt);
if (limiteRessources(lanceur, options, 'guérison', 'guérison', evt)) return;
cibles.forEach(function(cible) {
guerisonPerso(cible, evt, lanceur);
});
if (options.messages) {
options.messages.forEach(function(m) {
sendChat('', m);
});
}
}
function armeDeContact(perso, arme, labelArmeDefaut, armeContact) {
if (arme) return arme;
arme = armesEnMain(perso);
if (arme === undefined && labelArmeDefaut)
arme = getWeaponStats(perso, labelArmeDefaut);
//L'arme doit être une arme de contact ?
if (armeContact && arme && arme.portee) {
sendPerso(perso, armeContact + " " + arme.name + " est une arme à distance.");
return;
}
if (arme) {
if (arme.deuxMains && attributeAsBool(perso, 'espaceExigu')) {
sendPerso(perso, "ne peut pas utiliser d'arme à deux mains dans un espace aussi exigu.");
return;
}
return arme;
}
arme = {
name: 'Attaque par défaut',
attSkillDiv: 0,
attSkill: "@{ATKCAC}",
crit: 20,
parDefaut: true,
};
return arme;
}
//parse the relevant options from weaponStats and adds them to option
//option must not be undefined
function parseWeaponStatsOptions(attaquant, defenseur, weaponStats, playerId, options) {
if (!weaponStats) return;
let tokenDef;
if (defenseur) tokenDef = defenseur.token;
let optArgs = [];
let commandArgs = [...optArgs];
addWeaponStatsToOptions(attaquant, weaponStats, optArgs, options);
parseAttackOptions(attaquant, optArgs, undefined, options.type, options, playerId, {}, tokenDef, weaponStats.label, weaponStats, options, commandArgs);
}
function attaqueContactOpposee(playerId, attaquant, defenseur, evt, options, callback) {
let explications = [];
options = options || {
pasDeDmg: true
};
options.contact = true;
entrerEnCombat(attaquant, [defenseur], explications, evt);
//Recherche des armes utilisées
let armeAttaquant =
armeDeContact(attaquant, options.armeAttaquant, options.labelArmeAttaquant, options.armeAttaquantContact);
let armeDefenseur =
armeDeContact(defenseur, options.armeDefenseur, options.labelArmeDefenseur, options.armeDefenseurContact);
let action = options.action || "Attaque opposée";
if (!armeAttaquant.parDefaut) {
action += " (" + armeAttaquant.name + ")";
}
const display = startFramedDisplay(playerId, action, attaquant, {
perso2: defenseur
});
let optionsAttaquant = {...options
};
parseWeaponStatsOptions(attaquant, defenseur, armeAttaquant, playerId, optionsAttaquant);
let expliquer = function(msg) {
explications.push(msg);
};
let diceOptions =
computeAttackDiceOptions(attaquant, armeAttaquant, expliquer, evt, optionsAttaquant);
let critAttaquant = diceOptions.crit;
let toEvaluateAttack =
attackExpression(attaquant, diceOptions, armeAttaquant);
try {
sendChat('', toEvaluateAttack, function(resAttack) {
let rollsAttack = resAttack[0];
if (options.rolls && options.rolls.attack)
rollsAttack = options.rolls.attack;
let afterEvaluateAttack = rollsAttack.content.split(' ');
let attRollNumber = rollNumber(afterEvaluateAttack[0]);
let attSkillNumber = rollNumber(afterEvaluateAttack[1]);
let d20rollAttaquant = rollsAttack.inlinerolls[attRollNumber].results.total;
effetAuD20(attaquant, d20rollAttaquant);
let attSkill = rollsAttack.inlinerolls[attSkillNumber].results.total;
let attBonus =
bonusAttaqueA(attaquant, armeAttaquant, evt, explications, optionsAttaquant);
let pageId = options.pageId || attaquant.token.get('pageid');
attBonus +=
bonusAttaqueD(attaquant, defenseur, 0, pageId, evt, explications, optionsAttaquant);
let attackRollAttaquant = d20rollAttaquant + attSkill + attBonus;
let attRollValue = buildinline(rollsAttack.inlinerolls[attRollNumber]);
attRollValue += (attSkill > 0) ? "+" + attSkill : (attSkill < 0) ? attSkill : "";
attRollValue += (attBonus > 0) ? "+" + attBonus : (attBonus < 0) ? attBonus : "";
if (options.bonusAttaqueAttaquant) {
options.bonusAttaqueAttaquant.forEach(function(bad) {
attRollValue += (bad.val > 0) ? "+" + bad.val : (bad.val < 0) ? bad.val : "";
attackRollAttaquant += bad.val;
if (bad.explication) explications.push(bad.explication);
});
}
addLineToFramedDisplay(display, "Jet de " + nomPerso(attaquant) + " : " + attRollValue);
let optionsDefenseur = {...options
};
parseWeaponStatsOptions(defenseur, attaquant, armeDefenseur, playerId, optionsDefenseur);
diceOptions =
computeAttackDiceOptions(defenseur, armeDefenseur, expliquer, evt, optionsDefenseur);
let critDefenseur = diceOptions.crit;
toEvaluateAttack = attackExpression(defenseur, diceOptions, armeDefenseur);
sendChat('', toEvaluateAttack, function(resAttack) {
let rollsAttack = resAttack[0];
if (options.rolls && options.rolls.attackDefenseur)
rollsAttack = options.rolls.attackDefenseur;
afterEvaluateAttack = rollsAttack.content.split(' ');
attRollNumber = rollNumber(afterEvaluateAttack[0]);
attSkillNumber = rollNumber(afterEvaluateAttack[1]);
let d20rollDefenseur = rollsAttack.inlinerolls[attRollNumber].results.total;
effetAuD20(defenseur, d20rollDefenseur);
attSkill = rollsAttack.inlinerolls[attSkillNumber].results.total;
attBonus =
bonusAttaqueA(defenseur, armeDefenseur, evt, explications, optionsDefenseur);
attBonus +=
bonusAttaqueD(defenseur, attaquant, 0, pageId, evt, explications, optionsDefenseur);
let attackRollDefenseur = d20rollDefenseur + attSkill + attBonus;
attRollValue = buildinline(rollsAttack.inlinerolls[attRollNumber]);
attRollValue += (attSkill > 0) ? "+" + attSkill : (attSkill < 0) ? attSkill : "";
attRollValue += (attBonus > 0) ? "+" + attBonus : (attBonus < 0) ? attBonus : "";
if (options.bonusAttaqueDefenseur) {
options.bonusAttaqueDefenseur.forEach(function(bad) {
attRollValue += (bad.val > 0) ? "+" + bad.val : (bad.val < 0) ? bad.val : "";
attackRollDefenseur += bad.val;
if (bad.explication) explications.push(bad.explication);
});
}
addLineToFramedDisplay(display, "Jet de " + nomPerso(defenseur) + " : " + attRollValue);
let resultat = {
rollAttaquant: attackRollAttaquant,
rollDefenseur: attackRollDefenseur,
armeAttaquant
};
if (d20rollAttaquant == 1 && d20rollDefenseur > 1) {
resultat.echec = true;
resultat.echecCritique = true;
diminueMalediction(attaquant, evt);
} else if (d20rollDefenseur == 1 && d20rollAttaquant > 1) {
resultat.succes = true;
resultat.echecCritiqueDefenseur = true;
diminueMalediction(defenseur, evt);
} else if (d20rollAttaquant >= critAttaquant && d20rollDefenseur < critDefenseur) {
resultat.succes = true;
resultat.critique = true;
diminueMalediction(defenseur, evt);
} else if (d20rollAttaquant < critAttaquant && d20rollDefenseur >= critDefenseur) {
resultat.succes = false;
resultat.critiqueDefenseur = true;
diminueMalediction(attaquant, evt);
} else if (attackRollAttaquant < attackRollDefenseur) {
resultat.echec = true;
diminueMalediction(attaquant, evt);
} else {
resultat.succes = true;
diminueMalediction(defenseur, evt);
}
callback(resultat, display, explications); //evt est mis à jour
}); //fin du sendchat pour jet du défenseur
}); //Fin du sendChat pour jet de l'attaquant
} catch (rollError) {
error("Erreur pendant le jet " + toEvaluateAttack + " dans attaqueContactOpposé", options);
}
}
function testAttaqueOpposee(msg) {
let cmd = msg.content.split(' ');
if (cmd.length < 3) {
error("Il faut 2 personnages pour un test d'attaque en opposition", cmd);
return;
}
const attaquant = persoOfId(cmd[1], cmd[1]);
const defenseur = persoOfId(cmd[2], cmd[2]);
if (attaquant === undefined) {
error("Le premier argument de !cof-test-attaque-opposee doit être un token valide", cmd[1]);
return;
}
if (defenseur === undefined) {
error("Le deuxième argument de !cof-test-attaque-opposee doit être un token valide", cmd[2]);
return;
}
const evt = {
type: "Test d'attaque opposée"
};
const options = {
pasDeDmg: true
};
if (cmd.length > 3) options.labelArmeAttaquant = cmd[3];
let playerId = getPlayerIdFromMsg(msg);
attaqueContactOpposee(playerId, attaquant, defenseur, evt, options,
function(res, display, explications) {
if (res.succes)
addLineToFramedDisplay(display, nomPerso(attaquant) + " remporte le test");
else
addLineToFramedDisplay(display, nomPerso(defenseur) + " remporte le test");
explications.forEach(function(expl) {
addLineToFramedDisplay(display, expl, 80);
});
sendFramedDisplay(display);
addEvent(evt);
});
}
//!cof-desarmer attaquant cible, optionellement un label d'arme
function desarmer(msg) {
let cmd = msg.content.split(' ');
if (cmd.length < 3) {
error("Il manque des arguments à !cof-desarmer", msg.content);
return;
}
const guerrier = persoOfId(cmd[1], cmd[1]);
if (guerrier === undefined) {
error("Le premier argument de !cof-desarmer n'est pas un token valide", cmd);
return;
}
const cible = persoOfId(cmd[2], cmd[2]);
if (cible === undefined) {
error("Le deuxième argument de !cof-desarmer n'est pas un token valide", cmd);
return;
}
let pageId = guerrier.token.get('pageid');
if (distanceCombat(guerrier.token, cible.token, pageId)) {
sendPerso(guerrier, "est trop loin de " + nomPerso(cible) + " pour le désarmer.");
return;
}
const options = {
action: "Désarmement",
armeContact: "doit porter une arme de contact pour désarmer son adversaire.",
pasDeDmg: true,
pageId: pageId,
};
//On cherche l'arme de la cible. On en aura besoin pour désarmer
let armeCible = armesEnMain(cible);
let optDegainer = {
seulementDroite: true
};
if (armeCible) {
options.armeDefenseur = armeCible;
if (armeCible.deuxMains) {
options.bonusAttaqueDefenseur = [{
val: 5,
explication: nomPerso(cible) + " porte une arme à 2 mains => +5 à son jet"
}];
}
} else {
armeCible = cible.armeGauche;
options.armeDefenseur = armeCible;
optDegainer = {
gauche: true
};
}
const evt = {
type: 'Désarmer'
};
if (cmd.length > 3) options.labelArmeAttaquant = cmd[3];
const playerId = getPlayerIdFromMsg(msg);
attaqueContactOpposee(playerId, guerrier, cible, evt, options,
function(res, display, explications) {
let resultat;
if (res.echecCritique) {
resultat = "échec critique";
} else if (res.echecCritiqueDefenseur) {
resultat = "succès, " + nomPerso(cible) + " laisse tomber son arme, difficile de la récupérer...";
degainerArme(cible, '', evt, optDegainer);
} else if (res.critique) {
resultat = "réussite critique : " + nomPerso(cible) + " est désarmé, et " + nomPerso(guerrier) + " empêche de reprendre l'arme";
degainerArme(cible, '', evt, optDegainer);
} else if (res.critiqueDefenseur) {
resultat = "échec, " + nomPerso(cible) + " garde son arme bien en main";
} else if (res.echec) {
resultat = "échec, " + nomPerso(guerrier) + " n'a pas réussi à désarmer son adversaire";
} else { //succès
degainerArme(cible, '', evt, optDegainer);
if (res.rollAttaquant > res.rollDefenseur + 9) {
resultat = "succès, " + nomPerso(guerrier) + " désarme son adversaire et l'empêche de récupérer son arme";
} else {
resultat = "succès, " + nomPerso(guerrier) + " désarme son adversaire.";
}
}
addLineToFramedDisplay(display, resultat);
explications.forEach(function(expl) {
addLineToFramedDisplay(display, expl, 80);
});
sendFramedDisplay(display);
addEvent(evt);
});
}
function appliquerBloquer(attaquant, cible, critique, evt, envoyerMessage) {
let msg;
if (envoyerMessage) msg = "est bloqué par son adversaire";
setAttrDuree(cible, 'bloqueManoeuvre', 1, evt, msg);
if (critique)
appliquerTenirADistance(attaquant, cible, false, evt, envoyerMessage);
}
function appliquerTenirADistance(attaquant, cible, critique, evt, envoyerMessage) {
let msg;
if (envoyerMessage) msg = "est tenu à distance par son adversaire";
setAttrDuree(
cible, 'tenuADistanceManoeuvre(' + attaquant.token.id + ')', 1, evt, msg);
if (critique) appliquerBloquer(attaquant, cible, false, evt, envoyerMessage);
}
const listeManoeuvres = {
aveugler: {
appliquer: function(attaquant, cible, critique, evt, envoyerMessage) {
let duree = 1;
if (critique) duree = randomInteger(6);
let msg;
if (envoyerMessage) msg = "est aveuglé par son adversaire";
setAttrDuree(
cible, 'aveugleManoeuvre', duree, evt, msg);
return critique; //Pour les DMs en plus
},
verbe: 'aveugler',
duelliste: false
},
bloquer: {
appliquer: appliquerBloquer,
penalitePlusPetit: true,
verbe: 'bloquer',
duelliste: true
},
desarmer: {
appliquer: function(attaquant, cible, critique, evt, envoyerMessage) {
degainerArme(cible, '', evt, {
seulementDroite: true
});
if (envoyerMessage) {
let msgDesarme = "est désarmé" + onGenre(cible, '', 'e');
if (critique) msgDesarme += ", son adversaire lui a pris son arme.";
else msgDesarme += ".";
sendPerso(cible, msgDesarme);
}
},
verbe: 'désarmer',
duelliste: true
},
faireDiversion: {
appliquer: function(attaquant, cible, critique, evt, envoyerMessage) {
let msg;
if (envoyerMessage) msg = "a son attention attirée ailleurs";
let malus = -5;
if (critique) malus = -10;
setAttrDuree(cible, 'diversionManoeuvre', 1, evt, msg);
setTokenAttr(cible, 'diversionManoeuvreValeur', malus, evt);
},
verbe: 'faire diversion sur',
duelliste: false
},
menacer: {
appliquer: function(attaquant, cible, critique, evt, envoyerMessage) {
let msg;
if (envoyerMessage) msg = "est sous le coup d'une menace";
let effet = 'menaceManoeuvre(' + attaquant.token.id;
if (critique) effet += ',crit';
effet += ')';
setAttrDuree(cible, effet, 1, evt, msg);
},
verbe: 'menacer',
duelliste: false
},
renverser: {
appliquer: function(attaquant, cible, critique, evt, envoyerMessage) {
if (envoyerMessage) sendPerso(cible, "tombe au sol");
setState(cible, 'renverse', true, evt);
return critique; //Pour les DM en plus
},
penalitePlusPetit: true,
verbe: 'renverser',
duelliste: true
},
repousser: {
appliquer: function(attaquant, cible, critique, evt, envoyerMessage) {
let distance = rollDePlus(6);
if (critique && distance < 3) distance = 3;
if (envoyerMessage)
sendPerso(cible, "est repoussé" + onGenre(cible, '', 'e') + " et doit reculer de " + distance.roll + "m.");
if (critique) setState(cible, 'renverse', true, evt);
},
penalitePlusPetit: true,
verbe: 'repousser',
duelliste: true
},
tenirADistance: {
appliquer: appliquerTenirADistance,
verbe: 'tenir à distance',
duelliste: true
}
};
//!cof-appliquer-manoeuvre id1 id2 effet attrId
//attrId est utilisé pour limiter le nombre d'utilisations
function appliquerManoeuvre(msg) {
let cmd = msg.content.split(' ');
if (cmd.length < 5) {
error("cof-appliquer-manoeuvre attend 4 arguments", msg.content);
return;
}
if (!_.has(listeManoeuvres, cmd[3])) {
error("Manoeuvre " + cmd[3] + " inconnue.", cmd);
return;
}
let limiteAttr = getObj('attribute', cmd[4]);
if (limiteAttr === undefined) {
sendPlayer(msg, "La manoeuvre a déjà été choisie");
return;
}
let attaquant = persoOfId(cmd[1], cmd[1]);
if (attaquant === undefined) {
error("Le premier argument de !cof-appliquer-maneuvre n'est pas un token valide", cmd);
return;
}
let cible = persoOfId(cmd[2], cmd[2]);
if (cible === undefined) {
error("Le deuxième argument de !cof-appliquer-manoeuvre n'est pas un token valide", cmd);
return;
}
let effet = listeManoeuvres[cmd[3]];
const evt = {
type: 'Application de manoeuvre',
deletedAttributes: [limiteAttr]
};
limiteAttr.remove();
effet.appliquer(attaquant, cible, false, evt, true);
addEvent(evt);
}
//!cof-manoeuvre id1 id2 effet
function manoeuvreRisquee(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
options.pasDeDmg = true;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 4) {
error("cof-manoeuvre attend 3 arguments", msg.content);
return;
}
if (!_.has(listeManoeuvres, cmd[3])) {
sendPlayer(msg, "Manoeuvre " + cmd[3] + " inconnue.");
return;
}
let effet = listeManoeuvres[cmd[3]];
let attaquant = persoOfId(cmd[1], cmd[1]);
if (attaquant === undefined) {
error("Le premier argument de !cof-maneuvre n'est pas un token valide", cmd);
return;
}
let cible = persoOfId(cmd[2], cmd[2]);
if (cible === undefined) {
error("Le deuxième argument de !cof-manoeuvre n'est pas un token valide", cmd);
return;
}
const evt = {
type: 'manoeuvre'
};
if (effet.penalitePlusPetit) {
let tailleAttaquant = taillePersonnage(attaquant);
let tailleCible = taillePersonnage(cible);
if (tailleAttaquant && tailleCible && tailleAttaquant < tailleCible) {
let penalite = 5 * (tailleAttaquant - tailleCible);
options.bonusAttaqueAttaquant = [{
val: penalite,
explication: nomPerso(attaquant) + " est plus petit que " + nomPerso(cible) + " => " + penalite + " Att"
}];
}
}
let playerId = getPlayerIdFromMsg(msg);
let manoeuvreDuelliste = effet.duelliste && predicateAsBool(attaquant, 'manoeuvreDuelliste');
attaqueContactOpposee(playerId, attaquant, cible, evt, options,
function(res, display, explications) {
let dmSupp;
if (res.succes) {
addLineToFramedDisplay(display, nomPerso(attaquant) + " réussit à " + effet.verbe + " " + nomPerso(cible));
dmSupp = effet.appliquer(attaquant, cible, res.critique, evt);
if (manoeuvreDuelliste && !dmSupp) {
let pageId = cible.token.get('pageid');
let defense = defenseOfPerso(attaquant, cible, pageId, evt, options);
dmSupp = res.rollAttaquant >= defense + 10;
}
} else {
addLineToFramedDisplay(display, nomPerso(attaquant) + " ne réussit pas à " + effet.verbe + " " + nomPerso(cible));
//Envoyer à la cible la possibilité d'appliquer un effet de son choix
}
explications.forEach(function(expl) {
addLineToFramedDisplay(display, expl, 80);
});
if (dmSupp) {
let actionGratuite =
"!cof-attack " + attaquant.token.id + " " + cible.token.id + " -1 --auto";
addLineToFramedDisplay(display, nomPerso(attaquant) + " fait en plus des dégâts à " + nomPerso(cible) + boutonSimple(actionGratuite, "lancer une attaque pour déterminer le montant"), 80);
}
sendFramedDisplay(display);
addEvent(evt);
/*if (dmSupp) {
turnAction(attaquant, playerId);
}*/
if (!res.succes && !manoeuvreDuelliste) {
let charCible = getObj('character', cible.charId);
if (charCible === undefined) {
error("Cible sans personnage associé", cible);
return;
}
let titre = "Choisir un effet contre " + nomPerso(attaquant);
//On crée un display sans le header
display = startFramedDisplay(undefined, titre, cible, {
retarde: true
});
//Attribut pour empecher plusieurs utilisations
let attrLimit = createObj('attribute', {
_characterid: cible.charId,
name: 'limiteApplicationManoeuvre',
current: '1'
});
for (let man in listeManoeuvres) {
let appliquerManoeuvre = '!cof-appliquer-manoeuvre ' + cible.token.id + ' ' + attaquant.token.id + ' ' + man + ' ' + attrLimit.id;
let ligneManoeuvre = boutonSimple(appliquerManoeuvre, man);
addLineToFramedDisplay(display, ligneManoeuvre, 90);
}
// on envoie la liste aux joueurs qui gèrent le voleur
let playerIds = getPlayerIds(cible);
playerIds.forEach(function(playerid) {
addFramedHeader(display, playerid, true);
sendFramedDisplay(display);
});
if (playerIds.length === 0) {
addFramedHeader(display, undefined, 'gm');
sendFramedDisplay(display);
}
}
});
}
//!cof-expert-combat-bousculer
function expertDuCombatBousculer(msg) {
let cmd = msg.content.split(' ');
if (!stateCOF.combat) {
error("On ne peut utiliser !cof-expert-combat-bousculer qu'en combat", msg.content);
return;
}
if (cmd.length < 3) {
error("Il manque des arguments à !cof-expert-combat-bousculer", msg.content);
return;
}
let expert = persoOfId(cmd[1], cmd[1]);
if (expert === undefined) {
error("Le premier argument de !cof-expert-combat-bousculer n'est pas un token valide", cmd);
return;
}
let cible = persoOfId(cmd[2], cmd[2]);
if (cible === undefined) {
error("Le deuxième argument de !cof-expert-combat-bousculer n'est pas un token valide", cmd);
return;
}
if (!predicateAsBool(expert, 'expertDuCombat')) {
error(nomPerso(expert) + " n'est pas un expert du combat !", cmd);
return;
}
let pageId = expert.token.get('pageid');
if (distanceCombat(expert.token, cible.token, pageId)) {
sendPerso(expert, "est trop loin de " + nomPerso(cible) + " pour le bousculer.");
return;
}
const evt = {
type: 'Bousculer'
};
addEvent(evt);
if (!persoUtiliseDeExpertDuCombat(expert, evt)) return;
let deExpertise = rollDePlus(6);
let playerId = getPlayerIdFromMsg(msg);
let explications = [];
testOppose("bouculer", expert, "FOR", {
bonus: deExpertise.val
}, cible, "FOR", {}, explications, evt, function(resultat, crit, rt1, rt2) {
const display = startFramedDisplay(playerId, "Bousculer", expert, {
perso2: cible
});
explications.push("Dé d'expertise : " + deExpertise.roll);
if (resultat === 1) {
addLineToFramedDisplay(display, nomPerso(cible) + " est repoussé de " +
Math.ceil(deExpertise.val / 2) + " mètre" + (deExpertise.val > 1 ? "s" : "") + "
S'il est acculé : " +
boutonSimple("!cof-dmg " + deExpertise.val + " --target " + cmd[2], "Appliquer " + deExpertise.val + " DM"));
setState(cible, "renverse", "true", evt);
} else {
addLineToFramedDisplay(display, nomPerso(cible) + " n'est pas renversé");
}
explications.forEach(function(expl) {
addLineToFramedDisplay(display, expl, 80);
});
sendFramedDisplay(display);
});
}
function sendCommands(from, commands) {
if (commands.length === 0) return;
let c = commands.shift();
if (c.startsWith('!')) {
_.delay(function() {
sendChat(from, c);
sendCommands(from, commands);
}, 10);
} else error("multi-commande invalide", c);
}
//!cof-multi-command !cmd1 ... --cof-multi-command !cmd2 .. --cof-multi-command !cmd3...
function multiCommand(msg) {
let posFirstCommand = msg.content.indexOf('!', 2);
let commands = msg.content.substr(posFirstCommand).split(' --cof-multi-command ');
sendCommands(msg.who, commands);
/* commands.forEach(function(c) {
if (c.startsWith('!')) sendChat(msg.who, c);
else error("multi-commande invalide", c);
});*/
}
const predateurs = {
loup: {
nom: 'Loup',
avatar: "https://s3.amazonaws.com/files.d20.io/images/59094468/bX_aTjrVAbIRHjpRn-HwdQ/max.jpg?1532611383",
token: "https://s3.amazonaws.com/files.d20.io/images/59489165/3R9Ob68sTiqvNeEhwzwWcg/thumb.png?1533047142",
attributesFiche: {
type_personnage: 'PNJ',
niveau: 1,
force: 12,
pnj_for: 1,
dexterite: 12,
pnj_dex: 1,
constitution: 12,
pnj_con: 1,
con_sup: '@{jetsup}',
pnj_con_sup: 'on',
intelligence: 2,
pnj_int: -4,
sagesse: 14,
pnj_sag: 2,
sag_sup: '@{jetsup}',
pnj_sag_sup: 'on',
charisme: 6,
pnj_cha: -2,
DEFDIV: 3,
pnj_def: 14,
pnj_init: 12,
race: 'loup',
taille: 'moyen'
},
pv: 9,
attaques: [{
nom: 'Morsure',
atk: 2,
dmnbde: 1,
dmde: 6,
dm: 1,
}],
attributes: [],
abilities: []
},
loupAlpha: {
nom: 'Loup alpha',
avatar: "https://s3.amazonaws.com/files.d20.io/images/59094818/J0yWdxryZFKakJtNGJNNvw/max.jpg?1532612061",
token: "https://s3.amazonaws.com/files.d20.io/images/60183959/QAMH6WtyoK78aa4zX_mR_Q/thumb.png?1533898482",
attributesFiche: {
type_personnage: 'PNJ',
niveau: 2,
force: 16,
pnj_for: 3,
dexterite: 12,
pnj_dex: 1,
constitution: 16,
pnj_con: 3,
con_sup: '@{jetsup}',
pnj_con_sup: 'on',
intelligence: 2,
pnj_int: -4,
sagesse: 14,
pnj_sag: 2,
sag_sup: '@{jetsup}',
pnj_sag_sup: 'on',
charisme: 6,
pnj_cha: -2,
DEFDIV: 4,
pnj_def: 15,
INIT_DIV: 5,
pnj_init: 17,
race: 'loup',
taille: 'moyen'
},
pv: 15,
attaques: [{
nom: 'Morsure',
atk: 4,
dmnbde: 1,
dmde: 6,
dm: 3,
}],
attributes: [{
name: 'predicats_script',
current: 'embuscade',
}],
abilities: [{
name: 'Embuscade',
action: '!cof-surprise [[15 + @{selected|DEX}]] --target @{target|token_id}'
}],
},
worg: {
nom: 'Grand loup',
avatar: "https://s3.amazonaws.com/files.d20.io/images/25294798/4dJ_60uP2mw6UJA2elkoXA/max.jpg?1479223790",
token: "https://s3.amazonaws.com/files.d20.io/images/60184237/smG5o2-siD2pChhPblO_sQ/thumb.png?1533899118",
attributesFiche: {
type_personnage: 'PNJ',
niveau: 3,
force: 16,
pnj_for: 3,
dexterite: 12,
pnj_dex: 1,
constitution: 16,
pnj_con: 3,
con_sup: '@{jetsup}',
pnj_con_sup: 'on',
intelligence: 4,
pnj_int: -3,
sagesse: 14,
pnj_sag: 2,
sag_sup: '@{jetsup}',
pnj_sag_sup: 'on',
charisme: 6,
pnj_cha: -2,
DEFDIV: 6,
pnj_def: 17,
INIT_DIV: 5,
pnj_init: 17,
race: 'loup',
taille: 'moyen'
},
pv: 35,
attaques: [{
nom: 'Morsure',
atk: 6,
dmnbde: 1,
dmde: 6,
dm: 5,
}],
attributes: [{
name: 'predicats_script',
current: 'embuscade'
}],
abilities: [{
name: 'Embuscade',
action: '!cof-surprise [[15 + @{selected|DEX}]]'
}],
},
lion: {
nom: 'Lion',
avatar: "https://s3.amazonaws.com/files.d20.io/images/59486104/SngxPIGXDJKdCqsbrXxRYQ/max.jpg?1533041390",
token: "https://s3.amazonaws.com/files.d20.io/images/60184437/df1MT2T6lrfo7st02Htxeg/thumb.png?1533899407",
attributesFiche: {
type_personnage: 'PNJ',
niveau: 4,
force: 20,
pnj_for: 5,
dexterite: 18,
pnj_dex: 4,
dex_sup: '@{jetsup}',
pnj_dex_sup: 'on',
constitution: 20,
pnj_con: 5,
intelligence: 4,
pnj_int: -3,
sagesse: 14,
pnj_sag: 2,
sag_sup: '@{jetsup}',
pnj_sag_sup: 'on',
charisme: 6,
pnj_cha: -3,
DEFDIV: 4,
pnj_def: 18,
INIT_DIV: 5,
pnj_init: 23,
race: 'lion',
taille: 'grand'
},
pv: 30,
attaques: [{
nom: 'Morsure',
atk: 7,
dmnbde: 2,
dmde: 6,
dm: 5,
}],
attributes: [{
name: 'predicats_script',
current: 'embuscade devorer',
}],
abilities: [{
name: 'Embuscade',
action: '!cof-surprise [[15 + @{selected|DEX}]]'
}],
},
grandLion: {
nom: 'Grand lion',
avatar: "https://s3.amazonaws.com/files.d20.io/images/59486144/8wHs_5WfEIeL_7dKbALHHA/max.jpg?1533041459",
token: "https://s3.amazonaws.com/files.d20.io/images/60186141/mUZzndi9_sYIzdVVNNka_w/thumb.png?1533903070",
attributesFiche: {
type_personnage: 'PNJ',
niveau: 5,
force: 22,
pnj_for: 6,
dexterite: 18,
pnj_dex: 3,
dex_sup: '@{jetsup}',
pnj_dex_sup: 'on',
constitution: 20,
pnj_con: 5,
intelligence: 2,
pnj_int: -4,
sagesse: 14,
pnj_sag: 2,
sag_sup: '@{jetsup}',
pnj_sag_sup: 'on',
charisme: 14,
pnj_cha: 2,
DEFDIV: 6,
pnj_def: 20,
pnj_init: 18,
race: 'lion',
taille: 'grand'
},
pv: 50,
attaques: [{
nom: 'Morsure',
atk: 9,
dmnbde: 2,
dmde: 6,
dm: 7,
}],
attributes: [{
name: 'predicats_script',
current: 'embuscade devorer',
}],
abilities: [{
name: 'Embuscade',
action: '!cof-surprise [[15 + @{selected|DEX}]]'
}],
},
oursPolaire: {
nom: 'Ours polaire',
avatar: "https://s3.amazonaws.com/files.d20.io/images/59486216/UssilagWK_2dfVGuPABBpA/max.png?1533041591",
token: "https://s3.amazonaws.com/files.d20.io/images/60186288/B1uAii9G01GcPfQFNozIbw/thumb.png?1533903333",
attributesFiche: {
type_personnage: 'PNJ',
niveau: 6,
force: 26,
pnj_for: 8,
for_sup: '@{jetsup}',
pnj_for_sup: 'on',
dexterite: 11,
pnj_dex: 0,
constitution: 26,
pnj_con: 8,
con_sup: '@{jetsup}',
pnj_con_sup: 'on',
intelligence: 2,
pnj_int: -4,
sagesse: 14,
pnj_sag: 2,
charisme: 6,
pnj_cha: -2,
DEFDIV: 10,
pnj_def: 20,
pnj_init: 11,
race: 'ours',
taille: 'grand'
},
pv: 70,
attaques: [{
nom: 'Morsure',
atk: 12,
dmnbde: 2,
dmde: 8,
dm: 7,
}],
attributes: [{
name: 'predicats_script',
curreent: 'peutEnrager',
}],
actions: [{
titre: 'Charge',
code: '!cof-attack @{selected|token_id} @{target|token_id} 1 --m2d20 --pietine}'
}, ]
},
tigreDentsDeSabre: {
nom: 'Tigre à dents de sabre',
avatar: "https://s3.amazonaws.com/files.d20.io/images/59486272/f5lUcN3Y9H0thmJPrqa6FQ/max.png?1533041702",
token: "https://s3.amazonaws.com/files.d20.io/images/60186469/ShcrgpvgXKiQsLVOyg4SZQ/thumb.png?1533903741",
attributesFiche: {
type_personnage: 'PNJ',
niveau: 7,
force: 26,
pnj_for: 8,
for_sup: '@{jetsup}',
pnj_for_sup: 'on',
dexterite: 18,
pnj_dex: 4,
dex_sup: '@{jetsup}',
pnj_dex_sup: 'on',
constitution: 26,
pnj_con: 8,
intelligence: 2,
pnj_int: -4,
sagesse: 12,
pnj_sag: 1,
sag_sup: '@{jetsup}',
pnj_sag_sup: 'on',
charisme: 2,
pnj_cha: -4,
DEFDIV: 8,
pnj_def: 22,
pnj_init: 18,
race: 'tigre',
taille: 'grand'
},
pv: 90,
attaques: [{
nom: 'Morsure',
atk: 14,
dmnbde: 2,
dmde: 6,
dm: 12,
}],
attributes: [{
name: 'predicats_script',
current: 'embuscade devorer',
}],
abilities: [{
name: 'Embuscade',
action: '!cof-surprise [[15 + @{selected|DEX}]]',
}],
},
oursPrehistorique: {
nom: 'Ours préhistorique',
avatar: "https://s3.amazonaws.com/files.d20.io/images/59486323/V6RVSlBbeRJi_aIaIuGGBw/max.png?1533041814",
token: "https://s3.amazonaws.com/files.d20.io/images/60186633/lNHXvCOsvfPMZDQnqJKQVw/thumb.png?1533904189",
attributesFiche: {
type_personnage: 'PNJ',
niveau: 8,
force: 32,
pnj_for: 11,
dexterite: 10,
pnj_dex: 0,
constitution: 32,
pnj_con: 11,
con_sup: '@{jetsup}',
pnj_con_sup: 'on',
intelligence: 2,
pnj_int: -4,
sagesse: 14,
pnj_sag: 2,
charisme: 6,
pnj_cha: -2,
DEFDIV: 12,
pnj_def: 22,
pnj_init: 10,
RDS: 2,
race: 'ours',
taille: 'énorme'
},
pv: 110,
attaques: [{
nom: 'Griffes',
atk: 17,
dmnbde: 3,
dmde: 6,
dm: 13,
}],
attributes: [{
name: 'fauchage',
current: 'true'
}],
actions: [{
titre: 'Charge',
code: '!cof-attack @{selected|token_id} @{target|token_id} 1 --m2d20 --pietine}'
}, ]
}
};
function conjurationPredateur(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Pas de commande", msg.content);
return;
}
let renforce = 0;
if (cmd.length > 1) {
renforce = parseInt(cmd[1]);
if (isNaN(renforce)) {
error("Il faut un nombre comme premier argument de !cof-conjuration-de-predateur");
renforce = 0;
}
}
getSelected(msg, function(selected, playerId) {
if (selected === undefined || selected.length === 0) {
error("pas de lanceur pour la conjuration de prédateurs", msg);
return;
}
let evt = {
type: 'conjuration de prédateurs'
};
let combat = initiative(selected, evt);
let abort;
iterSelected(selected, function(invocateur) {
if (abort) return;
if (options.tempeteDeMana) {
if (selected.length > 1) {
sendPlayerAndGM(msg, playerId, "Il faut sélectionner un seul token pour les options de tempête de mana");
abort = true;
return;
}
if (options.tempeteDeMana.cout === 0) {
//On demande de préciser les options
let optMana = {
mana: options.mana,
portee: false,
duree: true,
dm: true,
intense: 0,
rang: 1
};
setTempeteDeMana(playerId, invocateur, msg.content, optMana);
abort = true;
return;
} else {
if (options.tempeteDeMana.cout > 1) {
sendPlayerAndGM(msg, playerId, "Attention, le coût de la tempête de mana (" + options.tempeteDeMana.cout + ") est supérieur au rang du sort");
}
}
}
if (limiteRessources(invocateur, options, 'invocationPredateur', 'lancer une invocation de prédateur', evt)) return;
let pageId = invocateur.token.get('pageid');
let niveau = ficheAttributeAsInt(invocateur, 'niveau', 1);
if (!renforce) {
renforce = predicateAsInt(invocateur, 'voieDeLaConjuration', 0);
if (renforce == 1) renforce = 0;
}
niveau += renforce;
let predateur;
if (niveau < 5) predateur = predateurs.loup;
else if (niveau < 9) predateur = predateurs.loupAlpha;
else if (niveau < 12) predateur = predateurs.worg;
else if (niveau < 15) predateur = predateurs.lion;
else if (niveau < 18) predateur = predateurs.grandLion;
else if (niveau < 21) predateur = predateurs.oursPolaire;
else if (niveau < 23) predateur = predateurs.tigreDentsDeSabre;
else predateur = predateurs.oursPrehistorique;
if (options.tempeteDeManaIntense) {
// on copie les attaques pour leur ajouter --si predateurConjure
let attaques = predateur.attaques;
predateur = {...predateur
};
predateur.attaques = [];
attaques.forEach(function(attaque) {
attaque = {...attaque
};
attaque.options = '--si etat predateurConjure';
predateur.attaques.push(attaque);
});
}
let nomPredateur =
predateur.nom + ' de ' + nomPerso(invocateur);
let token = createObj('graphic', {
name: nomPredateur,
subtype: 'token',
pageid: pageId,
imgsrc: predateur.token,
left: invocateur.token.get('left'),
top: invocateur.token.get('top'),
width: 70,
height: 70,
layer: 'objects',
showname: 'true',
showplayers_bar1: 'true',
light_hassight: 'true',
light_angle: 0, //Pour que le joueur ne voit rien par ses yeux
has_bright_light_vision: true,
has_limit_field_of_vision: true,
});
toFront(token);
let charPredateur =
createCharacter(nomPredateur, playerId, predateur.avatar, token, predateur, evt);
//Tous les prédateurs sont des quadrupèdes
let persoPredateur = {
token: token,
charId: charPredateur.id
};
setPredicate(persoPredateur, 'quadrupede', evt);
//Attribut de predateur conjuré pour la disparition automatique
let attr = createObj('attribute', {
name: 'predateurConjure',
_characterid: charPredateur.id,
current: 5 + modCarac(invocateur, 'charisme'),
max: combat.init
});
if (options.mana !== undefined) {
addEffetTemporaireLie(invocateur, attr, evt);
}
if (options.tempeteDeManaIntense) {
createObj('attribute', {
name: 'predateurConjureTempeteDeManaIntense',
_characterid: charPredateur.id,
current: options.tempeteDeManaIntense,
});
}
evt.characters = [charPredateur];
evt.tokens = [token];
initiative([{
_id: token.id
}], evt);
// Ajout du Prédateur aux alliés de l'invocateur
let alliesInvocateur = alliesParPerso[invocateur.charId] || new Set();
alliesInvocateur.add(charPredateur.id);
alliesParPerso[invocateur.charId] = alliesInvocateur;
}); //end iterSelected
addEvent(evt);
}); //end getSelected
}
//!cof-sphere-de-feu
function sphereDeFeu(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Pas de commande", msg.content);
return;
}
getSelected(msg, function(selected, playerId) {
if (selected === undefined || selected.length === 0) {
error("pas de lanceur pour la sphere de feu", msg);
return;
}
let evt = {
type: "invocation d'une sphère de feu",
};
let combat = initiative(selected, evt);
iterSelected(selected, function(invocateur) {
if (limiteRessources(invocateur, options, 'sphereDeFeu', 'lancer un sort de sphère de feu', evt)) return;
let character = getObj('character', invocateur.charId);
if (character === undefined) {
error("Impossible de trouver le personnage de " + nomPerso(invocateur), invocateur);
return;
}
let pageId = invocateur.token.get('pageid');
let niveau = ficheAttributeAsInt(invocateur, 'niveau', 1);
let sphere = {
nom: 'Sphère de feu',
avatar: "https://s3.amazonaws.com/files.d20.io/images/260057530/nL8O6US3f1BpeTJkodWNCg/max.png?16394116785",
token: "https://s3.amazonaws.com/files.d20.io/images/260057530/nL8O6US3f1BpeTJkodWNCg/thumb.png?16394116785",
attributesFiche: {
type_personnage: 'PNJ',
niveau: 1,
},
pv: 1,
attaques: [{
nom: 'Brûlure',
atk: 0,
dmnbde: 3,
dmde: 6,
dm: 0,
typedegats: 'feu',
modificateurs: 'auto',
options: '--saveDM DEX ' + (10 + modCarac(invocateur, 'intelligence')),
}],
attributes: [{
name: 'predicats_script',
current: 'nonVivant immunite_feu sansEsprit initiativeDeriveeDe::' + character.get('name') + '\n',
}, {
name: 'predateurConjure', //Pas exactement ça, mais ça fait ce qu'il faut
current: niveau,
max: combat.init,
lie: options.mana !== undefined
}],
};
let nomSphere = sphere.nom + ' de ' + nomPerso(invocateur);
let token = createObj('graphic', {
name: nomSphere,
subtype: 'token',
pageid: pageId,
imgsrc: sphere.token,
left: invocateur.token.get('left'),
top: invocateur.token.get('top'),
width: 35,
height: 35,
layer: 'objects',
showname: 'true',
showplayers_bar1: 'true',
light_hassight: 'true',
light_angle: 0, //Pour que le joueur ne voit rien par ses yeux
has_bright_light_vision: true,
has_limit_field_of_vision: true,
});
if (token === undefined) {
error("Impossible de créer le token de la sphère de feu ", sphere);
return;
}
toFront(token);
let charSphere =
createCharacter(nomSphere, playerId, sphere.avatar, token, sphere, evt, invocateur);
evt.characters = [charSphere];
evt.tokens = [token];
initiative([{
_id: token.id
}], evt);
// Ajout du Prédateur aux alliés de l'invocateur
let alliesInvocateur = alliesParPerso[invocateur.charId] || new Set();
alliesInvocateur.add(charSphere.id);
alliesParPerso[invocateur.charId] = alliesInvocateur;
}); //end iterSelected
addEvent(evt);
}); //end getSelected
}
//!cof-conjuration-armee [dé de DM] --limiteParJour...
function conjurationArmee(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Problème de parse options", msg.content);
return;
}
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
sendPlayer(msg, "Il faut sélectionner le lanceur de la conjuration d'arméé", playerId);
return;
}
let abort;
iterSelected(selected, function(invocateur) {
if (abort) return;
if (options.tempeteDeMana) {
if (selected.length > 1) {
sendPlayerAndGM(msg, playerId, "Il faut sélectionner un seul token pour les options de tempête de mana");
abort = true;
return;
}
if (options.tempeteDeMana.cout === 0) {
//On demande de préciser les options
let optMana = {
mana: options.mana,
portee: false,
duree: false,
dm: true,
intense: 0,
rang: 3
};
setTempeteDeMana(playerId, invocateur, msg.content, optMana);
abort = true;
return;
} else {
if (options.tempeteDeMana.cout > 1) {
sendPlayerAndGM(msg, playerId, "Attention, le coût de la tempête de mana (" + options.tempeteDeMana.cout + ") est supérieur au rang du sort");
}
}
}
let pageId = invocateur.token.get('pageid');
let niveau = ficheAttributeAsInt(invocateur, 'niveau', 1);
const evt = {
type: "Conjuration d'armée"
};
if (limiteRessources(invocateur, options, 'conjurationArmee', "conjurer une armée", evt)) {
addEvent(evt);
return;
}
let dm;
if (cmd.length > 1) {
dm = parseDice(cmd[1]);
}
if (dm === undefined) {
dm = {
nbDe: 1,
dice: 6,
bonus: 0
};
let rang = predicateAsInt(invocateur, 'voieDeLaConjuration', 3);
if (rang == 4) {
dm.dice = 10;
} else if (rang > 4) {
dm.nbDe = 2;
}
}
let nomArmee = "Armée de " + nomPerso(invocateur);
let token = createObj('graphic', {
name: nomArmee,
subtype: 'token',
pageid: pageId,
imgsrc: 'https://s3.amazonaws.com/files.d20.io/images/73283129/-jrKAyQQ1P7zpD09xeTbXw/thumb.png?1549546953',
left: invocateur.token.get('left'),
top: invocateur.token.get('top'),
width: 70,
height: 70,
layer: 'objects',
showname: 'true',
showplayers_bar1: 'true',
light_hassight: 'true',
light_angle: 0, //Pour que le joueur ne voit rien par ses yeux
has_bright_light_vision: true,
has_limit_field_of_vision: true,
aura1_radius: 10,
aura1_color: "#d56eef",
aura1_square: true
});
toFront(token);
let avatar = "https://s3.amazonaws.com/files.d20.io/images/73283254/r6sbxbP1QKKtqXyYq-MlLA/max.png?1549547198";
let attaque = {
nom: 'Attaque',
dmnbde: dm.nbDe,
dmde: dm.dice,
modificateurs: 'auto',
options: "--allonge 20 --si etat armeeConjuree",
};
if (dm.bonus) attaque.dm = dm.bonus;
let attributes = [{
name: 'armeeConjuree',
current: invocateur.charId,
lie: options.mana !== undefined
}];
if (options.tempeteDeManaIntense) {
attributes.push({
name: 'armeeConjureeTempeteDeManaIntense',
current: options.tempeteDeManaIntense
});
}
let charArmee =
createCharacter(nomArmee, playerId, avatar, token, {
pv: niveau * 10,
attaques: [attaque],
attributes: attributes
}, evt, invocateur);
evt.characters = [charArmee];
evt.tokens = [token];
if (stateCOF.combat) {
initiative([{
_id: token.id
}], evt);
}
});
});
}
//!cof-tenebres token-lanceur token-cible
// possibilité de --brumes pour un effet de brumes
function tenebres(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 2) {
error("!cof-tenebres mal formé, il faut un token comme premier argument", msg.content);
return;
}
let necromant = persoOfId(cmd[1], cmd[1], options.pageId);
if (necromant === undefined) {
error("Le premier argument de !cof-tenebres n'est pas un token valide", cmd);
return;
}
let target = persoOfId(cmd[2], cmd[2], options.pageId);
if (target === undefined) {
error("Le second argument de !cof-tenebres n'est pas un token valide", cmd);
return;
}
options.lanceur = necromant;
const playerId = getPlayerIdFromMsg(msg);
if (options.tempeteDeMana) {
if (options.tempeteDeMana.cout === 0) {
//On demande de préciser les options
let optMana = {
mana: options.mana,
portee: true,
duree: true,
intense: 0,
rang: 1
};
setTempeteDeMana(playerId, options.lanceur, msg.content, optMana);
return;
} else {
if (options.tempeteDeMana.cout > 1) {
sendPlayerAndGM(msg, playerId, "Attention, le coût de la tempête de mana (" + options.tempeteDeMana.cout + ") est supérieur au rang du sort");
}
}
}
let portee = options.portee || 20;
if (options.puissantPortee || options.tempeteDeManaPortee) {
portee = portee * 2;
}
let rayon = options.rayon;
if (rayon === undefined) {
rayon = options.brumes ? 20 : 5;
}
if (options.puissant || options.tempeteDeManaIntense) rayon = Math.floor(Math.sqrt(2) * rayon);
if (distanceCombat(necromant.token, target.token, options.pageId, {
strict2: true
}) > portee) {
sendPlayer(msg, "Le point visé est trop loin (portée " + portee + ")", playerId);
return;
}
let duree = 5;
if (options.brumes) duree += modCarac(necromant, 'sagesse');
else duree += modCarac(necromant, 'intelligence');
if (options.puissantDuree || options.tempeteDeManaDuree) {
duree = duree * 2;
}
const evt = {
type: 'tenebres'
};
addEvent(evt);
if (options.brumes) {
if (limiteRessources(necromant, options, 'brumes', 'lancer un sort de brumes', evt)) return;
} else {
if (limiteRessources(necromant, options, 'tenebres', 'lancer un sort de ténèbres', evt)) return;
}
if (!stateCOF.combat) {
initPerso(necromant, evt);
}
let rayonUnite = scaleDistance(necromant, rayon);
let tokSpec = {
showname: true,
subtype: 'token',
pageid: options.pageId,
left: target.token.get("left"),
top: target.token.get("top"),
};
if (options.brumes) {
tokSpec.name = "Brumes de " + nomPerso(necromant);
tokSpec.imgsrc = 'https://s3.amazonaws.com/files.d20.io/images/274452104/DUq74hFhXq9yPK3Q1KinlA/thumb.png?1646665878';
tokSpec.width = rayonUnite * PIX_PER_UNIT;
tokSpec.height = rayonUnite * PIX_PER_UNIT;
tokSpec.layer = 'map';
} else {
tokSpec.name = "Ténèbres de " + nomPerso(necromant);
tokSpec.imgsrc = 'https://s3.amazonaws.com/files.d20.io/images/192072874/eJXFx20fD931DuBDvzAnQQ/thumb.png?1610469273';
tokSpec.width = 70;
tokSpec.height = 70;
tokSpec.layer = 'objects';
tokSpec.aura1_radius = 0;
tokSpec.aura1_color = "#c1c114";
tokSpec.aura1_square = true;
tokSpec.aura2_radius = rayonUnite;
tokSpec.aura2_color = "#000000";
tokSpec.showplayers_aura2 = true;
}
let token = createObj('graphic', tokSpec);
if (token) {
evt.tokens = [token];
toFront(token);
}
if (stateCOF.options.affichage.val.duree_effets.val) {
if (options.brumes)
sendPerso(necromant, "lance un sort de brumes pour " + duree + " tours");
else
sendPerso(necromant, "lance un sort de ténèbres pour " + duree + " tours");
}
// Calcul des cibles à aveugler
let cibles = [];
let allToksDisque =
findObjs({
_type: 'graphic',
_pageid: options.pageId,
_subtype: 'token',
layer: 'objects'
});
let saufAllies = msg.content.includes(" --saufAllies");
let allies;
if (saufAllies) {
allies = alliesParPerso[necromant.charId];
allies = (new Set(allies)).add(necromant.charId);
}
allToksDisque.forEach(function(obj) {
if (obj.get('bar1_max') == 0) return; // jshint ignore:line
let objCharId = obj.get('represents');
if (objCharId === '') return;
if (saufAllies && allies.has(objCharId)) return;
let cible = {
token: obj,
charId: objCharId
};
if (getState(cible, 'mort')) return;
let distanceCentre =
distanceCombat(target.token, obj, options.pageId, {
strict1: true
});
if (distanceCentre > rayon) return;
cibles.push(cible);
});
let effetAveugle = {
effet: 'aveugleTemp',
duree: duree
};
if (options.brumes) effetAveugle.effet = 'penombreTemp';
cibles.forEach(function(perso) {
setEffetTemporaire(perso, effetAveugle, duree, evt, {});
});
let effetTenebres = {
effet: 'tenebres',
duree: duree,
valeur: token.id,
pasDeMessageDActivation: true,
attaquant: necromant
};
if (options.brumes) effetTenebres.effet = 'brumes';
setEffetTemporaire(necromant, effetTenebres, duree, evt, options);
if (target.token.get('bar1_max') == 0) { // jshint ignore:line
//C'est juste un token utilisé pour définir le disque
target.token.remove(); //On l'enlève, normalement plus besoin
}
if (options.messages) {
options.messages.forEach(function(m) {
sendPerso(necromant, m, options.secret);
});
}
}
const demonInvoque = {
nom: 'Démon',
avatar: "https://s3.amazonaws.com/files.d20.io/images/183633585/DWpHYp4SLPCDCMHdmTyKOw/thumb.png?1607339938",
token: "https://s3.amazonaws.com/files.d20.io/images/183633585/DWpHYp4SLPCDCMHdmTyKOw/thumb.png?1607339938",
attributesFiche: {
type_personnage: 'PNJ',
niveau: 5,
force: 20,
pnj_for: 5,
for_sup: '@{jetsup}',
pnj_for_sup: 'on',
dexterite: 14,
pnj_dex: 2,
constitution: 18,
pnj_con: 4,
con_sup: '@{jetsup}',
pnj_con_sup: 'on',
intelligence: 14,
pnj_int: 2,
sagesse: 14,
pnj_sag: 2,
charisme: 10,
pnj_cha: 0,
DEFDIV: 3,
pnj_def: 17,
pnj_init: 16,
race: 'démon',
taille: 'moyen'
},
attaques: [{
nom: 'Griffes',
dmnbde: 1,
dmde: 8,
dm: 5,
modificateurs: 'magique'
}],
attributes: [{
name: 'predicats_script',
current: 'démon'
}],
abilities: []
};
function invocationDemon(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 2) {
error("!cof-invoquer-demon mal formé, il faut un token comme premier argument", msg.content);
return;
}
let necromant = persoOfId(cmd[1], cmd[1], options.pageId);
if (necromant === undefined) {
error("Le premier argument de !cof-invoquer-demon n'est pas un token valie", cmd);
return;
}
options.lanceur = necromant;
getSelected(msg, function(selected, playerId) {
const evt = {
type: 'invocationDemon',
action: {
rolls: {}
}
};
addEvent(evt);
if (limiteRessources(necromant, options, 'invoquerDemon', 'lance une invocation de démon', evt)) return;
let d6 = rollDePlus(6);
evt.action.rolls.invocationDemonDmg = d6;
let r = {
total: d6.val,
type: 'normal',
display: d6.roll
};
let explications = [];
necromant.ignoreTouteRD = true;
dealDamage(necromant, r, [], evt, false, {}, explications,
function(dmgDisplay, dmg) {
if (!stateCOF.combat) {
initPerso(necromant, evt);
}
let tokenDemon = "Démon de " + nomPerso(necromant);
let token = createObj('graphic', {
name: tokenDemon,
showname: 'true',
subtype: 'token',
pageid: options.pageId,
imgsrc: demonInvoque.token,
left: necromant.token.get('left'),
top: necromant.token.get('top'),
width: 70,
height: 70,
layer: 'objects',
showplayers_bar1: 'true',
light_hassight: 'true',
light_angle: 0,
has_bright_light_vision: true,
has_limit_field_of_vision: true,
});
toFront(token);
let niveau = ficheAttributeAsInt(necromant, "niveau", 1);
let demon = {...demonInvoque
};
demon.pv = niveau * 5;
demon.attaques[0].atk = niveau;
let charDemon = createCharacter(tokenDemon, playerId, demonInvoque.avatar, token, demon, evt);
evt.characters = [charDemon];
evt.tokens = [token];
let duree = 5 + modCarac(necromant, 'intelligence');
//Attribut de démon invoqué pour la disparition automatique
createObj('attribute', {
name: 'demonInvoque',
_characterid: charDemon.id,
current: duree,
max: getInit()
});
createObj('attribute', {
name: 'resistanceA_nonMagique',
_characterid: charDemon.id,
current: 'true',
});
initiative([{
_id: token.id
}], evt);
// Ajout du Démon aux alliés du Nécromant
let alliesNecromant = alliesParPerso[necromant.charId] || new Set();
alliesNecromant.add(charDemon.id);
alliesParPerso[necromant.charId] = alliesNecromant;
let msg = "invoque un démon";
if (stateCOF.options.affichage.val.duree_effets.val) msg += " pour " + duree + " tours";
msg += " mais cela lui coûte " + dmgDisplay + " PV";
sendPerso(necromant, msg);
});
}, options);
}
const zombieAnime = {
nom: 'Zombie',
avatar: "https://s3.amazonaws.com/files.d20.io/images/147503510/RKFKQefVjSiyNPJtvBuGOg/thumb.png?1593643543",
token: "https://s3.amazonaws.com/files.d20.io/images/147503510/RKFKQefVjSiyNPJtvBuGOg/thumb.png?1593643543",
attributesFiche: {
type_personnage: 'PNJ',
niveau: 1,
force: 12,
pnj_for: 1,
dexterite: 8,
pnj_dex: -1,
constitution: 12,
pnj_con: 1,
intelligence: 2,
pnj_int: -4,
sagesse: 6,
pnj_sag: -2,
charisme: 2,
pnj_cha: -4,
pnj_def: 10,
pnj_init: 8,
race: 'mort-vivant',
taille: 'moyen'
},
attaques: [{
nom: 'Coup',
atk: 3,
dmnbde: 1,
dmde: 6,
dm: 1
}],
pv: 12,
attributes: [],
abilities: []
};
function animerMort(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 2) {
error("!cof-animer-mort mal formé, il faut un token comme premier argument", msg.content);
return;
}
const necromant = persoOfId(cmd[1], cmd[1], options.pageId);
if (necromant === undefined) {
error("Le premier argument de !cof-animer-mort n'est pas un token valie", cmd);
return;
}
let zombiesControles = attributeAsInt(necromant, 'zombiesControles', 0);
let rangVoie = predicateAsInt(necromant, 'voieOutreTombe', 1);
if (zombiesControles >= rangVoie) {
sendPerso(necromant, "ne peut plus animer de Zombie car il en contrôle déjà assez.");
return;
}
options.lanceur = necromant;
const evt = {
type: 'animerMort',
action: {
rolls: {}
},
characters: [],
tokens: []
};
addEvent(evt);
if (limiteRessources(necromant, options, 'animerMort', "lancer un sort d'Animation des morts", evt)) return;
let combat = stateCOF.combat;
if (!combat) {
combat = initPerso(necromant, evt);
}
let nomToken = "Zombie de " + nomPerso(necromant);
let token = createObj('graphic', {
name: nomToken,
showname: 'true',
subtype: 'token',
pageid: options.pageId,
imgsrc: zombieAnime.token,
left: necromant.token.get('left'),
top: necromant.token.get('top'),
width: 70,
height: 70,
layer: 'objects',
showplayers_bar1: 'true',
light_hassight: 'true',
light_angle: 0,
has_bright_light_vision: true,
has_limit_field_of_vision: true,
});
toFront(token);
let zombie = {
...zombieAnime
};
let playerId = getPlayerIdFromMsg(msg);
let charZombie = createCharacter(nomToken, playerId, zombieAnime.avatar, token, zombie, evt);
evt.characters.push(charZombie);
evt.tokens.push(token);
// Dégradation du Zombie
createObj('attribute', {
name: 'degradationZombie',
_characterid: charZombie.id,
current: 71,
max: combat.init,
});
// Gestion de la limitation des zombies
createObj('attribute', {
name: 'necromant',
_characterid: charZombie.id,
current: necromant.token.id,
});
initiative([{
_id: token.id
}], evt);
// Ajout du Zombie aux alliés du Nécromant
let alliesNecromant = alliesParPerso[necromant.charId] || new Set();
alliesNecromant.add(charZombie.id);
alliesParPerso[necromant.charId] = alliesNecromant;
sendPerso(necromant, "anime un Zombie", options.secret);
if (options.messages) {
options.messages.forEach(function(m) {
sendPerso(necromant, m, options.secret);
});
}
setTokenAttr(necromant, "zombiesControles", zombiesControles + 1, evt);
}
const PAUSE = String.fromCharCode(0x23F8);
const PLAY = String.fromCharCode(0x23F5);
const MONTER = String.fromCharCode(0x2197);
const DESCENDRE = String.fromCharCode(0x2198);
//Crée les macros utiles au jeu
const gameMacros = [{
name: 'Actions',
action: "!cof-liste-actions",
visibleto: 'all',
istokenaction: true
}, {
name: 'Attaque',
action: "!cof-attack @{selected|token_id} @{target|token_id}",
visibleto: 'all',
istokenaction: false
}, {
name: 'Consommables',
action: "!cof-consommables",
visibleto: 'all',
istokenaction: true
}, {
name: 'Bouger',
action: "!cof-bouger",
visibleto: '',
istokenaction: false,
inBar: true
}, {
name: MONTER,
oldName: 'Monter',
action: "!cof-escalier haut",
visibleto: 'all',
istokenaction: true,
inBar: false
}, {
name: DESCENDRE,
oldName: 'Descendre',
action: "!cof-escalier bas",
visibleto: 'all',
istokenaction: true,
inBar: false
}, {
name: 'Fin-combat',
action: "!cof-fin-combat",
visibleto: '',
istokenaction: false,
inBar: true
}, {
name: 'Init',
action: "!cof-init",
visibleto: '',
istokenaction: false,
inBar: true
}, {
name: 'Jets',
action: "!cof-jet",
visibleto: 'all',
istokenaction: true,
}, {
name: 'Jets-GM',
action: "!cof-jet --secret",
visibleto: '',
istokenaction: false,
inBar: true
}, {
name: 'Nuit',
action: "!cof-nouveau-jour ?{Repos?|Oui,--repos|Non}",
visibleto: '',
istokenaction: false,
inBar: true
}, {
name: 'Repos',
action: "!cof-recuperation",
visibleto: 'all',
istokenaction: true,
inBar: false
}, {
name: 'Statut',
action: "!cof-statut",
visibleto: 'all',
istokenaction: true
}, {
name: 'Surprise',
action: "!cof-surprise ?{difficulté}",
visibleto: '',
istokenaction: false,
inBar: true
}, {
name: 'Torche',
action: "!cof-torche @{selected|token_id}",
visibleto: 'all',
istokenaction: true,
}, {
name: 'Éteindre',
action: "!cof-eteindre-lumiere ?{Quelle lumière?|Tout}",
visibleto: '',
istokenaction: false,
inBar: true
}, {
name: 'devient',
action: "!cof-set-state ?{État|mort|surpris|assomme|renverse|aveugle|affaibli|etourdi|paralyse|ralenti|immobilise|endormi|apeure|invisible|blesse|encombre} true",
visibleto: '',
istokenaction: false,
inBar: true
}, {
name: 'enlève',
action: "!cof-set-state ?{État|mort|surpris|assomme|renverse|aveugle|affaibli|etourdi|paralyse|ralenti|immobilise|endormi|apeure|invisible|blesse|encombre} false",
visibleto: '',
istokenaction: false,
inBar: true
}, {
name: 'Suivre',
action: "!cof-suivre @{selected|token_id} @{target|token_id}",
visibleto: 'all',
istokenaction: true
}, {
name: 'undo',
action: "!cof-undo",
visibleto: '',
istokenaction: false,
inBar: true
}, {
name: PAUSE,
action: "!cof-pause",
visibleto: '',
istokenaction: false,
inBar: true
}, ];
function setGameMacros(msg) {
let playerId = msg.playerid;
let force = playerIsGM(playerId) && msg.content.includes('--force');
let inBar = [];
let allMacros = findObjs({
_type: 'macro'
});
gameMacros.forEach(function(m) {
let prev =
allMacros.find(function(macro) {
return macro.get('name') == m.name;
});
if (prev === undefined) {
m.playerid = playerId;
createObj('macro', m);
sendPlayer(msg, "Macro " + m.name + " créée.");
if (m.inBar) inBar.push(m.name);
} else if (force) {
prev.set('action', m.action);
prev.set('visibleto', m.visibleto);
prev.set('istokenaction', m.istokenaction);
sendPlayer(msg, "Macro " + m.name + " réécrite.");
if (m.inBar) inBar.push(m.name);
} else {
sendPlayer(msg, "Macro " + m.name + " déjà présente (utiliser --force pour réécrire).");
}
});
if (inBar.length > 0) {
sendPlayer(msg, "Macros à mettre dans la barre d'action du MJ : " + inBar.join(', '));
}
stateCOF.gameMacros = gameMacros;
}
function ajouteLumiere(msg) {
const options = parseOptions(msg);
if (options === undefined) return;
const cmd = options.cmd;
if (cmd === undefined || cmd.length < 3) {
error("Il faut au moins 2 arguments à !cof-lumiere", cmd);
return;
}
const cible = persoOfId(cmd[1]);
if (cible === undefined) {
error("le premier argument de !cof-lumière doit être un token", cmd);
return;
}
const radius = parseInt(cmd[2]);
if (isNaN(radius) || radius <= 0) {
error("La distance de vue de la lumière doit être positive", cmd[2]);
return;
}
let dimRadius = '';
if (cmd.length > 3) {
dimRadius = parseInt(cmd[3]);
if (isNaN(dimRadius)) {
error("La distance de vue de la lumière assombrie doit être un nombre", cmd[3]);
dimRadius = '';
}
}
let nomToken = 'lumiere';
if (cmd.length > 4) {
nomToken = cmd[4].trim();
if (nomToken === '') nomToken = 'lumiere';
}
const evt = {
type: 'lumiere',
};
addEvent(evt);
if (limiteRessources(options.lanceur, options, 'lumière', "lumière", evt)) return;
ajouteUneLumiere(cible, nomToken, radius, dimRadius, evt);
}
function eteindreUneLumiere(perso, pageId, al, lumName, evt) {
if (al === undefined) {
let attrLumiere = tokenAttribute(perso, 'lumiere');
al = attrLumiere.find(function(a) {
return a.get('current') == lumName;
});
if (al === undefined) return;
}
let lumId = al.get('max');
if (lumId == 'surToken') {
//Il faut enlever la lumière sur tous les tokens
let allTokens = [perso.token];
if (perso.token.get('bar1_value') !== '') {
allTokens = findObjs({
type: 'graphic',
represents: perso.charId
});
allTokens = allTokens.filter(function(tok) {
return tok.get('bar1_value') !== '';
});
}
allTokens.forEach(function(token) {
setToken(token, 'light_radius', '', evt);
setToken(token, 'light_dimradius', '', evt);
setToken(token, 'emits_bright_light', false, evt);
setToken(token, 'emits_low_light', false, evt);
});
al.remove();
return;
}
let lumiere = getObj('graphic', lumId);
if (lumiere === undefined) {
let tokensLumiere = findObjs({
_type: 'graphic',
layer: 'walls',
name: lumName
});
if (tokensLumiere.length === 0) {
log("Pas de token pour la lumière " + lumName);
al.remove();
return;
}
lumiere = tokensLumiere.shift();
if (tokensLumiere.length > 0) {
//On cherche le token le plus proche de perso
let d = distancePixToken(lumiere, perso.token);
let samePage = lumiere.get('pageid') == pageId;
tokensLumiere.forEach(function(tl) {
if (tl.get('pageid') != pageId) return;
if (samePage) {
let d2 = distancePixToken(tl, perso.token);
if (d2 < d) {
d = d2;
lumiere = tl;
}
} else {
lumiere = tl;
}
});
}
}
al.remove();
if (lumiere) lumiere.remove();
}
function eteindreLumieres(msg) {
const options = parseOptions(msg);
if (options === undefined) return;
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
sendPlayer(msg, "Pas de cible sélectionnée pour !cof-eteindre-lumiere", playerId);
return;
}
const cmd = options.cmd;
let groupe;
if (cmd.length > 1) groupe = cmd[1];
if (groupe && groupe.toLowerCase() == 'tout') groupe = '';
let pageId = options.pageId;
const evt = {
type: "Eteindre la lumière"
};
iterSelected(selected, function(perso) {
pageId = pageId || perso.token.get('pageid');
let attrLumiere = tokenAttribute(perso, 'lumiere');
attrLumiere.forEach(function(al) {
let lumName = al.get('current');
if (groupe && !lumName.startsWith(groupe)) return;
eteindreUneLumiere(perso, pageId, al, lumName, evt);
});
});
}, options);
}
//renvoit undefined si on ne compte pas les torches
function statutDesTorches(perso, eteindre) {
let nbTorches = 0;
let tempsTorche = 0;
let attrTorches = tokenAttribute(perso, 'torches');
if (attrTorches.length > 0) {
nbTorches = parseInt(attrTorches[0].get('current'));
if (isNaN(nbTorches) || nbTorches < 0) {
error("Nombre de torches incorrect", nbTorches);
return;
}
if (!eteindre && nbTorches === 0) {
whisperChar(perso.charId, "n'a pas de torche.");
return;
}
tempsTorche = parseInt(attrTorches[0].get('max'));
if (isNaN(tempsTorche) || tempsTorche < 0) {
error("Temps restant pour la torche incorrect", tempsTorche);
return;
}
return {
nbTorches,
tempsTorche
};
}
}
// return true si il y a une lumière de torche à éteindre, false sinon
function eteindreTorche(perso, pageId) {
let attrLumiere = tokenAttribute(perso, 'lumiere').filter(function(a) {
return a.get('current').startsWith('torche');
});
if (attrLumiere.length > 0) {
const evt = {
type: "Éteindre les torches"
};
addEvent(evt);
attrLumiere.forEach(function(al) {
let lumName = al.get('current');
eteindreUneLumiere(perso, pageId, al, lumName, evt);
});
let s = statutDesTorches(perso, true);
if (s) {
let {
nbTorches,
tempsTorche
} = s;
let msgTorche = "/w gm torche éteinte. ";
if (nbTorches > 1) {
msgTorche += "Reste " + (nbTorches - 1) + " torche";
if (nbTorches > 2) msgTorche += "s neuves";
else msgTorche += " neuve";
msgTorche += ", et une torche pouvant encore éclairer " + tempsTorche + " minutes.";
} else {
msgTorche += "Elle peut encore éclairer " + tempsTorche + " minutes.";
}
sendChar(perso.charId,
msgTorche +
boutonSimple("!cof-torche " + perso.token.id + " ?{Durée?}", "Temps depuis allumage"));
return true;
}
//On ne tient pas le compte précis des torches
whisperChar(perso.charId, "éteint sa torche");
return true;
}
}
function switchTorche(msg) {
const options = parseOptions(msg);
if (options === undefined) return;
const cmd = options.cmd;
if (cmd.length < 2) {
error("Il faut préciser le token en argument de !cof-torche");
return;
}
let pageId = options.pageId;
const perso = persoOfId(cmd[1], cmd[1], pageId);
if (perso === undefined) {
error("Token invalide", cmd);
return;
}
pageId = perso.token.get('pageid');
let diminueDuree = 0;
if (cmd.length > 2) {
//Dans ce cas, c'est pour diminuer la durée de vie de la torche
diminueDuree = parseInt(cmd[2]);
if (isNaN(diminueDuree) || diminueDuree <= 0) {
sendPlayer(msg, "Le deuxième argument de !cof-torche doit être un nombre strictement positif " + msg.content);
return;
}
}
if (eteindreTorche(perso, pageId)) return;
let s = statutDesTorches(perso, true);
if (s) {
let {
nbTorches,
tempsTorche
} = s;
if (tempsTorche === 0) {
if (nbTorches === 0) {
//On remet l'attribut dans un état convenable
const evt = {
type: "Formatage de l'attribut torches"
};
addEvent(evt);
setTokenAttr(perso, 'torches', 0, evt, {
maxVal: 60
});
return;
}
nbTorches--;
tempsTorche = 60;
}
if (diminueDuree) {
const evt = {
type: "Diminuer le duree de vie d'une torche"
};
addEvent(evt);
let temps = diminueDuree;
tempsTorche -= diminueDuree;
if (tempsTorche <= 0) {
nbTorches--;
temps += tempsTorche;
tempsTorche = 60;
let msgDiminue = "torche épuisée.";
if (nbTorches === 0) {
msgDiminue += " Plus de torche !";
} else if (nbTorches == 1) {
msgDiminue += " Plus qu'une torche.";
} else {
msgDiminue += " Il lui reste " + nbTorches + " torches.";
}
whisperChar(perso.charId, msgDiminue);
}
setTokenAttr(perso, 'torches', nbTorches, evt, {
maxVal: tempsTorche
});
sendChar(perso.charId, '/w gm temps de torche diminué de ' + temps + ' minutes');
return;
}
const evt = {
type: "Allumer une torche"
};
addEvent(evt);
ajouteUneLumiere(perso, 'torche', 13, 7, evt);
let msgAllume =
"allume une torche, qui peut encore éclairer pendant " + tempsTorche +
" minute";
if (tempsTorche > 1) msgAllume += 's';
msgAllume += '.';
if (nbTorches > 1) {
msgAllume += " Il lui reste encore " + (nbTorches - 1);
if (nbTorches == 2) msgAllume += " autre torche.";
else msgAllume += " autres torches.";
}
whisperChar(perso.charId, msgAllume);
} else {
//On ne tient pas le compte précis des torches
const evt = {
type: "Allumer une torche"
};
addEvent(evt);
ajouteUneLumiere(perso, 'torche', 13, 7, evt);
whisperChar(perso.charId, "allume sa torche");
}
}
//!cof-options
//!cof-options opt1 [... optn] val, met l'option à val
//!cof-options [opt0 ... optk] reset remet toutes les options à leur valeur patr défaut
//Dans tous les cas, affiche les options du niveau demandé
function setCofOptions(msg) {
const playerId = getPlayerIdFromMsg(msg);
if (!playerIsGM(playerId)) {
sendPlayer(msg, "Seul le MJ peut changer les options du script", playerId);
return;
}
let cmd = msg.content.split(' ');
let cofOptions = stateCOF.options;
if (cofOptions === undefined) {
sendPlayer(msg, "Options non diponibles", playerId);
return;
}
let prefix = '';
let up;
let defOpt = defaultOptions;
let newOption;
let lastCmd;
let fini;
let current = '';
cmd.shift();
cmd.forEach(function(c) {
if (fini) {
sendPlayer(msg, "Option " + c + " ignorée", playerId);
return;
}
if (c == 'reset') {
for (let opt in cofOptions) delete cofOptions[opt];
copyOptions(cofOptions, defOpt);
fini = true;
} else if (cofOptions[c]) {
if (cofOptions[c].type == 'options') {
if (defOpt[c] === undefined) {
sendPlayer(msg, "Option " + c + " inconnue dans les options par défaut", playerId);
fini = true;
return;
}
defOpt = defOpt[c].val;
cofOptions = cofOptions[c].val;
up = prefix;
prefix += ' ' + c;
} else {
newOption = cofOptions[c];
}
} else {
if (newOption) { //on met newOption à c
let val = c;
switch (newOption.type) {
case 'bool':
switch (c) {
case 'oui':
case 'true':
case '1':
val = true;
break;
case 'non':
case 'false':
case '0':
val = false;
break;
default:
sendPlayer(msg, "L'option " + lastCmd + " ne peut être que true ou false", playerId);
val = newOption.val;
}
fini = true;
break;
case 'int':
val = parseInt(c);
if (isNaN(val)) {
sendPlayer(msg, "L'option " + lastCmd + " est une valeur entière", playerId);
val = newOption.val;
}
fini = true;
break;
default:
if (current === '') current = val;
else current += ' ' + val;
val = current;
}
newOption.val = val;
} else if (lastCmd) {
sendPlayer(msg, "L'option " + lastCmd + " ne contient pas de sous-option " + c, playerId);
} else {
sendPlayer(msg, "Option " + c + " inconnue.", playerId);
}
}
lastCmd = c;
});
let titre = "Options de COFantasy";
if (prefix !== '') {
titre += "
" + prefix + ' (';
titre += boutonSimple('!cof-options' + up, 'retour') + ')';
}
let display = startFramedDisplay(playerId, titre, undefined, {
chuchote: true
});
for (let opt in cofOptions) {
let optVu = opt.replace(/_/g, ' ');
let line = '' +
optVu + ' : ';
let action = '!cof-options' + prefix + ' ' + opt;
let displayedVal = cofOptions[opt].val;
let after = '';
switch (cofOptions[opt].type) {
case 'options':
displayedVal = 'l';
break;
case 'bool':
action += ' ?{Nouvelle valeur de ' + optVu + '|actif,true|inactif,false}';
if (displayedVal)
// Bizarrement, le caractère '*' modifie la suite du tableau
displayedVal = '3';
else
displayedVal = '*';
break;
case 'int':
action += ' ?{Nouvelle valeur de ' + optVu + '(entier)}';
break;
case 'image':
action += " ?{Entrez l'url pour " + optVu + '}';
after =
'';
displayedVal = 'u';
break;
case 'son':
action += " ?{Entrez le nom du son pour " + optVu + '}';
if (displayedVal === '') {
displayedVal = 'u';
} else {
after = boutonSimple('!cof-jouer-son ' + displayedVal,
'> ');
displayedVal = 'm';
}
break;
default:
action += ' ?{Nouvelle valeur de ' + optVu + '}';
}
line += boutonSimple(action, displayedVal) + after;
addLineToFramedDisplay(display, line);
}
addLineToFramedDisplay(display, boutonSimple('!cof-options' + prefix + ' reset', 'Valeurs par défaut'), 70);
sendFramedDisplay(display);
}
function lancerDefiSamourai(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined || cmd.length < 3) {
error("cof-defi-samourai demande au moins 2 options",
msg.content);
return;
}
let pageId = options.pageId;
let samourai = persoOfId(cmd[1], cmd[1], pageId);
if (samourai === undefined) {
error("Le token sélectionné n'est pas valide", msg.content);
return;
}
if (attributeAsBool(samourai, 'defiSamourai')) {
sendPlayer(msg, nomPerso(samourai) + " a déjà lancé un défi durant ce combat.");
return;
}
let cible = persoOfId(cmd[2], cmd[2], pageId);
if (cible === undefined) {
error("Le deuxième token sélectionné n'est pas valide", msg.content);
return;
}
const evt = {
type: 'Défi samouraï'
};
let explications = [];
entrerEnCombat(samourai, [cible], explications, evt);
explications.forEach(function(m) {
sendPerso(samourai, m);
});
let bonus;
if (cmd.length > 3) {
bonus = parseInt(cmd[3]);
if (isNaN(bonus) || bonus < 1) {
error("Bonus de défi de samouraï incorrect", cmd[3]);
bonus = undefined;
}
}
if (bonus === undefined)
bonus = predicateAsInt(samourai, 'voieDeLHonneur', 2);
setTokenAttr(samourai, 'defiSamourai', bonus, evt, {
msg: nomPerso(samourai) + " lance un défi à " + nomPerso(cible),
maxVal: idName(cible)
});
}
function parseEnveloppement(msg) {
const options = parseOptions(msg);
if (options === undefined) return;
const cmd = options.cmd;
if (cmd === undefined) {
error("Problème de parse options", msg.content);
return;
}
if (cmd.length < 6) {
error("Il manque des arguments à !cof-enveloppement", cmd);
return;
}
let cube = persoOfId(cmd[1]);
if (cube === undefined) {
error("Token non défini", cmd[1]);
return;
}
if (!peutController(msg, cube)) {
sendPlayer(msg, "pas le droit d'utiliser ce bouton");
return;
}
let cible = persoOfId(cmd[2]);
if (cible === undefined) {
error("Token non défini", cmd[2]);
return;
}
let difficulte = parseInt(cmd[3]);
if (isNaN(difficulte)) {
error("Difficulté n'est pas un nombre, on prend 15 par défaut", cmd[3]);
difficulte = 15;
}
let exprDM;
let type;
switch (cmd[4]) {
case 'label':
case 'ability':
type = 'enveloppement';
exprDM = cmd[4] + ' ' + cmd[5];
break;
case 'etreinte':
type = 'étreinte';
exprDM = cmd[4] + ' ' + cmd[5];
break;
default:
error("Impossible de déterminer les dégâts quand enveloppé", cmd[4]);
return;
}
doEnveloppement(cube, cible, difficulte, type, exprDM, options);
}
//!cof-enveloppement cubeId targetId Difficulte Attaque
//Attaque peut être soit label l, soit ability a, soit etreinte expr
function doEnveloppement(attaquant, cible, difficulte, type, exprDM, options) {
const evt = {
type: type,
action: {
titre: "Enveloppement",
attaquant: attaquant,
cible: cible,
difficulte: difficulte,
type: type,
exprDM: exprDM,
options: options,
}
};
addEvent(evt);
//Choix de la caractéristique pour résister : FOR ou DEX
let caracRes = meilleureCarac('FOR', 'DEX', cible, 10 + modCarac(attaquant, 'force'));
let titre = (type == 'étreinte') ? 'Étreinte' : 'Enveloppement';
let display = startFramedDisplay(options.playerId, titre, attaquant, {
perso2: cible
});
let explications = [];
let rollId = 'enveloppement_' + cible.token.id;
testOppose(rollId, attaquant, 'FOR', options, cible, caracRes, options,
explications, evt,
function(res, crit, rt1, rt2) {
let act = " a absorbé ";
switch (res) {
case 1:
if (type == 'étreinte') act = " s'est enroulé autour de ";
explications.push(nomPerso(attaquant) + act + nomPerso(cible));
let attaquantId = idName(attaquant);
let maxval = difficulte;
if (type == 'étreinte') maxval = 'etreinte ' + difficulte;
setTokenAttr(cible, 'enveloppePar', attaquantId, evt, {
maxVal: maxval
});
let cibleId = idName(cible);
moveTokenWithUndo(cible.token, attaquant.token.get('left'), attaquant.token.get('top'), evt);
toFront(attaquant.token);
setTokenAttr(attaquant, 'enveloppe', cibleId, evt, {
maxVal: exprDM
});
if (type == 'étreinte') setState(cible, 'immobilise', true, evt);
break;
case 2:
if (caracRes == 'FOR') {
if (type == 'étreinte') act = 'étreindre';
else act = 'absorber';
explications.push(nomPerso(cible) + " résiste et ne se laisse pas " + act);
} else {
if (type == 'étreinte') act = "l'étreinte";
else act = "l'absorption";
explications.push(nomPerso(cible) + " évite " + act);
}
break;
default: //match null, la cible s'en sort
if (type == 'étreinte') act = "l'étreinte";
else act = "l'enveloppement";
explications.push(nomPerso(cible) + " échappe de justesse à " + act);
}
explications.forEach(function(e) {
addLineToFramedDisplay(display, e);
});
sendFramedDisplay(display);
});
}
//!cof-echapper-enveloppement
function parseEchapperEnveloppement(msg) {
let options = msg.options || parseOptions(msg);
if (options === undefined) return;
let libere =
options.cmd && options.cmd.length > 1 && options.cmd[1] == 'libere';
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
sendPlayer(msg, "!cof-echapper-enveloppement sans sélection de token", playerId);
log("!cof-echapper-enveloppement requiert de sélectionner des tokens");
return;
}
iterSelected(selected, function(perso) {
let attr = tokenAttribute(perso, 'enveloppePar');
if (attr.length === 0) {
sendPlayer(msg, nomPerso(perso) + " n'est pas englouti.", playerId);
return;
}
attr = attr[0];
let cube = persoOfIdName(attr.get('current'), options.pageId);
if (cube === undefined) {
error("Attribut enveloppePar mal formé, on le supprime", attr.get('current'));
attr.remove();
unlockToken(perso);
return;
}
let etreinte = false;
let maxAttr = attr.get('max') + '';
if (maxAttr.startsWith('etreinte ')) {
etreinte = true;
maxAttr = maxAttr.substring(9);
}
if (libere) {
let evt = {
type: 'Libération',
deletedAttributes: [attr]
};
addEvent(evt);
attr.remove();
unlockToken(perso);
if (etreinte) setState(perso, 'immobilise', false, evt);
attr = tokenAttribute(cube, 'enveloppe');
attr.forEach(function(a) {
let ca = persoOfIdName(a.get('current'));
if (ca && ca.token.id == perso.token.id) {
evt.deletedAttributes.push(a);
a.remove();
}
});
sendChar(cube.charId, "libère " + nomPerso(perso));
return;
}
let difficulte = parseInt(maxAttr);
if (isNaN(difficulte)) {
error("Difficulté mal formée", attr.get('max'));
difficulte = 15;
}
doEchapperEnveloppement(perso, etreinte, cube, difficulte, options);
});
});
}
function doEchapperEnveloppement(perso, etreinte, cube, difficulte, options) {
const evt = {
type: (etreinte) ? "echapperEtreinte" : "echapperEnveloppement",
personnage: perso,
action: {
perso: perso,
etreinte: etreinte,
cube: cube,
difficulte: difficulte,
options: options,
rolls: options.rolls || {}
}
};
addEvent(evt);
let titre = "Tentative de sortir de " + nomPerso(cube);
if (etreinte) titre = "Tentative de se libérer de l'etreinte de " + cube.tokName;
const display = startFramedDisplay(options.playerId, titre, perso, {
chuchote: options.secret
});
const testId = 'enveloppement_' + perso.token.id;
testCaracteristique(perso, 'FOR', difficulte, testId, options, evt,
function(tr) {
addLineToFramedDisplay(display, "Résultat : " + tr.texte);
if (tr.reussite) {
addLineToFramedDisplay(display, "C'est réussi, " + nomPerso(perso) + " s'extirpe de " + cube.tokName + tr.modifiers);
toFront(perso.token);
evt.deletedAttributes = evt.deletedAttributes || [];
let attr = tokenAttribute(perso, 'enveloppePar')[0];
evt.deletedAttributes.push(attr);
attr.remove();
unlockToken(perso, evt);
if (etreinte) setState(perso, 'immobilise', false, evt);
attr = tokenAttribute(cube, 'enveloppe');
attr.forEach(function(a) {
let ca = persoOfIdName(a.get('current'));
if (ca && ca.token.id == perso.token.id) {
evt.deletedAttributes.push(a);
a.remove();
}
});
} else {
let msgRate = "C'est raté." + tr.rerolls + tr.modifiers;
addLineToFramedDisplay(display, msgRate);
}
sendFramedDisplay(display);
});
}
function parseLibererAgrippe(msg) {
let options = msg.options || parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Problème de parse options", msg.content);
return;
}
if (cmd.length < 2) {
error("Il faut l'id du token en argument de !cof-liberer-agrippe", msg.content);
return;
}
let perso = persoOfId(cmd[1], cmd[1], options.pageId);
if (!perso) {
error("Token invalide", cmd);
return;
}
let attrName = 'estAgrippePar';
let attr = tokenAttribute(perso, 'estAgrippePar');
if (attr.length === 0) {
attr = tokenAttribute(perso, 'etreinteImmolePar');
attrName = 'etreinteImmolePar';
if (attr.length === 0) {
attr = tokenAttribute(perso, 'etreinteScorpionPar');
attrName = 'etreinteScorpionPar';
if (attr.length === 0) {
attr = tokenAttribute(perso, 'estDevorePar');
attrName = 'estDevorePar';
if (attr.length === 0) {
sendPlayer(msg, nomPerso(perso) + " n'est pas agrippé.");
return;
}
}
}
}
attr = attr[0];
let agrippant = persoOfIdName(attr.get('current'), options.pageId);
if (agrippant === undefined) {
error("Attribut " + attrName + " mal formé, on le supprime", attr.get('current'));
attr.remove();
unlockToken(perso);
return;
}
if (cmd.length > 2 && cmd[2] == 'libere') {
const evt = {
type: "Libère une cible agrippée"
};
finAgripper(perso, agrippant, attrName, evt);
sendPerso(agrippant, "relâche " + nomPerso(perso));
return;
}
doLibererAgrippe(perso, agrippant, attrName, options);
}
function finAgripper(perso, agrippant, attrName, evt) {
evt.deletedAttributes = evt.deletedAttributes || [];
let attr = tokenAttribute(perso, attrName);
attr[0].remove();
unlockToken(perso, evt);
evt.deletedAttributes.push(attr[0]);
if (attrName == 'estAgrippePar') removeTokenAttr(perso, 'agrippeParUnDemon', evt);
if (attrName == 'etreinteImmolePar' || attrName == 'estDevorePar' || attr[0].get('max'))
setState(perso, 'immobilise', false, evt);
if (attrName == 'etreinteScorpionPar') {
let etrScorpAttr = tokenAttribute(perso, "etreinteScorpionRatee");
if (etrScorpAttr && etrScorpAttr.length > 0) {
etrScorpAttr[0].remove();
evt.deletedAttributes.push(etrScorpAttr[0]);
}
}
let attrAgrippant = 'agrippe';
if (attrName == 'etreinteImmolePar') attrAgrippant = 'etreinteImmole';
if (attrName == 'etreinteScorpionPar') attrAgrippant = 'etreinteScorpionSur';
if (attrName == 'estDevorePar') attrAgrippant = 'devore';
attr = tokenAttribute(agrippant, attrAgrippant);
attr.forEach(function(a) {
let ca = persoOfIdName(a.get('current'));
if (ca && ca.token.id == perso.token.id) {
evt.deletedAttributes.push(a);
a.remove();
unlockToken(ca, evt);
}
});
}
//!cof-liberer-agrippe token_id
function doLibererAgrippe(perso, agrippant, attrName, options) {
const evt = {
type: 'libererAgrippe',
action: {
titre: "Se libérer",
perso,
agrippant,
attrName,
options
}
};
addEvent(evt);
let titre = "Tentative de se libérer de " + nomPerso(agrippant);
let playerId = options.playerId;
let display = startFramedDisplay(playerId, titre, perso, {
chuchote: options.secret
});
let explications = [];
const rollId = 'libererAgrippe_' + perso.token.id;
let options1 = {...options
};
if (attrName == 'etreinteImmolePar') options1.dice = 20;
testOppose(rollId, perso, 'FOR', options1, agrippant, 'FOR',
options, explications, evt,
function(tr, crit, rt1, rt2) {
explications.forEach(function(e) {
addLineToFramedDisplay(display, e);
});
if (tr == 2) {
let msgRate = "C'est raté, " + nomPerso(perso) + " est toujours ";
if (attrName == 'etreinteImmolePar' || attrName == 'etreinteScorpionPar')
msgRate += "prisonnier de l'étreinte de " + nomPerso(agrippant);
else if (attrName == 'estDevorePar')
msgRate += 'saisi' + eForFemale(perso) + '.';
else msgRate += "agrippé" + eForFemale(perso) + ".";
addLineToFramedDisplay(display, msgRate);
if (attrName == 'etreinteScorpionPar') { // Cas d'étreinte de scorpion avec dommages automatiques
let d6 = evt.action.rolls.etreinteDmg || rollDePlus(6, {
bonus: 3
});
evt.action.rolls.etreinteDmg = d6;
let r = {
total: d6.val,
type: 'normal',
display: d6.roll
};
let explications2 = [];
perso.ignoreTouteRD = true;
dealDamage(perso, r, [], evt, false, {}, explications2, function(dmgDisplay) {
let dmgMsg = "L'étreinte du scorpion inflige " + dmgDisplay + " dégâts.";
setTokenAttr(perso, "etreinteScorpionRatee", true, evt);
addLineToFramedDisplay(display, dmgMsg);
explications2.forEach(function(expl) {
addLineToFramedDisplay(display, expl, 80);
});
sendFramedDisplay(display);
});
} else {
sendFramedDisplay(display);
}
} else {
if (tr === 0)
addLineToFramedDisplay(display, "Réussi de justesse, " + nomPerso(perso) + " se libère.");
else //tr == 1
addLineToFramedDisplay(display, "Réussi, " + nomPerso(perso) + " se libère.");
toFront(perso.token);
finAgripper(perso, agrippant, attrName, evt);
sendFramedDisplay(display);
if (attrName == 'etreinteScorpionPar') {
turnAction(perso);
}
}
});
}
function parseLibererEcrase(msg) {
let options = msg.options || parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Problème de parse options", msg.content);
return;
}
if (cmd.length < 2) {
error("Il faut l'id du token en argument de !cof-liberer-ecrase", msg.content);
return;
}
let perso = persoOfId(cmd[1], cmd[1], options.pageId);
let attrName = 'estEcrasePar';
let attr = tokenAttribute(perso, attrName);
if (attr.length === 0) {
sendPlayer(msg, nomPerso(perso) + " n'est pas écrasé.");
return;
}
attr = attr[0];
let agrippant = persoOfIdName(attr.get('current'), options.pageId);
if (agrippant === undefined) {
error("Attribut " + attrName + " mal formé, on le supprime", attr.get('current'));
attr.remove();
unlockToken(perso);
return;
}
let difficulte = 16;
if (attributeAsBool(agrippant, 'rage')) difficulte += 2;
let bonus = 0;
let explications = [];
if (attributeAsBool(perso, 'protectionContreLeMal') &&
estMauvais(agrippant)) {
let bonusProtectionContreLeMal = getIntValeurOfEffet(perso, 'protectionContreLeMal', 2);
bonus += bonusProtectionContreLeMal;
explications.push("Protection contre le mal => +" + bonusProtectionContreLeMal + " au jet pour se libérer");
}
if (predicateAsBool(perso, 'actionLibre')) {
bonus += 5;
explications.push("Action libre => +5 pour résister aux entraves");
}
let carac = meilleureCarac('FOR', 'DEX', perso, difficulte - bonus);
if (bonus) options.bonus = bonus;
let titre = "Tentative de se libérer de " + nomPerso(agrippant);
doLibererEcrase(perso, agrippant, titre, carac, difficulte, explications, options);
}
//!cof-liberer-ecrase token_id
function doLibererEcrase(perso, agrippant, titre, carac, difficulte, explications, options) {
const evt = {
type: 'libererEcrase',
action: {
titre,
perso,
agrippant,
carac,
difficulte,
explications,
options
}
};
addEvent(evt);
let playerId = options.playerId;
let display = startFramedDisplay(playerId, titre, perso, {
chuchote: options.secret
});
const rollId = 'libererEcrase' + perso.token.id;
testCaracteristique(perso, carac, difficulte, rollId, options, evt,
function(tr, expl) {
let smsg = nomPerso(perso) + " fait ";
expl = expl.concat(explications);
if (expl.length === 0) {
smsg += tr.texte;
} else {
smsg += '' + tr.texte + '';
}
if (tr.reussite) {
smsg += " => réussi, " + nomPerso(perso) + " se libère.";
smsg += tr.modifiers;
addLineToFramedDisplay(display, smsg);
toFront(perso.token);
evt.deletedAttributes = evt.deletedAttributes || [];
let attr = tokenAttribute(perso, 'estEcrasePar');
attr[0].remove();
unlockToken(perso, evt);
evt.deletedAttributes.push(attr[0]);
attr = tokenAttribute(agrippant, 'ecrase');
attr.forEach(function(a) {
let ca = persoOfIdName(a.get('current'));
if (ca && ca.token.id == perso.token.id) {
evt.deletedAttributes.push(a);
a.remove();
unlockToken(ca, evt);
}
});
} else {
smsg += " => échec, " + nomPerso(perso) + " est toujours saisi dans les bras de " + nomPerso(agrippant);
smsg += tr.rerolls + tr.modifiers;
addLineToFramedDisplay(display, smsg);
}
sendFramedDisplay(display);
if (tr.reussite) turnAction(perso);
});
}
//!cof-animer-cadavre lanceur cible
function animerCadavre(msg) {
const options = msg.options || parseOptions(msg);
if (options === undefined) return;
const cmd = options.cmd;
if (cmd === undefined || cmd.length < 3) {
error("cof-animer-cadavre attend 2 arguments", msg.content);
return;
}
let lanceur = persoOfId(cmd[1], cmd[1], options.pageId);
if (lanceur === undefined) {
error("Le premier argument de !cof-animer-cadavre n'est pas un token valide", msg.content);
return;
}
let cible = persoOfId(cmd[2], cmd[2], options.pageId);
if (cible === undefined) {
error("Le deuxième argument de !cof-animer-cadavre n'est pas un token valide", msg.content);
return;
}
if (!getState(cible, 'mort')) {
sendPlayer(msg, nomPerso(cible) + " n'est pas mort" + eForFemale(cible) + ".");
return;
}
if (attributeAsBool(cible, 'cadavreAnime')) {
sendPlayer(msg, nomPerso(cible) + " a déjà été animé" + eForFemale(cible) + ".");
return;
}
let niveauLanceur = ficheAttributeAsInt(lanceur, 'niveau', 1);
let niveauCible = ficheAttributeAsInt(cible, 'niveau', 1);
if (niveauCible > niveauLanceur) {
sendPlayer(msg, nomPerso(cible) + " est de NC " + niveauCible + ", supérieur à celui de " + nomPerso(lanceur) + " (" + niveauLanceur + ")");
return;
}
const evt = {
type: "Animer un cadvre"
};
addEvent(evt);
if (limiteRessources(lanceur, options, 'animerUnCadavre', "animer un cadavre", evt)) return;
sendPerso(lanceur, 'réanime ' + nomPerso(cible));
setState(cible, 'mort', false, evt);
let pvmax = parseInt(cible.token.get('bar1_max'));
updateCurrentBar(cible, 1, pvmax, evt);
setTokenAttr(cible, 'cadavreAnime', true, evt, {
msg: 'se relève'
});
}
const niveauxEbriete = [
"sobre",
"pompette",
"bourré",
"ivre-mort",
"en coma éthylique"
];
function augmenteEbriete(personnage, evt, expliquer) {
let n = attributeAsInt(personnage, 'niveauEbriete', 0) + 1;
if (n >= niveauxEbriete.length) {
expliquer(nomPerso(personnage) + " est déjà en coma éthylique.");
return;
}
expliquer(nomPerso(personnage) + " devient " + niveauxEbriete[n]);
setTokenAttr(personnage, 'niveauEbriete', n, evt);
}
function diminueEbriete(personnage, evt, expliquer) {
let n = attributeAsInt(personnage, 'niveauEbriete', 0);
if (n < 1) return;
n--;
if (n >= niveauxEbriete.length) n = niveauxEbriete.length - 1;
expliquer(nomPerso(personnage) + " redevient " + niveauxEbriete[n]);
setTokenAttr(personnage, 'niveauEbriete', n, evt);
}
function parseVapeursEthyliques(msg) {
const options = parseOptions(msg);
if (options === undefined) return;
let cibles = [];
getSelected(msg, function(selected, playerId) {
if (selected === undefined || selected.length === 0) {
sendPlayer(msg, "pas de cible trouvée, action annulée", playerId);
return;
}
iterSelected(selected, function(perso) {
cibles.push(perso);
});
doVapeursEthyliques(playerId, cibles, options);
}, options); //fin getSelected
}
function doVapeursEthyliques(playerId, persos, options) {
const evt = {
type: 'vapeursEthyliques',
action: {
playerId: playerId,
persos: persos,
options: options
}
};
addEvent(evt);
if (limiteRessources(options.lanceur, options, 'vapeursEthyliques', "lancer une fiole de vapeurs éthyliques", evt)) {
addEvent(evt);
return;
}
let display = startFramedDisplay(playerId, 'Vapeurs éthyliques');
let expliquer = function(m) {
addLineToFramedDisplay(display, m);
};
const explications = [];
if (options.save) {
explications.push(" Jet de " + options.save.carac + " " + options.save.seuil + " pour résister à l'alcool");
}
entrerEnCombat(options.lanceur, persos, explications, evt);
explications.forEach(explication => expliquer(explications));
let count = persos.length;
let finalize = function() {
if (count == 1) sendFramedDisplay(display);
count--;
};
persos.forEach(function(perso) {
if (options.save) {
let saveOpts = {
hideSaveTitle: true,
rolls: options.rolls,
chanceRollId: options.chanceRollId,
type: 'poison'
};
let saveId = 'vapeursEthyliques_' + perso.token.id;
save(options.save, perso, saveId, expliquer, saveOpts, evt,
function(succes, rollText) {
if (!succes) {
augmenteEbriete(perso, evt, expliquer);
setTokenAttr(perso, 'vapeursEthyliques', 0, evt, {
maxVal: options.save.seuil
});
}
finalize();
});
} else { //pas de save
augmenteEbriete(perso, evt, expliquer);
setTokenAttr(perso, 'vapeursEthyliques', 0, evt);
finalize();
}
});
}
function desaouler(msg) {
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
sendPlayer(msg, "Aucune sélection pour !cof-desaouler", playerId);
return;
}
const evt = {
type: 'desaoûler'
};
const expliquer = function(s) {
sendChat('', s);
};
iterSelected(selected, function(perso) {
diminueEbriete(perso, evt, expliquer);
});
addEvent(evt);
});
}
function parseBoireAlcool(msg) {
const options = parseOptions(msg);
if (options === undefined) return;
let persos = [];
getSelected(msg, function(selected, playerId) {
iterSelected(selected, function(perso) {
persos.push(perso);
});
doBoireAlcool(playerId, persos, options);
}, options); //fin getSelected
}
function doBoireAlcool(playerId, persos, options) {
const evt = {
type: 'boireAlcool',
action: {
playerId: playerId,
persos: persos,
options: options
}
};
addEvent(evt);
if (limiteRessources(options.lanceur, options, 'boireAlcool', "est affecté par l'alcool", evt)) {
return;
}
const display = startFramedDisplay(playerId, 'Alcool');
const expliquer = function(m) {
addLineToFramedDisplay(display, m);
};
if (options.save) {
expliquer("Jet de " + options.save.carac + " " + options.save.seuil + " pour résister à l'alcool");
}
let count = persos.length;
const finalize = function() {
if (count == 1) sendFramedDisplay(display);
count--;
};
persos.forEach(function(perso) {
if (options.save) {
const saveOpts = {
hideSaveTitle: true,
rolls: options.rolls,
chanceRollId: options.chanceRollId,
type: 'poison'
};
let saveId = 'boireAlcool_' + perso.token.id;
save(options.save, perso, saveId, expliquer, saveOpts, evt,
function(succes, rollText) {
if (!succes) {
augmenteEbriete(perso, evt, expliquer);
}
finalize();
});
} else { //pas de save
augmenteEbriete(perso, evt, expliquer);
finalize();
}
});
}
function jouerSon(msg) {
let sonIndex = msg.content.indexOf(' ');
if (sonIndex > 0) {
//On joue un son
let son = msg.content.substring(sonIndex + 1);
playSound(son);
} else { //On arrête tous les sons
let AMdeclared;
try {
AMdeclared = Roll20AM;
} catch (e) {
if (e.name != "ReferenceError") throw (e);
}
if (AMdeclared) {
//With Roll20 Audio Master
sendChat("GM", "!roll20AM --audio,stop|");
} else {
let jukebox = findObjs({
type: 'jukeboxtrack',
playing: true
});
jukebox.forEach(function(track) {
track.set('playing', false);
});
}
}
}
function bonusCouvert(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) return;
let nouveauBonus = 0;
if (cmd.length > 1) {
nouveauBonus = parseInt(cmd[1]);
if (isNaN(nouveauBonus) || nouveauBonus < 0) {
error("Il faut un argument positif pour !cof-bonus-couvert", cmd);
return;
}
}
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
error('pas de token sélectionné pour !cof-bonus-couvert');
return;
}
const evt = {
type: 'Bonus couvert'
};
addEvent(evt);
if (limiteRessources(options.lanceur, options, 'bonus couvert', 'bonus couvert', evt)) return;
iterSelected(selected, function(perso) {
if (nouveauBonus) {
setTokenAttr(perso, 'bonusCouvert', nouveauBonus, evt, {
msg: "se met à couvert",
secret: options.secret
});
} else {
removeTokenAttr(perso, 'bonusCouvert', evt, {
msg: "n'est plus à couvert",
secret: options.secret
});
}
}); //fin iterSelected
}, options); //fin getSelected
}
//!cof-set-attribute nom valeur [max]
function setAttributeInterface(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) return;
if (cmd.length < 3) {
error("Pas assez d'arguments pour !cof-set-attribute", cmd);
return;
}
let opt = {
secret: options.secret
};
if (cmd.length > 3) {
opt.maxVal = cmd[3];
}
if (options.messages && options.messages.length > 0)
opt.msg = options.messages[0];
getSelected(msg, function(selected, playerId) {
/*if (!playerIsGM(playerId)) {
sendChat('COF', "Seul le MJ peut utiliser la commande !cof-set-attributes");
return;
}*/
if (selected.length === 0) {
error('pas de token sélectionné pour !cof-set-attribute');
return;
}
const evt = {
type: 'Changement attribut'
};
addEvent(evt);
if (limiteRessources(options.lanceur, options, 'changementAttribut', 'changementAttribut', evt)) return;
iterSelected(selected, function(perso) {
setTokenAttr(perso, cmd[1], cmd[2], evt, opt);
if (options.etats) {
for (let etat in options.etats) {
setState(perso, etat, options.etats[etat], evt);
}
}
}); //fin iterSelected
}, options);
}
function setPredicate(perso, predicat, evt) {
let pred = ficheAttribute(perso, 'predicats_script', '');
if (pred.includes(predicat)) {
return;
}
if (pred === '') pred = predicat;
else pred = pred + ' ' + predicat;
evt = evt || {};
setFicheAttr(perso, 'predicats_script', pred, evt);
}
//!cof-set-predicate nom [valeur]
function setPredicateInterface(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) return;
if (cmd.length < 2) {
error("Pas assez d'arguments pour !cof-set-predicate", cmd);
return;
}
let predicat = cmd[1].trim();
let set = true;
if (cmd.length > 2) {
set = cmd[2];
}
let opt = {
secret: options.secret
};
if (options.messages && options.messages.length > 0)
opt.msg = options.messages[0];
let predicats = state.COFantasy.predicats;
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
error('pas de token sélectionné pour !cof-set-predicate');
return;
}
const evt = {
type: 'Changement predicat'
};
addEvent(evt);
if (limiteRessources(options.lanceur, options, 'changementPredicat', 'changementPredicat', evt)) return;
iterSelected(selected, function(perso) {
let pred = ficheAttribute(perso, 'predicats_script', '');
switch (set) {
case 'true':
case 'vrai':
case 'oui':
if (pred.includes(predicat)) {
sendPlayer(msg, 'Prédicat ' + predicat + ' déjà présent', playerId);
return;
}
if (pred === '') pred = predicat;
else pred = pred + ' ' + predicat;
setFicheAttr(perso, 'predicats_script', pred, evt);
if (predicats) predicats[perso.charId] = undefined;
sendPlayer(msg, 'Prédicat ' + predicat + ' ajouté', playerId);
break;
case 'false':
case 'faux':
case 'non':
let regPred = new RegExp('(^| |,|\n)' + predicat + '($| |,|\n)');
let newPred = pred.replace(regPred, ' ');
if (newPred == pred) {
sendPlayer(msg, 'Prédicat ' + predicat + ' non trouvé', playerId);
} else {
setFicheAttr(perso, 'predicats_script', newPred, evt);
if (predicats) predicats[perso.charId] = undefined;
sendPlayer(msg, 'Prédicat ' + predicat + ' enlevé', playerId);
}
break;
default:
error("L'argument de " + msg.content + " est inutilisable", set);
return;
}
if (options.etats) {
for (let etat in options.etats) {
setState(perso, etat, options.etats[etat], evt);
}
}
}); //fin iterSelected
}, options);
}
//!cof-defense-armee-des-morts tokenId
function defenseArmeeDesMorts(msg) {
var options = parseOptions(msg);
if (options === undefined) return;
var cmd = options.cmd;
if (cmd === undefined) return;
if (cmd.length < 2) {
error("Pas assez d'arguments pour !cof-defense-armee-des-morts", cmd);
return;
}
var perso = persoOfId(cmd[1]);
if (perso === undefined) {
error("Le token renseigné pour !cof-defense-armee-des-morts est inconnu", cmd);
return;
}
if (!peutController(msg, perso)) {
sendPlayer(msg, "ne peut pas faire ça.");
return;
}
var evt = {
type: "DefenseArmeeDesMorts"
};
addEvent(evt);
var opt = {
msg: "se défend contre les morts"
};
setTokenAttr(perso, "defenseArmeeDesMorts", true, evt, opt);
}
function addLigneOptionAttaque(display, perso, val, texte, attr) {
var box;
var action = "!cof-options-d-attaque " + attr + "_check ?{" + texte + "?|";
if (val) {
box = '3';
action += "Non|Oui}";
} else {
box = ' ';
action += "Oui|Non}";
}
action += " --target " + perso.token.id;
var ligne = boutonSimple(action, box) + texte;
addLineToFramedDisplay(display, ligne);
}
//!cof-options-d-attaque, affiche les options d'attaque du token sélectionné
// si on donne reset en argument, remet tout à zéro
// si on donne en argument option valeur, change la valeur de l'option
function optionsDAttaque(msg) {
const options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) return;
let evt = {
type: "Option d'attaque"
};
let def0 = {
default: 0
};
getSelected(msg, function(selected, playerId) {
iterSelected(selected, function(perso) {
//D'abord on lit les valeurs
let aepc = ficheAttributeAsBool(perso, 'attaque_en_puissance_check', false);
let arc = ficheAttributeAsBool(perso, 'attaque_risquee_check', false);
let aac = ficheAttributeAsBool(perso, 'attaque_assuree_check', false);
let adtc = ficheAttributeAsBool(perso, 'attaque_dm_temp_check', false);
let aep = ficheAttributeAsInt(perso, 'attaque_en_puissance', 1);
let adg;
if (persoEstPNJ(perso)) {
adg = ficheAttributeAsInt(perso, 'attaque_de_groupe', 1);
}
if (cmd.length > 1 && cmd[1] == 'reset') {
if (adg > 1) {
setFicheAttr(perso, 'attaque_de_groupe', 1, evt, {
default: 1
});
adg = 1;
}
if (aepc) {
setFicheAttr(perso, 'attaque_en_puissance_check', 0, evt, def0);
aepc = false;
}
if (arc) {
setFicheAttr(perso, 'attaque_risquee_check', 0, evt, def0);
arc = false;
}
if (aac) {
setFicheAttr(perso, 'attaque_assuree_check', 0, evt, def0);
aac = false;
}
if (adtc) {
setFicheAttr(perso, 'attaque_dm_temp_check', 0, evt, def0);
adtc = false;
}
turnAction(perso, playerId);
return;
} else if (cmd.length > 2) {
switch (cmd[1]) {
case 'attaque_en_puissance_check':
if (cmd[2] == 'true' || cmd[2] == 'oui' || cmd[2] == 'Oui') {
if (!aepc) {
setFicheAttr(perso, cmd[1], 1, evt);
aepc = true;
aac = false;
}
} else {
if (aepc) {
setFicheAttr(perso, cmd[1], 0, evt, def0);
aepc = false;
}
}
break;
case 'attaque_risquee_check':
if (cmd[2] == 'true' || cmd[2] == 'oui' || cmd[2] == 'Oui') {
if (!arc) {
setFicheAttr(perso, cmd[1], 1, evt);
arc = true;
}
} else {
if (arc) {
setFicheAttr(perso, cmd[1], 0, evt, def0);
arc = false;
}
}
break;
case 'attaque_assuree_check':
if (cmd[2] == 'true' || cmd[2] == 'oui' || cmd[2] == 'Oui') {
if (!aac) {
setFicheAttr(perso, cmd[1], 1, evt);
aac = true;
aepc = false;
}
} else {
if (aac) {
setFicheAttr(perso, cmd[1], 0, evt, def0);
aac = false;
}
}
break;
case 'attaque_dm_temp_check':
if (cmd[2] == 'true' || cmd[2] == 'oui' || cmd[2] == 'Oui') {
if (!adtc) {
setFicheAttr(perso, cmd[1], 1, evt);
adtc = true;
}
} else {
if (adtc) {
setFicheAttr(perso, cmd[1], 0, evt, def0);
adtc = false;
}
}
break;
case 'attaque_de_groupe':
if (persoEstPNJ(perso)) {
var nadg = parseInt(cmd[2]);
if (isNaN(nadg) || nadg < 1) nadg = 1;
if (nadg != adg) {
setFicheAttr(perso, cmd[1], nadg, evt, {
default: 1
});
adg = nadg;
}
}
break;
case 'attaque_en_puissance':
let naep = parseInt(cmd[2]);
if (isNaN(naep) || naep < 1) naep = 1;
if (naep != aep) {
setFicheAttr(perso, cmd[1], naep, evt, {
default: 1
});
aep = naep;
}
break;
default:
error("Argument de !cof-options-d-attaque non reconnu", cmd);
//Mais on peut quand même afficher les options
}
turnAction(perso, playerId);
return;
}
var action;
var title = "Options d'attaque";
var opt_display = {
chuchote: true
};
if (aepc || arc || aac || adtc || (persoEstPNJ(perso) && adg > 1)) {
action = "!cof-options-d-attaque reset --target " + perso.token.id;
opt_display.action_right = boutonSimple(action, 'réinit.');
}
var display = startFramedDisplay(playerId, title, perso, opt_display);
var ligne = '';
var overlay = '';
if (persoEstPNJ(perso)) {
ligne = "Attaque de groupe : ";
action = "!cof-options-d-attaque attaque_de_groupe ?{Combien d'adversaires par jet?}";
action += " --target " + perso.token.id;
overlay = 'title="+2 Att. par créature, si Att > DEF + 5, DM x2, si critique DM x3"';
ligne += boutonSimple(action, adg, overlay);
if (adg < 2) {
ligne += "attaquant";
} else {
ligne += "attaquants";
}
addLineToFramedDisplay(display, ligne);
}
var text;
action = "!cof-options-d-attaque attaque_en_puissance_check ?{Attaque en puissance?|";
if (aepc) {
text = '3';
action += "Non|Oui}";
} else {
text = ' ';
action += "Oui|Non}";
}
action += " --target " + perso.token.id;
ligne = boutonSimple(action, text) + "Attaque en puissance";
action = "!cof-options-d-attaque attaque_en_puissance ?{nombre de dés de bonus (-5 att par dé)?}";
action += " --target " + perso.token.id;
ligne += "(+" + boutonSimple(action, aep) + "d";
if (predicateAsBool(perso, 'tropPetit') && !attributeAsBool(perso, 'grandeTaille')) {
ligne += "4 DM)";
} else {
ligne += "6 DM)";
}
addLineToFramedDisplay(display, ligne);
addLigneOptionAttaque(display, perso, arc, "Attaque risquée", 'attaque_risquee');
addLigneOptionAttaque(display, perso, aac, "Attaque assurée", 'attaque_assuree');
addLigneOptionAttaque(display, perso, adtc, "Attaque pour assommer", 'attaque_dm_temp');
sendFramedDisplay(display);
});
});
if (evt.attributes) addEvent(evt);
}
//si evt est défini, on ajoute les actions à evt
function nePlusSuivre(perso, pageId, evt, reutilise) {
let attrSuit = tokenAttribute(perso, 'suit');
if (attrSuit.length > 0) {
attrSuit = attrSuit[0];
let idSuivi = attrSuit.get('current');
let suivi = persoOfIdName(idSuivi, pageId);
if (evt) {
evt.attributes = evt.attribute || [];
evt.attributes.push({
attribute: attrSuit,
current: idSuivi,
max: attrSuit.get('max')
});
}
if (!reutilise) attrSuit.remove();
if (suivi === undefined) {
sendPerso(perso, "ne suit plus personne");
return;
} else {
sendPerso(perso, "ne suit plus " + nomPerso(suivi));
let suivantDeSuivi = tokenAttribute(suivi, 'estSuiviPar');
if (suivantDeSuivi.length > 0) {
suivantDeSuivi = suivantDeSuivi[0];
let currentSuivantDeSuivi = suivantDeSuivi.get('current');
let found;
let csds = currentSuivantDeSuivi.split(':::').filter(function(idn) {
if (found) return true;
let sp = splitIdName(idn);
if (sp === undefined) return false;
if (sp.id == perso.token.id) {
found = true;
return false;
}
if (sp.name == perso.token.get('name')) {
found = true;
return false;
}
return true;
});
if (csds.length === 0) {
if (evt) {
evt.deletedAttributes = evt.deletedAttributes || [];
evt.deletedAttributes.push(suivantDeSuivi);
}
suivantDeSuivi.remove();
} else {
if (evt) {
evt.attributes.push({
attribute: suivantDeSuivi,
current: currentSuivantDeSuivi
});
}
suivantDeSuivi.set('current', csds.join(':::'));
}
}
}
return attrSuit;
}
}
//!cof-suivre @{selected|token_id} @{target|token_id}
function suivre(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) return;
if (cmd.length < 3) {
error("Pas assez d'arguments pour !cof-suivre", cmd);
return;
}
let perso = persoOfId(cmd[1], cmd[1], options.pageId);
if (perso === undefined) {
error("Token sélectionne incorrect pour !cof-suivre", cmd);
return;
}
let pageId = perso.token.get('pageid');
let cible = persoOfId(cmd[2], cmd[2], pageId);
if (cible === undefined) {
error("Cible incorrecte pour !cof-suivre", cmd);
return;
}
let evt = {
type: 'Suivre',
attributes: []
};
//D'abord on arrête de suivre si on suivait quelqu'un
let attrSuit = nePlusSuivre(perso, pageId, evt, true);
let cibleId = idName(cible);
let attr = tokenAttribute(cible, 'estSuiviPar');
let suiveurs;
if (attr.length === 0) {
attr = setTokenAttr(cible, 'estSuiviPar', '', evt);
suiveurs = [];
} else {
attr = attr[0];
suiveurs = attr.get('current');
evt.attributes.push({
attribute: attr,
current: suiveurs,
});
suiveurs = suiveurs.split(':::');
}
let xt = perso.token.get('left');
let yt = perso.token.get('top');
let xc = cible.token.get('left');
let yc = cible.token.get('top');
let distance = Math.floor(Math.sqrt((xc - xt) * (xc - xt) + (yc - yt) * (yc - yt)));
if (attrSuit) {
//alors evt contient déjà attrSuit
attrSuit.set('current', cibleId);
attrSuit.set('max', distance);
} else {
setTokenAttr(perso, 'suit', cibleId, evt, {
maxVal: distance
});
}
suiveurs.push(idName(perso));
attr.set('current', suiveurs.join(':::'));
sendPerso(perso, "suit " + nomPerso(cible));
addEvent(evt);
}
// !cof-centrer-sur-token tid (ou nom de token)
function centrerSurToken(msg) {
let cmd = msg.content.split(' ').filter(function(c) {
return c !== '';
});
if (cmd.length < 2) {
error("Il faut préciser un token sur lequel se centrer", cmd);
return;
}
let playerId = getPlayerIdFromMsg(msg);
let pageId;
if (playerIsGM(playerId)) {
let p = getObj('player', playerId);
if (p === undefined) {
error("Impossible de trouver le joueur qui a lancé la commande", msg);
return;
}
pageId = p.get('_lastpage');
} else {
let c = Campaign();
let ps = c.get('playerspecificpages');
if (ps) pageId = ps[playerId];
if (pageId === undefined) pageId = c.get('playerpageid');
}
let indexNom = msg.content.indexOf(' ');
let nom = msg.content.substring(indexNom).trim();
let perso = persoOfId(cmd[1], nom, pageId);
if (perso === undefined) {
sendPlayer(msg, "Impossible de trouver le personnage sur lequel se centrer", playerId);
return;
}
let token = perso.token;
sendPing(token.get('left'), token.get('top'), pageId, playerId, true, playerId);
}
function afficherRichesse(perso, dest) {
var msg = '';
var possede;
var pp = ficheAttributeAsInt(perso, 'bourse_pp', 0);
if (pp > 0) {
msg = pp + " PP";
possede = true;
}
var po = ficheAttributeAsInt(perso, 'bourse_po', 0);
if (po > 0) {
if (possede) msg += ', ';
else possede = true;
msg += po + " PO";
}
var pa = ficheAttributeAsInt(perso, 'bourse_pa', 0);
if (pa > 0) {
if (possede) msg += ', ';
else possede = true;
msg += pa + " PA";
}
var pc = ficheAttributeAsInt(perso, 'bourse_pc', 0);
if (pc > 0) {
if (possede) msg += ' et ';
else possede = true;
msg += pc + " PC";
}
if (possede) msg = 'possède ' + msg;
else msg = "n'a pas d'argent";
if (dest) sendPlayer(dest, nomPerso(perso) + ' ' + msg);
else whisperChar(perso.charId, msg);
}
function depenserSous(perso, unite, bourse, depense) {
if (depense <= 0) return 0;
var retenue = 0;
if (bourse[unite] < depense) {
retenue = Math.ceil((depense - bourse[unite]) / 10);
bourse[unite] += retenue * 10;
}
bourse[unite] -= depense;
return retenue;
}
function ajouteLignePieces(perso, display, unite, nom, piece) {
let finAction = unite + " --target " + perso.token.id;
let val = ficheAttributeAsInt(perso, 'bourse_' + unite, 0);
let line = '
';
addLineToFramedDisplay(display, line);
}
//!cof-bourse [action]
//Les actions peuvent être depenser val [unite], fixer val unite ou gagner val [unite]
function gestionBourse(msg) {
var cmd = msg.content.split(' ').filter(function(c) {
return c.trim() !== '';
});
var action = '';
var montant;
var unite = 'pa';
var depense = {
pc: 0,
pa: 0,
po: 0,
pp: 0
};
if (cmd.length > 1) {
switch (cmd[1]) {
case 'depenser':
case 'dépenser':
case 'gagner':
case 'fixer':
if (cmd.length < 3) {
error("Il faut spécifier un montant à " + cmd[1], msg.content);
return;
}
montant = parseInt(cmd[2]);
if (isNaN(montant)) {
error("montant " + cmd[2] + " incorrect", cmd);
return;
}
if (cmd[1] == 'gagner') {
montant = -montant;
action = 'depenser';
} else if (cmd[1] == 'fixer') {
if (montant < 0) {
error("On ne peut avoir qu'un nombre positif de pièces", cmd);
return;
}
action = 'fixer';
} else action = 'depenser';
if (cmd.length > 3 && !cmd[3].startsWith('--')) {
unite = cmd[3].toLowerCase().trim();
if (unite != 'pp' && unite != 'po' && unite != 'pa' && unite != 'pc') {
error("Pièces non reconnues : " + cmd[3], cmd);
return;
}
depense[unite] = montant;
} else if (action == 'fixer') {
error("Il faut préciser les unités pour !cof-bourse fixer", msg.content);
return;
}
depense.total = depense.pc + 10 * (depense.pa + 10 * (depense.po + 10 * depense.pp));
}
}
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
sendPlayer(msg, "Pas de personnage sélectionné pour !cof-bourse", playerId);
return;
}
var evt = {
type: 'bourse'
};
iterSelected(selected, function(perso) {
switch (action) {
case 'depenser':
var pc = ficheAttributeAsInt(perso, 'bourse_pc', 0);
var pa = ficheAttributeAsInt(perso, 'bourse_pa', 0);
var po = ficheAttributeAsInt(perso, 'bourse_po', 0);
var pp = ficheAttributeAsInt(perso, 'bourse_pp', 0);
if (depense.total < 0) {
if (depense.pc < 0) {
pc -= depense.pc;
setTokenAttr(perso, 'bourse_pc', pc, evt, {
charAttr: true
});
}
if (depense.pa < 0) {
pa -= depense.pa;
setTokenAttr(perso, 'bourse_pa', pa, evt, {
charAttr: true
});
}
if (depense.po < 0) {
po -= depense.po;
setTokenAttr(perso, 'bourse_po', po, evt, {
charAttr: true
});
}
if (depense.pp < 0) {
pp -= depense.pp;
setTokenAttr(perso, 'bourse_pp', pp, evt, {
charAttr: true
});
}
addEvent(evt);
afficherRichesse(perso);
return;
}
var montantPossede = pc + 10 * (pa + 10 * (po + 10 * pp));
if (montantPossede < depense.total) {
sendPerso(perso, "ne possède pas assez d'argent pour cette dépense");
afficherRichesse(perso, msg);
return;
}
var bourse = {
pc: pc,
pa: pa,
po: po,
pp: pp
};
// On privilégie les dépenses directes
var dpp = depense.pp;
if (dpp <= bourse.pp) {
bourse.pp -= dpp;
dpp = 0;
} else {
dpp -= bourse.pp;
bourse.pp = 0;
}
var dpo = depense.po;
if (dpo < bourse.po) {
bourse.po -= dpo;
dpo = 0;
} else {
dpo -= bourse.po;
bourse.po = 0;
}
var dpa = depense.pa;
if (dpa <= bourse.pa) {
bourse.pa -= dpa;
dpa = 0;
} else {
dpa -= bourse.pa;
bourse.pa = 0;
}
var dpc = depense.pc;
if (dpc < bourse.pc) {
bourse.pc -= dpc;
dpc = 0;
} else {
dpc -= bourse.pc;
bourse.pc = 0;
}
// Puis on dépense d'abord la petite monnaie
var v = dpc + 10 * (dpa + 10 * (dpo + 10 * dpp));
v = depenserSous(perso, 'pc', bourse, v);
v = depenserSous(perso, 'pa', bourse, v);
v = depenserSous(perso, 'po', bourse, v);
v = depenserSous(perso, 'pp', bourse, v);
if (v > 0) {
error("Erreur interne de calcul, il reste " + v + " PP à dépenser ??", bourse);
return;
}
if (bourse.pc != pc)
setTokenAttr(perso, 'bourse_pc', bourse.pc, evt, {
charAttr: true
});
if (bourse.pa != pa)
setTokenAttr(perso, 'bourse_pa', bourse.pa, evt, {
charAttr: true
});
if (bourse.po != po)
setTokenAttr(perso, 'bourse_po', bourse.po, evt, {
charAttr: true
});
if (bourse.pp != pp)
setTokenAttr(perso, 'bourse_pp', bourse.pp, evt, {
charAttr: true
});
addEvent(evt);
afficherRichesse(perso);
return;
case 'fixer':
addEvent(evt);
setTokenAttr(perso, 'bourse_' + unite, montant, evt, {
charAttr: true
});
afficherRichesse(perso);
return;
default:
if (selected.length > 1) {
afficherRichesse(perso, msg);
return;
}
let optionsDisplay = {
chuchote: true,
image: 'https://www.on6rm.be/wp-content/uploads/2015/08/vignette_2_bourse-argent.png'
};
let titre = "Bourse de " + nomPerso(perso);
let display = startFramedDisplay(playerId, titre, perso, optionsDisplay);
ajouteLignePieces(perso, display, 'pp', 'Platine', "de platine");
ajouteLignePieces(perso, display, 'po', 'Or', "d'or");
ajouteLignePieces(perso, display, 'pa', 'Argent', "d'argent");
ajouteLignePieces(perso, display, 'pc', 'Cuivre', "de cuivre");
sendFramedDisplay(display);
}
});
});
}
//!cof-mot-de-pouvoir-immobilise --lanceur toid
function motDePouvoirImmobilise(msg) {
let options = parseOptions(msg);
let pageId = options.pageId;
let evt = {
type: 'Mot de pouvoir'
};
addEvent(evt);
if (options.lanceur) {
sendPerso(options.lanceur, "prononce un mot avec la Voix d'une puissance supérieure. Tous ses ennemis sont immobilisés et ses alliés sont galvanisés.");
let allies = alliesParPerso[options.lanceur.charId];
if (allies) {
let tokens = findObjs({
_type: 'graphic',
_subtype: 'token',
_pageid: pageId,
layer: 'objects'
});
tokens.forEach(function(tok) {
let ci = tok.get('represents');
if (ci === '') return;
if (!allies.has(ci)) return;
let perso = {
charId: ci,
token: tok
};
setAttrDuree(perso, 'bonusAttaqueTemp', 1, evt);
});
}
}
getSelected(msg, function(selected, playerId) {
iterSelected(selected, function(perso) {
if (predicateAsBool(perso, 'liberteDAction') ||
predicateAsBool(perso, 'actionLibre') ||
(predicateAsInt(perso, 'voieDeLArchange', 1) > 1 && attributeAsBool(perso, 'formeDAnge'))
) {
sendPerso(perso, "reste libre de ses mouvements");
return;
}
setState(perso, 'immobilise', true, evt);
setAttrDuree(perso, 'immobiliseTemp', 1, evt);
});
}, options);
}
// Ajoute evt à l'historique si présent
// msg n'est pas forcément présent
function nextTurnChargeFantastique(msg, oldTurnOrder) {
if (oldTurnOrder) Campaign().set('turnorder', oldTurnOrder);
let cf = stateCOF.chargeFantastique;
if (cf === undefined) {
sendChat("Pas de charge fantastique en cours");
return;
}
if (stateCOF.chargeFantastique.activeTokenId && !peutController(msg, persoOfId(stateCOF.chargeFantastique.activeTokenId))) {
sendPlayer(msg, "ne peut utiliser ce bouton maintenant");
return;
}
let evt = {
type: "Tour de charge fantastique",
chargeFantastique: cf
};
let tid, perso;
if (cf.mouvements && cf.mouvements.length > 0) {
evt.chargeFantastique.mouvements = [...cf.mouvements];
evt.chargeFantastique.attaques = cf.attaques;
tid = cf.mouvements.shift();
perso = persoOfId(tid);
if (perso === undefined) {
error("Personnage en charge fantastique manquant", tid);
return;
}
addEvent(evt);
let playerIds = getPlayerIds(perso);
let playerId;
let optionsDisplay = {
chuchote: 'gm'
};
if (playerIds.length > 0) {
playerId = playerIds[0];
optionsDisplay.chuchote = true;
}
stateCOF.chargeFantastique.activeTokenId = perso.token.id;
setTokenInitAura(perso);
let display = startFramedDisplay(playerId, "Charge fantastique", perso, optionsDisplay);
addLineToFramedDisplay(display, "Phase de mouvement : déplacez votre token en ligne droite");
addLineToFramedDisplay(display, "puis " + boutonSimple("!cof-next-charge-fantastique", "cliquez ici"));
sendFramedDisplay(display);
return;
}
if (cf.attaques && cf.attaques.length > 0) {
evt.chargeFantastique.attaques = [...cf.attaques];
tid = cf.attaques.shift();
cf.tokenAttaque = tid;
if (cf.attaques.length === 0) cf.attaques = undefined;
perso = persoOfId(tid);
if (perso === undefined) {
error("Personnage en charge fantastique manquant", tid);
return;
}
addEvent(evt);
stateCOF.chargeFantastique.activeTokenId = perso.token.id;
setTokenInitAura(perso);
turnAction(perso);
return;
}
stateCOF.chargeFantastique = undefined;
}
// !cof-charge-fantastique token_id
function chargeFantastque(msg) {
const options = parseOptions(msg);
const cmd = options.cmd;
if (!cmd || cmd.length < 2) {
error("Pas assez d'arguments pour !cof-charge-fantastique", cmd);
return;
}
const chevalier = persoOfId(cmd[1], cmd[1], options.pageId);
if (chevalier === undefined) {
error("Le token sélectionné ne représente pas un personnage", cmd);
return;
}
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
error('pas de token sélectionné pour !cof-charge-fantastique', msg.content);
return;
}
const evt = {
type: 'Charge fantastique'
};
addEvent(evt);
if (limiteRessources(chevalier, options, 'chargeFantastique', 'charge fantastique', evt)) return;
sendPerso(chevalier, "mène une charge fantastique !");
initiative(selected, evt);
stateCOF.chargeFantastique = {};
let ordreActions = [];
let chevalierIn;
iterSelected(selected, function(perso) {
if (perso.token.id == chevalier.token.id) {
chevalierIn = true;
return;
}
ordreActions.push({
init: persoInit(perso, evt),
perso: perso
});
});
ordreActions.sort(function(p1, p2) {
if (p1.init < p2.init) return 1;
if (p1.init > p2.init) return -1;
// Priorité aux joueurs
// Premier critère : la barre de PV des joueurs est liée
if (p1.perso.token.get('bar1_link') === '') {
if (p2.perso.token.get('bar1_link') === '') return 0;
return 1;
}
if (p2.perso.token.get('bar1_link') === '') return -1;
// Deuxième critère : les joueurs ont un DV
let dvA = ficheAttributeAsInt(p1.perso, "DV", 0);
let dvB = ficheAttributeAsInt(p2.perso, "DV", 0);
if (dvA === 0) {
if (dvB === 0) return 0;
return 1;
}
if (dvB === 0) return -1;
//Entre joueurs, priorité à la plus grosse sagesse
let sagA = ficheAttributeAsInt(p1.perso, 'sagesse', 10);
let sagB = ficheAttributeAsInt(p2.perso, 'sagesse', 10);
if (sagA < sagB) return 1;
if (sagB > sagA) return -1;
return 0;
});
let mouvements = ordreActions.map(function(p) {
return p.perso.token.id;
});
if (chevalierIn) mouvements.unshift(chevalier.token.id);
stateCOF.chargeFantastique.mouvements = mouvements;
stateCOF.chargeFantastique.attaques = [...mouvements];
nextTurnChargeFantastique();
}, {
lanceur: chevalier
}); //fin du getSelected
}
//!cof-prescience token_id
function utiliserPrescience(msg) {
let options = parseOptions(msg);
let cmd = options.cmd;
if (!cmd || cmd.length < 2) {
error("Pas assez d'arguments pour !cof-prescience", cmd);
return;
}
let ensorceleur = persoOfId(cmd[1], cmd[1], options.pageId);
if (ensorceleur === undefined) {
error("Impossible de trouver le personnage qui utilise prescience", cmd);
return;
}
let combat = stateCOF.combat;
if (!combat) {
sendPlayer(msg, "On ne peut utiliser prescience qu'en combat");
return;
}
if (stateCOF.prescience === undefined) {
error("Pas de sauvegarde disponible pour la prescience", stateCOF);
return;
}
var testPrescience =
testLimiteUtilisationsCapa(ensorceleur, 'prescience', 'combat',
"ne peut plus utiliser son pouvoir de prescience pendant ce combat",
"ne peut pas faire de prescience");
if (testPrescience === undefined) {
return;
}
//On commence par faire les undo
var evt = lastEvent();
if (evt === undefined) {
error("Impossible d'utiliser la prescience car l'historique est vide", cmd);
return;
}
//Au cas où, on vérifie que l'événement de début de tour est bien présent
if (!findEvent(stateCOF.prescience.evt.id)) {
error("Impossible de trouver le début du tour dans l'historique.", stateCOF.prescience);
return;
}
while (evt && evt.id != stateCOF.prescience.evt.id) {
undoEvent();
evt = lastEvent();
}
//Ensuite on remet les tokens en position
stateCOF.prescience.dernieresPositions.forEach(function(pos) {
pos.token.set('left', pos.left);
pos.token.set('top', pos.top);
});
//Et enfin, on diminue les utilisations de prescience et on diminue la mana si possible.
utiliseCapacite(ensorceleur, testPrescience, {});
//Pas d'undo possible
//on cherche si un autre personnage dispose de prescience
let allToks =
findObjs({
_type: 'graphic',
_pageid: combat.pageId,
_subtype: 'token',
});
let prescienceActif = allToks.find(function(tok) {
let ci = tok.get('represents');
if (ci === undefined) return false;
let perso = {
token: tok,
charId: ci
};
return capaciteDisponible(perso, 'prescience', 'combat');
});
if (!prescienceActif) stateCOF.prescience = undefined;
if (limiteRessources(ensorceleur, options, 'prescience', "lancer un sort de prescience", {})) return;
setTokenAttr(ensorceleur, 'prescienceUtilisee', true, {});
initiative([{
_id: ensorceleur.token.id
}], {}, true);
updateNextInit(ensorceleur);
}
//Synchronise les tokens de même nom entre les cartes
function multiCartes(msg) {
let options = parseOptions(msg);
let enlever = options && options.cmd && options.cmd.length > 1 &&
options.cmd[1] == 'false';
getSelected(msg, function(selected, playerId) {
let evt = {
type: "Synchronisation des tokens"
};
if (selected.length === 0) {
if (enlever) {
removeAllAttributes('tokensSynchronises', evt);
addEvent(evt);
return;
}
sendPlayer(msg, "Aucun token selectionné pour !cof-multi-cartes", playerId);
return;
}
addEvent(evt);
if (enlever) {
iterSelected(selected, function(perso) {
sendPlayer(msg, nomPerso(perso) + " n'est plus synchronisé", playerId);
removeTokenAttr(perso, 'tokensSynchronises', evt);
});
return;
}
let allTokens = findObjs({
_type: 'graphic',
_subtype: 'token',
layer: 'objects',
});
iterSelected(selected, function(perso) {
let name = nomPerso(perso);
let left = perso.token.get('left');
let top = perso.token.get('top');
let listTokens = [perso.token.id];
//On cherche les tokens de même nom et on les met en même position
allTokens.forEach(function(tok) {
if (tok.get('represents') != perso.charId) return;
if (tok.get('name') != name) return;
if (tok.id == perso.token.id) return;
tok.set('left', left);
tok.set('top', top);
listTokens.push(tok.id);
});
if (listTokens.length < 2) {
sendPlayer(msg, name + " n'a qu'un seul token sur toutes les cartes", playerId);
return;
}
setTokenAttr(perso, 'tokensSynchronises', listTokens.join(), evt);
});
});
}
function ombreMouvante(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
sendPlayer(msg, "Pas de token sélectionné pour !cof-ombre-mouvante", playerId);
return;
}
iterSelected(selected, function(perso) {
doOmbreMouvante(perso, playerId, options);
});
});
}
function doOmbreMouvante(voleur, playerId, options) {
const evt = {
type: "ombre_mouvante",
action: {
perso: voleur,
playerId,
options,
}
};
addEvent(evt);
if (limiteRessources(voleur, options, 'ombreMouvante', 'disparaître dans les ombres', evt)) return;
let optionsDisplay = {
secret: options.secret
};
let display = startFramedDisplay(playerId, 'Ombre mouvante', voleur, optionsDisplay);
let difficulte = predicateAsInt(voleur, 'difficulteOmbreMouvante', 10);
testCaracteristique(voleur, 'DEX', difficulte, 'ombreMouvante', options, evt,
function(tr, explications) {
let msgRes = "Résultat : " + tr.texte;
if (tr.reussite) {
msgRes += ", " + nomPerso(voleur) + " disparaît dans les ombres";
addLineToFramedDisplay(display, msgRes + tr.modifiers);
let ef = {
effet: 'invisibleTemp',
duree: true,
pasDeMessageDActivation: true
};
setEffetTemporaire(voleur, ef, 1, evt, {});
effetsSpeciaux(voleur, undefined, options);
} else {
msgRes += ", " + nomPerso(voleur) + " ne réussit pas à se fondre dans les ombres.";
addLineToFramedDisplay(display, msgRes + tr.rerolls + tr.modifiers);
}
explications.forEach(function(m) {
addLineToFramedDisplay(display, m, 80);
});
if (options.messages) {
options.messages.forEach(function(m) {
addLineToFramedDisplay(display, m);
});
}
if (display.retarde) {
addFramedHeader(display, playerId, true);
sendFramedDisplay(display);
addFramedHeader(display, undefined, 'gm');
sendFramedDisplay(display);
} else sendFramedDisplay(display);
});
}
const attributesWithTokNames = new RegExp('^enveloppe($|_)|^enveloppePar($|_)|^agrippe($|_)|^agrippePar($|_)|^devore($|_)|^devorePar($|_)||^ecrase($|_)|^ecrasePar($|_)|^aGobe($|_)|^estGobePar($|_)|^etreinteImmole($|_)|^etreinteImmolePar($|_)|^etreinteScorpion($|_)|^etreinteScorpionPar($|_)|^capitaine($|_)|^suit($|_)|^estSuiviPar($|_)');
//!cof-reveler-nom [nouveau nom des tokens]
function revelerNom(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Problème de parse options", msg.content);
return;
}
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
sendPlayer(msg, "Pas de token sélectionné pour !cof-reveler-nom", playerId);
return;
}
let nouveauNomToken;
if (cmd.length > 1) nouveauNomToken = cmd.slice(1).join(' ');
if (selected.length > 1 && nouveauNomToken) {
sendPlayer(msg, "Attention, on ne peut sélectionner qu'un seul token quand on précise le nouveau nom des tokens", playerId);
return;
}
const evt = {
type: "Révélation de nom",
characterNames: [],
defaultTokens: [],
attributes: []
};
addEvent(evt);
let allAttrs = findObjs({
_type: 'attribute',
});
let attrsWithTokNames = allAttrs.filter(function(attr) {
return attributesWithTokNames.test(attr.get('name'));
});
let attrsWithCharNames;
let treated = new Set(); //On ne veut pas traiter un personnage plus d'une fois.
iterSelected(selected, function(perso) {
if (treated.has(perso.charId)) return;
treated.add(perso.charId);
let character = getObj('character', perso.charId);
if (character === undefined) {
error("Personnage de " + nomPerso(perso) + " perdu", perso);
return;
}
let displayName = ficheAttribute(perso, 'displayname', '@{character_name}');
let ancienNom;
let nouveauNom;
if (displayName == '@{alias}') {
setFicheAttr(perso, 'displayname', '@{character_name}', evt, {
default: '@{character_name}'
});
ancienNom = ficheAttribute(perso, 'alias', '');
nouveauNom = character.get('name');
sendChat('', ancienNom + " était en réalité " + nouveauNom + " !");
} else {
nouveauNom = ficheAttribute(perso, 'alias', '');
ancienNom = character.get('name');
if (nouveauNom === '') {
sendPlayer(msg, ancienNom + " n'a pas d'alias, rien à révéler.");
return;
}
setFicheAttr(perso, 'alias', '', evt, {
default: ''
});
sendChar(perso.charId, "était en réalité " + nouveauNom + " !");
evt.characterNames.push({
character: character,
name: ancienNom
});
character.set('name', nouveauNom);
//On change aussi les prédicats qui stoquent le nom du personnage
if (attrsWithCharNames === undefined) {
attrsWithCharNames = allAttrs.filter(function(attr) {
return attr.get('name') == 'predicats_script';
});
}
attrsWithCharNames.forEach(function(attr) {
let predicats = attr.get('current');
let i = predicats.indexOf('PVPartagesAvec::' + ancienNom + '\n');
if (i < 0) return;
evt.attributes.push({
attribute: attr,
current: predicats
});
predicats = predicats.replace('PVPartagesAvec::' + ancienNom + '\n',
'PVPartagesAvec::' + nouveauNom + '\n',
);
attr.set('current', predicats);
});
}
if (!nouveauNomToken) nouveauNomToken = nouveauNom;
let traitementEnCours;
character.get('_defaulttoken', function(defaultToken) {
if (traitementEnCours) return;
traitementEnCours = true;
let defaultTokenName;
let defaultTokenToSet;
if (defaultToken !== '') {
defaultToken = JSON.parse(defaultToken);
evt.defaultTokens.push({
character: character,
defaultToken: {...defaultToken
}
});
defaultTokenName = defaultToken.name;
defaultToken.name = nouveauNomToken;
defaultTokenToSet = true;
}
let tokens =
findObjs({
_type: 'graphic',
_subtype: 'token',
represents: perso.charId
});
tokens.forEach(function(tok) {
let tokName = tok.get('name');
if (defaultTokenToSet) {
defaultTokenToSet = false;
setDefaultTokenFromSpec(character, defaultToken, tok);
}
let tokAttr;
if (tok.get('bar1_link') === '') {
if (defaultTokenName) {
if (tokName.startsWith(defaultTokenName)) {
let suffix = tokName.substring(defaultTokenName.length);
let localTokName = nouveauNomToken + suffix;
setToken(tok, 'name', localTokName, evt);
tokAttr = tokAttr || findObjs({
_type: 'attribute',
_characterid: perso.charId
});
let endName = "_" + tokName;
tokAttr.forEach(function(attr) {
let attrName = attr.get('name');
if (attrName.endsWith(endName)) {
evt.attributes.push({
attribute: attr,
current: attr.get('current'),
name: attrName
});
var posEnd = attrName.length - tokName.length;
attrName = attrName.substring(0, posEnd) + localTokName;
attr.set('name', attrName);
}
});
attrsWithTokNames = attrsWithTokNames.filter(function(attr) {
let sp = splitIdName(attr.get('current'));
if (sp === undefined) return false;
if (sp.id == tok.id || sp.name == tokName) {
evt.attributes.push({
attribute: attr,
current: attr.get('current'),
});
attr.set('current', sp.id + ' ' + localTokName);
return false;
} else {
return true;
}
});
} else {
sendPlayer(msg, "Pas de renommage de " + tokName, playerId);
}
} else {
sendPlayer(msg, "Pas de token par défaut pour " + tokName + ", ce n'est pas encore géré dans !cof-reveler-nom", playerId);
}
} else {
if (defaultTokenName && tokName == defaultTokenName) {
setToken(tok, 'name', nouveauNomToken, evt);
attrsWithTokNames = attrsWithTokNames.filter(function(attr) {
let sp = splitIdName(attr.get('current'));
if (sp === undefined) return false;
if (sp.id == tok.id || sp.name == tokName) {
evt.attributes.push({
attribute: attr,
current: attr.get('current'),
});
attr.set('current', sp.id + ' ' + nouveauNomToken);
return false;
} else {
return true;
}
});
} else {
sendPlayer(msg, "Pas de renommage de " + tokName, playerId);
}
}
});
});
});
});
}
function tenebresMagiques(msg) {
let cmd = msg.content.split(' ');
cmd = cmd.filter(function(c) {
return c.trim() !== '';
});
let b = true;
if (cmd.length > 1) {
switch (cmd[1]) {
case 'true':
case 'oui':
case 'noir':
b = true;
break;
case 'non':
case 'sortir':
case 'false':
case 'fin':
b = false;
break;
default:
error("Option de !cof-tenebres-magiques non reconnue", cmd);
return;
}
}
if (b) {
if (stateCOF.tenebresMagiques) {
sendPlayer('GM', "Les personnages sont déjà dans des ténèbres magiques");
return;
}
sendPlayer('GM', "Les personnages entrent dans des ténèbres magiques");
stateCOF.tenebresMagiques = {};
} else {
stateCOF.tenebresMagiques = undefined;
sendPlayer('GM', "Les personnages sortent des ténèbres magiques");
}
}
function fioleDeLumiere(msg) {
let cmd = msg.content.split(' ');
let tm = stateCOF.tenebresMagiques;
if (tm === undefined) {
sendPlayer(msg, "Pas de ténèbres magiques, pas d'effet de fiole");
return;
}
cmd = cmd.filter(function(c) {
return c.trim() !== '';
});
if (cmd.length < 2) {
error("Il faut un argument à !cof-fiole-de-lumiere", cmd);
return;
}
const distance = parseInt(cmd[1]);
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
error("Aucun personnage sélectionné", cmd);
return;
}
if (selected.length > 1) {
error("Il n'y a qu'une seule fiole de lumière", cmd);
return;
}
const evt = {
type: 'fioleDeLumiere'
};
addEvent(evt);
iterSelected(selected, function(perso) {
if (cmd[1] == 'fin' || isNaN(distance) || distance < 0) {
tm.fioleDeLumiere = undefined;
let pageId = perso.token.get('pageid');
eteindreUneLumiere(perso, pageId, undefined, 'fioleDeLumiere', evt);
} else {
tm.fioleDeLumiere = {
porteur: perso,
distance: distance
};
let dimRadius = '';
if (cmd.length > 3) {
dimRadius = parseInt(cmd[3]);
if (isNaN(dimRadius)) {
error("La distance de vue de la lumière assombrie doit être un nombre", cmd[3]);
dimRadius = '';
}
}
ajouteUneLumiere(perso, 'fioleDeLumiere', distance, dimRadius, evt);
}
});
});
}
//!cof-agripper-de-demon @{selected|token_id} @{target|token_id}
function agripperDeDemon(msg) {
let cmd = msg.content.split(' ');
cmd = cmd.filter(function(c) {
return c !== '';
});
if (cmd.length < 3) {
error("Il faut spécifier un attaquant et un défenseur pour !cof-agripper-de-demon", cmd);
return;
}
let attaquant = persoOfId(cmd[1], cmd[1]);
let defenseur = persoOfId(cmd[2], cmd[2]);
if (attaquant === undefined) {
error("Le premier argument de !cof-agripper-de-demon doit être un token valide", cmd[1]);
return;
}
if (defenseur === undefined) {
error("Le deuxième argument de !cof-agripper-de-demon doit être un token valide", cmd[2]);
return;
}
if (attributeAsBool(defenseur, 'armureDEau')) {
sendChat("L'armure d'eau empêche " + nomPerso(defenseur) + " d'être aggripé");
return;
}
const evt = {
type: "Agripper (démon)"
};
let options = {
pasDeDmg: true
};
if (cmd.length > 3) options.labelArmeAttaquant = cmd[3];
const playerId = getPlayerIdFromMsg(msg);
attaqueContactOpposee(playerId, attaquant, defenseur, evt, options,
function(res, display, explications) {
if (res.succes) {
addLineToFramedDisplay(display, nomPerso(attaquant) + " agrippe fermement " + nomPerso(defenseur));
setTokenAttr(attaquant, 'agrippe', idName(defenseur), evt);
setTokenAttr(defenseur, 'estAgrippePar', idName(attaquant), evt);
setTokenAttr(defenseur, 'agrippeParUnDemon', true, evt);
} else {
addLineToFramedDisplay(display, nomPerso(defenseur) + " échappe à la tentative de saisie.");
}
explications.forEach(function(expl) {
addLineToFramedDisplay(display, expl, 80);
});
sendFramedDisplay(display);
addEvent(evt);
});
}
function deleteAttribute(attr, evt) {
evt.deletedAttributes.push(attr);
attr.remove();
}
function changeAttributeName(attr, nom, evt) {
evt.attributes.push({
attribute: attr,
name: nom
});
attr.set('name', nom);
}
function ajouteCompetence(perso, comp, carac, val, evt) {
let prefix = 'repeating_competences_' + generateRowID() + '_comp_';
let attrSpec = {
characterid: perso.charId
};
attrSpec.name = prefix + 'nom';
attrSpec.current = comp;
let attr = createObj('attribute', attrSpec);
evt.attributes.push({
attribute: attr
});
attrSpec.name = prefix + 'bonus';
attrSpec.current = val;
let attrBonus = createObj('attribute', attrSpec);
evt.attributes.push({
attribute: attrBonus
});
attrSpec.name = prefix + 'bonusTotal';
attr = createObj('attribute', attrSpec);
evt.attributes.push({
attribute: attr
});
attrSpec.name = prefix + 'carac';
attrSpec.current = carac;
attr = createObj('attribute', attrSpec);
evt.attributes.push({
attribute: attr
});
if ((carac == 'DEX' && comp != 'crochetage' && comp != 'désamorçage') ||
(carac == 'CON' && comp == 'survie') ||
comp == 'natation' || comp == 'escalade') {
attrSpec.name = prefix + 'malus';
attrSpec.current = 'armure';
attr = createObj('attribute', attrSpec);
evt.attributes.push({
attribute: attr
});
attrBonus.setWithWorker('current', val);
attr.setWithWorker('current', 'armure');
} else if (comp == 'perception' || comp == 'vigilance') {
attrSpec.name = prefix + 'malus';
attrSpec.current = 'casque';
attr = createObj('attribute', attrSpec);
evt.attributes.push({
attribute: attr
});
attrBonus.setWithWorker('current', val);
attr.setWithWorker('current', 'casque');
}
}
const regAtkPF1 = new RegExp("^(repeating_npcatk-(melee|ranged|special)_[^_]*_)(.*)$");
const regAbilitiesPF1 = new RegExp("^(repeating_abilities_[^_]*_)(.*)$");
const regFeatsPF1 = new RegExp("^(repeating_feats_[^_]*_)(.*)$");
//Transforme les personnages des tokens de Pathfinder 1 en COF
//Suppose qu'il s'agit d'un PNJ
function translateFromPathfinder1(msg) {
let treatedChars = new Set();
const optAttr = {
charAttr: true
};
getSelected(msg, function(selected, playerId) {
let evt = {
type: 'Tranformation depuis Pathfinder 1',
deletedAttributes: [],
attributes: [],
defaultTokens: []
};
addEvent(evt);
iterSelected(selected, function(perso) {
if (treatedChars.has(perso.charId)) return;
treatedChars.add(perso.charId);
if (!charAttributeAsBool(perso, 'ac')) {
sendPlayer(msg, nomPerso(perso) + " déjà converti (pas d'attribut ac)");
return;
}
let attributes = findObjs({
_type: 'attribute',
_characterid: perso.charId,
});
setFicheAttr(perso, 'type_personnage', 'PNJ', evt);
setFicheAttr(perso, 'tab', 'carac. pnj', evt);
let setAttr = function(nom, valeur) {
setTokenAttr(perso, nom, valeur, evt, optAttr);
};
let attributsIgnores = '';
let predicats = '';
let capacites = '';
let notes = '';
let equip_div = '';
let dexterite = 10;
let mod_dex = 0;
let init = 0;
let rd = '';
let race = '';
let attaques = {};
let abilities = {};
let feats = {};
let def;
attributes.forEach(function(attr) {
let nom = attr.get('name');
switch (nom) {
case 'ac':
def = parseInt(attr.get('current'));
changeAttributeName(attr, 'pnj_def', evt);
return;
case 'class':
let c = attr.get('current');
if (c.startsWith('Female ')) {
setFicheAttr(perso, 'sexe', 'F', evt);
c = c.substring(7).trim();
attr.set('current', c);
} else if (c.startsWith('Male ')) {
setFicheAttr(perso, 'sexe', 'M', evt);
c = c.substring(5).trim();
attr.set('current', c);
}
if (c.startsWith('goblin ')) {
race = "Gobelin";
c = c.substring(7).trim();
attr.set('current', c);
} else if (c.startsWith('human')) {
race = "Humain";
c = c.substring(6).trim();
attr.set('current', c);
} else if (c.startsWith('kobold')) {
race = "Kobold";
c = c.substring(7).trim();
attr.set('current', c);
}
changeAttributeName(attr, 'profil', evt);
return;
case 'defensive_abilities':
{
let da = attr.get('current');
let nonPrisEnCompte = '';
da.split(',').forEach(function(d) {
d = d.trim();
if (d === '') return;
switch (d) {
case 'incorporeal':
predicats += 'creatureIntangible ';
return;
case 'uncanny dodge':
predicats += 'immunite_surpris ';
return;
case 'improved uncanny dodge':
predicats += 'immuniteAuxSournoises ';
return;
case 'rejuvenation':
return; //Pas d'effet en combat
case 'rock catching':
notes += d + '\n';
return;
default:
if (d.startsWith('channel resistance ')) {
let resChannel = parseInt(d.substring(19));
if (isNaN(resChannel)) {
log("Capacité défensive " + d + " non connue");
} else {
predicats += 'bonusSaveContre_positif:' + resChannel + ' ';
return;
}
} else if (d.startsWith('bravery ')) {
let courage = parseInt(d.substring(8));
if (isNaN(courage)) {
log("Capacité défensive " + d + " non connue");
} else {
predicats += 'courage:' + courage + ' ';
return;
}
} else {
log("Capacité défensive " + d + " non connue");
}
if (nonPrisEnCompte === '')
nonPrisEnCompte = 'Capacités défensives : ' + d;
else nonPrisEnCompte += ', ' + d;
}
});
if (nonPrisEnCompte !== '') notes += nonPrisEnCompte + '\n';
return;
}
case 'weaknesses':
{
let wea = attr.get('current');
let nonPrisEnCompte = '';
wea.split(',').forEach(function(w) {
w = w.trim();
if (w === '') return;
switch (w) {
case 'resurrection vulnerability':
notes += "Détruit par un sort de résurection \n";
return;
case 'vulnerable to acid':
case 'vulnerability to acid':
predicats += 'vulnerableA_acide ';
return;
case 'vulnerable to cold':
case 'vulnerability to cold':
predicats += 'vulnerableA_froid ';
return;
case 'vulnerable to disease':
case 'vulnerability to disease':
predicats += 'vulnerableA_maladie ';
return;
case 'vulnerable to electricity':
case 'vulnerability to electricity':
predicats += 'vulnerableA_electrique ';
return;
case 'vulnerable to fire':
case 'vulnerability to fire':
predicats += 'vulnerableA_feu ';
return;
case 'vulnerable to poison':
case 'vulnerability to poison':
predicats += 'vulnerableA_poison ';
return;
default:
log("Faiblesse " + w + " non connue");
if (nonPrisEnCompte === '')
nonPrisEnCompte = 'Faiblesses : ' + w;
else nonPrisEnCompte += ', ' + w;
}
});
if (nonPrisEnCompte !== '') notes += nonPrisEnCompte + '\n';
return;
}
case 'hp':
changeAttributeName(attr, 'PV', evt);
return;
case 'hp_notes':
let hpNote = attr.get('current');
if (hpNote.startsWith('fast healing ')) {
let n = parseInt(hpNote.substring(13));
if (!isNaN(n) && n > 0) {
predicats += 'vitaliteSurnaturelle:' + n + ' ';
let index = 13 + ('' + n).length;
hpNote = hpNote.substring(index);
}
}
if (hpNote === '') deleteAttribute(attr, evt);
else attributsIgnores += 'hp_notes : ' + hpNote + ' .\n';
return;
case 'immune':
let immunites = attr.get('current').split(' ');
let immunitesNonTraitees = '';
immunites.forEach(function(i) {
i = i.trim();
if (i.endsWith(',')) i = i.substring(0, i.length - 1);
if (i === '') return;
switch (i) {
case 'acid':
predicats += 'immunite_acide ';
return;
case 'cold':
predicats += 'immunite_froid ';
return;
case 'disease':
predicats += 'immunite_maladie ';
return;
case 'death':
predicats += 'immunite_mort ';
return;
case 'electricity':
predicats += 'immunite_electrique ';
return;
case 'fire':
predicats += 'immunite_feu ';
return;
case 'mind-affecting':
predicats += 'sansEsprit ';
return;
case 'paralysis':
predicats += 'immunite_paralyse ';
return;
case 'petrification':
predicats += 'immunite_petrification ';
return;
case 'poison':
predicats += 'immunite_poison ';
return;
case 'fear':
predicats += 'immunite_peur ';
return;
case 'sleep':
predicats += 'immunite_endormi ';
return;
case 'construct':
predicats += 'sansEsprit creatureArtificielle immuniteSaignement ';
return;
case 'blindness':
predicats += 'immunite_aveugle ';
return;
case 'undead':
case 'traits':
case 'effects':
case 'plants':
case 'and':
return;
default:
log("Immunité à " + i + " non traitée");
immunitesNonTraitees += i + ' ';
}
});
if (immunitesNonTraitees !== '') {
log("Immunités non traitées : " + immunitesNonTraitees);
attributsIgnores += 'immune : ' + immunitesNonTraitees + '.\n';
}
deleteAttribute(attr, evt);
return;
case 'resist':
let resistances = attr.get('current').split(', ');
let resistancesNonTraitees = '';
resistances.forEach(function(r) {
r = r.trim();
if (r === '') return;
let res = r.split(' ');
if (res.length != 2) {
resistancesNonTraitees += r + ', ';
return;
}
let resVal = parseInt(res[1]);
if (isNaN(resVal) || resVal < 1) {
resistancesNonTraitees += r + ', ';
return;
}
switch (res[0]) {
case 'acid':
if (rd === '') rd = 'acide:' + resVal;
else rd += ', acide:' + resVal;
return;
case 'cold':
if (rd === '') rd = 'froid:' + resVal;
else rd += ', froid:' + resVal;
return;
case 'disease':
if (rd === '') rd = 'maladie:' + resVal;
else rd += ', maladie:' + resVal;
return;
case 'electricity':
if (rd === '') rd = 'electrique:' + resVal;
else rd += ', electrique:' + resVal;
return;
case 'fire':
if (rd === '') rd = 'feu:' + resVal;
else rd += ', feu:' + resVal;
return;
case 'poison':
if (rd === '') rd = 'poison:' + resVal;
else rd += ', poison:' + resVal;
return;
case 'sonic':
if (rd === '') rd = 'sonique:' + resVal;
else rd += ', sonique:' + resVal;
return;
default:
log("Résistance à " + res[0] + " non traitée");
resistancesNonTraitees += r + ', ';
}
});
if (resistancesNonTraitees !== '') {
log("Resistances non traitées : " + resistancesNonTraitees);
attributsIgnores += 'resist : ' + resistancesNonTraitees + '.\n';
}
deleteAttribute(attr, evt);
return;
case 'initiative':
init = parseInt(attr.get('current'));
if (isNaN(init)) init = 0;
deleteAttribute(attr, evt);
return;
case 'languages':
capacites += 'Langues: ' + attr.get('current') + '\n';
deleteAttribute(attr, evt);
return;
case 'npc_cr':
changeAttributeName(attr, 'niveau', evt);
return;
case 'npc_alignment':
if (attr.get('current').includes('E')) predicats += 'mauvais ';
deleteAttribute(attr, evt);
return;
case 'npc_dr':
let rdn = attr.get('current');
if (rdn) {
rdn = '' + rdn;
rdn = rdn.replace('bludgeoning', 'contondant').replace('slashing', 'tranchant').replace('piercing', 'percant').replace('silver', 'argent').replace('magic', 'magique').replace('adamantine', 'adamantium').replace('cold iron', 'ferFroid').replace('/-', '');
if (rd === '') rd = rdn;
else rd += ', ' + rdn;
}
deleteAttribute(attr, evt);
return;
case 'npc_type':
let npcType = attr.get('current');
switch (npcType.split(' ')[0]) {
case 'aberration':
predicats += 'aberration ';
break;
case 'humanoid':
predicats += 'humanoide ';
break;
case 'animal':
predicats += 'animal ';
break;
case 'construct':
predicats += 'nonVivant ';
break;
case 'dragon':
predicats += 'dragon ';
break;
case 'fey':
predicats += 'fée ';
break;
case 'outsider':
predicats += 'extérieur ';
break;
case 'undead':
predicats += 'nonVivant mortVivant ';
break;
case 'vermin':
case 'Vermin':
predicats += 'insecte ';
break;
case 'plant':
predicats += 'plante vegetatif ';
break;
default:
if (npcType.includes('humanoid')) {
predicats += 'humanoide ';
break;
}
log("npc_type non reconnue : " + npcType);
if (race === '') race = npcType;
}
deleteAttribute(attr, evt);
return;
case 'size':
let taille = '';
switch (attr.get('current')) {
case 'fine':
case 'diminutive':
taille = 'minuscule';
break;
case 'tiny':
taille = 'très petit';
break;
case 'small':
taille = 'petite';
break;
case 'medium':
break;
case 'large':
taille = 'grand';
break;
case 'huge':
taille = 'énorme';
break;
case 'gargantuan':
case 'colossal':
taille = 'colossal';
break;
default:
taille = attr.get('current');
}
if (taille !== '') setAttr('taille', taille);
deleteAttribute(attr, evt);
return;
case 'tactics':
let tactics = attr.get('current');
if (tactics !== '') notes += 'Tactiques : ' + tactics + '\n';
deleteAttribute(attr, evt);
return;
case 'background':
let background = attr.get('current');
if (background !== '') notes += 'Background : ' + background + '\n';
deleteAttribute(attr, evt);
return;
case 'treasure':
case 'combat_gear':
let gear = attr.get('current');
if (gear != 'none' && gear !== '') equip_div += gear + '\n';
deleteAttribute(attr, evt);
return;
case 'charisma':
changeAttributeName(attr, 'charisme', evt);
return;
case 'charisma_mod':
changeAttributeName(attr, 'pnj_cha', evt);
return;
case 'constitution':
return;
case 'constitution_mod':
changeAttributeName(attr, 'pnj_con', evt);
return;
case 'dexterity':
dexterite = parseInt(attr.get('current'));
changeAttributeName(attr, 'dexterite', evt);
return;
case 'dexterity_mod':
mod_dex = parseInt(attr.get('current'));
changeAttributeName(attr, 'pnj_dex', evt);
return;
case 'intelligence':
return;
case 'intelligence_mod':
changeAttributeName(attr, 'pnj_int', evt);
return;
case 'strength':
changeAttributeName(attr, 'force', evt);
return;
case 'strength_mod':
changeAttributeName(attr, 'pnj_for', evt);
return;
case 'wisdom':
changeAttributeName(attr, 'sagesse', evt);
return;
case 'wisdom_mod':
changeAttributeName(attr, 'pnj_sag', evt);
return;
case 'acrobatics':
ajouteCompetence(perso, 'acrobatie', 'DEX', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'appraise':
ajouteCompetence(perso, 'estimation', 'INT', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'bluff':
ajouteCompetence(perso, 'mentir', 'CHA', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'climb':
ajouteCompetence(perso, 'escalade', 'DEX', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'craft':
ajouteCompetence(perso, 'artisanat', 'INT', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'diplomacy':
ajouteCompetence(perso, 'diplomatie', 'CHA', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'disable_device':
ajouteCompetence(perso, 'désamorçage', 'DEX', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'disguise':
ajouteCompetence(perso, 'déguisement', 'CHA', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'escape_artist':
ajouteCompetence(perso, 'évasion', 'DEX', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'fly':
ajouteCompetence(perso, 'vol', 'DEX', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'handle_animal':
ajouteCompetence(perso, 'dressage', 'CHA', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'heal':
ajouteCompetence(perso, 'soigner', 'INT', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'intimidate':
ajouteCompetence(perso, 'intimidation', 'CHA', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'knowledge_arcana':
ajouteCompetence(perso, 'connaissance (arcanes)', 'INT', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'knowledge_dungeoneering':
ajouteCompetence(perso, 'connaissance (donjons)', 'INT', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'knowledge_engineering':
ajouteCompetence(perso, 'connaissance (ingéniérie)', 'INT', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'knowledge_geography':
ajouteCompetence(perso, 'connaissance (géographie)', 'INT', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'knowledge_history':
ajouteCompetence(perso, 'connaissance (histoire)', 'INT', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'knowledge_local':
ajouteCompetence(perso, 'connaissance (local)', 'INT', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'knowledge_nature':
ajouteCompetence(perso, 'connaissance (nature)', 'INT', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'knowledge_nobility':
ajouteCompetence(perso, 'connaissance (noblesse)', 'INT', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'knowledge_planes':
ajouteCompetence(perso, 'connaissance (plans)', 'INT', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'knowledge_religion':
ajouteCompetence(perso, 'connaissance (religion)', 'INT', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'linguistics':
ajouteCompetence(perso, 'linguistique', 'INT', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'perception':
ajouteCompetence(perso, 'perception', 'SAG', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'perform':
ajouteCompetence(perso, 'arts', 'CHA', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'profession':
ajouteCompetence(perso, 'profession', 'INT', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'ride':
ajouteCompetence(perso, 'Équitation', 'DEX', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'sense_motive':
ajouteCompetence(perso, 'psychologie', 'SAG', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'sleight_of_hand':
ajouteCompetence(perso, 'pick-pocket', 'DEX', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'spellcraft':
ajouteCompetence(perso, 'sortilèges', 'INT', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'stealth':
ajouteCompetence(perso, 'discrétion', 'DEX', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'survival':
ajouteCompetence(perso, 'survie', 'SAG', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'swim':
ajouteCompetence(perso, 'natation', 'FOR', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
//Attributs de toutes facons modifiés:
case 'tab':
case 'type_personnage':
return;
case 'use_magic_device':
ajouteCompetence(perso, 'objets magiques', 'INT', attr.get('current'), evt);
deleteAttribute(attr, evt);
return;
case 'version':
evt.attributes.push({
attribute: attr,
current: versionFiche
});
attr.set('current', versionFiche);
return;
case 'whispertype':
if (attr.get('current') == '/w gm') {
changeAttributeName(attr, 'jets_caches', evt);
} else {
deleteAttribute(attr, evt);
}
return;
case 'scriptVersion':
case 'bab':
case 'environment':
case 'skills_racial_modifiers':
case 'fortitude':
case 'reflex':
case 'will': //On n'y touche pas pour l'instant. À voir plus tard
let x = attr.get('current');
if (x === undefined || x === '') deleteAttribute(attr, evt);
return;
case 'ac_ability_primary':
case 'ac_ability_maximum':
case 'ac_flatfooted':
case 'ac_notes':
case 'ac_touch':
case 'alignment':
case 'ask_modifier':
case 'ask_atk_modifier':
case 'ask_dmg_modifier':
case 'ask_whisper':
case 'ask_public_roll':
case 'ask_whisper_roll':
case 'bab_multi':
case 'caster1_dc_level_0':
case 'class_favored':
case 'class1_level':
case 'cmb_mod':
case 'cmd_mod':
case 'encumbrance':
case 'encumbrance_ability_maximum':
case 'encumbrance_drag_push':
case 'encumbrance_lift_head':
case 'encumbrance_lift_ground':
case 'encumbrance_load_light':
case 'encumbrance_load_medium':
case 'encumbrance_load_heavy':
case 'encumbrance_run_factor':
case 'encumbrance_size':
case 'fob_multi':
case 'hd':
case 'hd_roll':
case 'level':
case 'npc':
case 'npc_speed':
case 'npcdrop_name':
case 'npcdrop_category':
case 'npcdrop_data':
case 'options-flag-npc':
case 'build-flag-npc':
case 'npc_fromcompendium':
case 'armor_spell_failure':
case 'npc_icon_climate':
case 'npc_icon_terrain':
case 'npc_icon_type':
case 'npc_expansion':
case 'npc_xp':
case 'organization':
case 'senses':
case 'size_display':
case 'space':
case 'speed':
case 'spell_flag':
case 'spellabilities_flag':
case 'sq':
case 'xp':
case 'l1mancer_status': //Attributs ignorés
deleteAttribute(attr, evt);
return;
default:
if (nom.endsWith('_display') || nom.endsWith('_flag') ||
nom.endsWith('half_mod')) {
deleteAttribute(attr, evt);
return;
}
let m = regAtkPF1.exec(nom);
if (m) {
attaques[m[1]] = attaques[m[1]] || {};
attaques[m[1]][m[3]] = attr.get('current');
deleteAttribute(attr, evt);
return;
}
m = regAbilitiesPF1.exec(nom);
if (m) {
abilities[m[1]] = abilities[m[1]] || {};
abilities[m[1]][m[2]] = attr.get('current');
deleteAttribute(attr, evt);
return;
}
m = regFeatsPF1.exec(nom);
if (m) {
feats[m[1]] = feats[m[1]] || {};
feats[m[1]][m[2]] = attr.get('current');
deleteAttribute(attr, evt);
return;
}
let v = attr.get('current');
if (v && ('' + v).trim() !== '') {
attributsIgnores += nom + ' : ' + v;
let max = attr.get('max');
if (max) attributsIgnores += ' , ' + max;
attributsIgnores += ' .\n';
}
deleteAttribute(attr, evt);
}
});
let cleave = false;
for (let pref in feats) {
let feat = feats[pref];
switch (feat.name) {
case undefined:
break;
case 'Improved Initiative':
case 'Toughness':
case 'Weapon Finesse':
case 'Dodge':
case 'Combat Casting':
case 'Brew Potion':
break;
case 'Alertness':
predicats += 'vigilance:2 ';
break;
case 'Cleave':
cleave = true;
break;
case 'Defect Arrows':
predicats += 'paradeDeProjectiles ';
break;
case 'Point-Blank Shot':
predicats += 'tirPrecis:1 ';
break;
case 'Iron Will':
predicats += 'bonusSagesseMagie:2 ';
break;
default:
if (feat.name.startsWith('Weapon Focus (')) continue;
if (feat.name.startsWith('Weapon Specialization (')) continue;
notes += feat.name + ' : ';
if (feat.benefits) notes += feat.benefits + '\n';
else if (feat.description) notes += feat.description + '\n';
attributsIgnores += 'Feat ' + feat.name + ' : {';
for (let field in feat) {
if (field == 'name') continue;
attributsIgnores += ' ' + field + ' : ' + feat[field] + ',';
}
attributsIgnores += '} .\n';
}
}
let maxAttackLabel = 0;
for (let pref in attaques) {
let attaque = attaques[pref];
log(attaque);
let nomAttaque = attaque.atkname || 'Attaque';
if (nomAttaque.startsWith('favored enemy ')) {
let i = nomAttaque.indexOf('(');
let j = nomAttaque.indexOf(')');
if (i < 1 || i > j) {
notes += nomAttaque + '\n';
} else {
let ennemis = nomAttaque.substring(i + 1, j).split(' ');
let ennemiJure = '';
let pasDEnnemi = true;
for (let e of ennemis) {
e = e.trim();
if (e === '') continue;
switch (e) {
case 'goblinoids':
if (pasDEnnemi) {
ennemiJure = 'gobelin';
pasDEnnemi = false;
} else ennemiJure += ', gobelin';
break;
case 'elves':
if (pasDEnnemi) {
ennemiJure = 'elfe';
pasDEnnemi = false;
} else ennemiJure += ', elfe';
break;
case 'dragons':
if (pasDEnnemi) {
ennemiJure = 'dragon';
pasDEnnemi = false;
} else ennemiJure += ', dragon';
break;
case 'giants':
if (pasDEnnemi) {
ennemiJure = 'geant';
pasDEnnemi = false;
} else ennemiJure += ', geant';
break;
case 'humans':
if (pasDEnnemi) {
ennemiJure = 'humain';
pasDEnnemi = false;
} else ennemiJure += ', humain';
break;
case 'dwarves':
if (pasDEnnemi) {
ennemiJure = 'nain';
pasDEnnemi = false;
} else ennemiJure += ', nain';
break;
default:
if (!e.startsWith('+')) {
log("Ennemi juré non reconnu : " + e);
}
}
}
if (ennemiJure !== '')
predicats += 'ennemiJure::' + ennemiJure + '\n';
else notes += nomAttaque + '\n';
}
continue;
} else if (nomAttaque.startsWith("weapon training (")) {
continue;
}
let prefix = 'repeating_pnjatk_' + generateRowID() + '_arme';
createObj('attribute', {
_characterid: perso.charId,
name: prefix + 'nom',
current: nomAttaque
});
let spec = '';
let options = '';
let portee = 0;
for (let field in attaque) {
switch (field) {
case 'atkmod':
createObj('attribute', {
_characterid: perso.charId,
name: prefix + 'atk',
current: attaque.atkmod
});
break;
case 'atkcritrange':
createObj('attribute', {
_characterid: perso.charId,
name: prefix + 'crit',
current: attaque.atkcritrange
});
break;
case 'dmgbase':
let dm = parseDice(attaque.dmgbase, 'dégâts');
if (dm) {
if (dm.nbDe)
createObj('attribute', {
_characterid: perso.charId,
name: prefix + 'dmnbde',
current: dm.nbDe
});
if (dm.dice != 4)
createObj('attribute', {
_characterid: perso.charId,
name: prefix + 'dmde',
current: dm.dice
});
if (dm.bonus)
createObj('attribute', {
_characterid: perso.charId,
name: prefix + 'dm',
current: dm.bonus
});
}
break;
case 'dmgtype':
let types = attaque.dmgtype.split('; ');
let typeInconnu = true;
for (let t of types) {
switch (t) {
case 'piercing':
if (typeInconnu) {
createObj('attribute', {
_characterid: perso.charId,
name: prefix + 'typedegats',
current: 'percant'
});
typeInconnu = false;
}
break;
case 'bludgeoning':
if (typeInconnu) {
createObj('attribute', {
_characterid: perso.charId,
name: prefix + 'typedegats',
current: 'contondant'
});
typeInconnu = false;
}
break;
case 'slashing':
typeInconnu = false;
break;
default:
spec += t + ' ';
}
}
break;
case 'atkrange':
portee = parseInt(attaque.atkrange);
if (isNaN(portee) || portee < 0) portee = 1;
else portee = Math.floor(portee / 2);
createObj('attribute', {
_characterid: perso.charId,
name: prefix + 'portee',
current: portee
});
break;
case 'dmgcritmulti':
let cm = parseInt(attaque.dmgcritmulti);
if (!isNaN(cm) && cm > 2) {
options += '--incrCritCoef ' + (cm - 2) + ' ';
}
break;
case 'atkname': //déjà traité plus haut
case 'options-flag':
case 'dmgflag':
case 'dmg2type':
case 'atkdisplay':
case 'multipleatk':
case 'atkmod2':
break;
default:
spec += field + ' : ' + attaque[field];
}
}
if (cleave && portee === 0) options += '--target @{target|Cible 2|token_id} ';
if (spec)
createObj('attribute', {
_characterid: perso.charId,
name: prefix + 'spec',
current: spec
});
if (options)
createObj('attribute', {
_characterid: perso.charId,
name: prefix + 'options',
current: options
});
createObj('attribute', {
_characterid: perso.charId,
name: prefix + 'optflag',
current: 'on'
});
maxAttackLabel++;
createObj('attribute', {
_characterid: perso.charId,
name: prefix + 'label',
current: maxAttackLabel
});
}
for (let pref in abilities) {
let ab = abilities[pref];
if (ab.name) notes += ab.name + ' : ';
if (ab.description) notes += ab.description + '\n';
}
if (maxAttackLabel > 0) setAttr('max_attack_label', maxAttackLabel);
//Puis on met les attributs nécessaires
if (attributsIgnores !== '')
setAttr('Attributs Pathfinder', attributsIgnores);
if (predicats !== '') setAttr('predicats_script', predicats);
if (capacites !== '') setAttr('capacites_pnj', capacites);
if (notes !== '') setAttr('notes', notes);
if (equip_div !== '') setAttr('equip_div', equip_div);
if (rd !== '') setAttr('RDS', rd);
if (race !== '') setAttr('race', race);
setTokenAttr(perso, 'scriptVersion', 'true', evt, {
charAttr: true,
maxVal: stateCOF.version
});
let initiative = dexterite + init - mod_dex;
if (isNaN(initiative)) initiative = 10 + 2 * init;
setAttr('pnj_init', initiative);
// Nécessaire pour éviter que les sheetworkers re-calculent init et def
if (initiative != dexterite) setAttr('INIT_DIV', initiative - dexterite);
if (def != 10 + mod_dex) setAttr('DEFDIV', def - 10 - mod_dex);
//Finalement, on change le token par défaut
let acAttr = perso.token.get('bar2_link');
affectToken(perso.token, 'bar2_link', acAttr, evt);
perso.token.set('bar2_link', '');
let ac = perso.token.get('bar2_value');
affectToken(perso.token, 'bar2_value', ac, evt);
perso.token.set('bar2_value', '');
let character = getObj('character', perso.charId);
if (character) {
let defaultToken = JSON.parse(JSON.stringify(perso.token));
evt.defaultTokens.push({
character: character,
defaultToken: defaultToken
});
setDefaultTokenForCharacter(character, perso.token);
sendChat('COFantasy', "/w gm traduction de " + character.get('name') + " vers COF");
} else {
sendChat('COFantasy', "/w gm traduction de " + nomPerso(perso) + " vers COF, mais personnage introuvable");
}
});
});
}
function canalisationParPM(expr, deParPM, phylactere, options) {
let res = expr;
let indexPM = expr.indexOf('pm');
if (indexPM > 0) {
let nbPMs = parseInt(expr.substring(0, indexPM));
if (!isNaN(nbPMs) || nbPMs > 0) {
if (nbPMs == 1) {
options.mana = options.mana || 0;
options.mana++;
res = deParPM;
if (phylactere) res += '+ ' + phylactere;
} else {
let de = parseDice(deParPM);
if (de) {
res = '';
if (de.nbDe) {
options.mana = options.mana || 0;
options.mana += nbPMs;
res = (nbPMs * de.nbDe) + 'd' + de.dice;
if (de.bonus) res += '+ ';
}
if (de.bonus) res += (nbPMs * de.bonus);
if (phylactere) {
let dp = parseDice(phylactere);
if (dp) {
if (dp.nbDe) {
res += '+ ' + (nbPMs * dp.nbDe) + 'd' + dp.dice;
}
if (dp.bonus) res += '+ ' + (nbPMs * dp.bonus);
}
}
}
}
}
}
return res;
}
//!cof-canaliser [positif|negatif] --soin expr --dm expr
function canaliser(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Problème de parse options", msg.content);
return;
}
if (cmd.length < 2) {
error("Il manque le type de canalisation", msg.content);
return;
}
let positif = true;
if (cmd[1] === 'false' || cmd[1] === 'negatif' || cmd[1] === 'négatif')
positif = false;
if (options.soin === undefined && options.dm === undefined) {
sendPlayer(msg, "Rien à canaliser. Il faut préciser un montant de soins ou de dégâts");
return;
}
getSelected(msg, function(selected, playerId) {
if (selected.length != 1) {
sendPlayer(msg, "Il faut sélectionner un personnage", playerId);
return;
}
let token = getObj('graphic', selected[0]._id);
if (token === undefined) {
error("Token non reconnu", selected);
return;
}
let charId = token.get('represents');
if (charId === undefined || charId === '') {
error("Le token ne représente pas un personnage", token);
return;
}
let pretre = {
token: token,
charId: charId
};
let evt = {
type: 'canalisation'
};
addEvent(evt);
//Conversion des #n pm en soins et dm
let deParPM = predicateAsBool(pretre, 'deCanalisation');
if (deParPM) {
let phylactere;
if (positif) phylactere = predicateAsBool(pretre, 'phylacterePositif');
else phylactere = predicateAsBool(pretre, 'phylactereNegatif');
if (options.soin) {
options.soin = canalisationParPM(options.soin, deParPM, phylactere, options);
}
if (options.dm) {
options.dm = canalisationParPM(options.dm, deParPM, phylactere, options);
}
}
if (limiteRessources(pretre, options, 'canalisation', 'canalisation', evt)) {
return;
}
let display = startFramedDisplay(playerId, "Canalisation", pretre);
let pageId = token.get('pageid');
let page = getObj('page', pageId);
let murs = getWalls(page, pageId);
let pc;
if (murs) {
pc = {
x: token.get('left'),
y: token.get('top')
};
}
let allToks =
findObjs({
_type: "graphic",
_pageid: pageId,
_subtype: 'token',
layer: 'objects'
});
let cibles = [];
allToks.forEach(function(obj) {
let objCharId = obj.get('represents');
if (objCharId === '') return;
if (obj.get('bar1_max') == 0) return; // jshint ignore:line
let cible = {
token: obj,
charId: objCharId
};
if (getState(cible, 'mort')) return; //pas d'effet aux morts
let objChar = getObj('character', objCharId);
if (objChar === undefined) return;
let distance = distanceCombat(token, obj, pageId, {
strict1: true
});
if (distance > 10) return;
if (murs) {
if (obstaclePresent(obj.get('left'), obj.get('top'), pc, murs)) return;
}
cibles.push(cible);
});
cibles = enleveDoublonsPartagePV(cibles);
let nbCibles = cibles.length;
if (options.soin && options.dm) nbCibles += nbCibles;
let ciblesAttaquees = [];
let sync = function() {
nbCibles--;
if (nbCibles < 1) {
if (ciblesAttaquees.length > 0) {
let explications = [];
entrerEnCombat(pretre, ciblesAttaquees, explications, evt);
explications.forEach(function(e) {
addLineToFramedDisplay(display, e);
});
}
sendFramedDisplay(display);
}
};
if (options.soin) {
let soins = '[[' + options.soin + ']]';
cibles.forEach(function(target) {
if (estMortVivant(target) == positif) {
sync();
return;
}
try {
sendChat('', soins, function(res) {
let soins = res[0].inlinerolls[0].results.total;
let soinTxt = buildinline(res[0].inlinerolls[0], 'normal', true);
if (soins <= 0) {
sendPerso(pretre, "ne réussit pas à soigner (total de soins " + soinTxt + ")", true);
sync();
return;
}
//TODO: tenir compte des PV partagés
soigneToken(target, soins, evt,
function(soinsEffectifs) {
let line = "" + nomPerso(target) + " : + ";
if (soinsEffectifs == soins) {
line += soinTxt + 'PV';
} else {
line += soinsEffectifs + ' PV (jet: ' + soinTxt + ')';
}
addLineToFramedDisplay(display, line);
sync();
},
function() {
addLineToFramedDisplay(display, "" + nomPerso(target) + " : pas besoin de soins.");
sync();
}, options);
});
} catch (rollError) {
error("Jet " + options.soin + " mal formé", options);
}
});
}
if (options.dm) {
parseDmgOptions(msg.content, options);
let dm = '[[' + options.dm + ']]';
if (options.maxDmg) {
dm = dm.replace(/d([1-9])/g, "*$1");
}
let dmgType = options.type || 'magique';
options.energiePositive = positif;
cibles.forEach(function(target) {
if (target.token.id == pretre.token.id ||
estMortVivant(target) != positif) {
sync();
return;
}
ciblesAttaquees.push(target);
try {
sendChat('', dm, function(res) {
let dmg = {
type: dmgType,
value: options.dm,
roll: res[0],
};
let afterEvaluateDmg = dmg.roll.content.split(' ');
let dmgRollNumber = rollNumber(afterEvaluateDmg[0]);
dmg.total = dmg.roll.inlinerolls[dmgRollNumber].results.total;
dmg.display = buildinline(dmg.roll.inlinerolls[dmgRollNumber], dmg.type, options.magique);
let explications = [];
copyDmgOptionsToTarget(target, options);
dealDamage(target, dmg, [], evt, false, options, explications, function(dmgDisplay, dmgFinal) {
addLineToFramedDisplay(display,
nomPerso(target) + " reçoit " + dmgDisplay + " DM");
explications.forEach(function(e) {
addLineToFramedDisplay(display, e, 80, false);
});
sync();
});
});
} catch (rollError) {
error("Jet " + options.dm + " mal formé", dm);
}
});
}
});
}
const objetsAnimes = {
1: {
token: "https://s3.amazonaws.com/files.d20.io/images/250177368/mJsYWMFqDeEmJDJy8tJKWA/thumb.png?1634130296",
avatar: "https://s3.amazonaws.com/files.d20.io/images/250177753/IMzDqEpNpuznVnAMYRil8A/max.jpg?1634130507",
taille: 'très petite',
force: 4,
pnj_for: -3,
constitution: 4,
pnj_con: -3,
pnj_def: 10,
pv: 4,
dmnbde: 0,
dmde: 4,
dm: 1,
},
2: {
token: "https://s3.amazonaws.com/files.d20.io/images/250179877/-j9v1JoPrX7StcH0jGTtMQ/thumb.png?1634132408",
avatar: "https://s3.amazonaws.com/files.d20.io/images/250179870/ana7iPnNx6lgPLRtSmBzXA/max.jpg?1634132399",
taille: 'petite',
force: 6,
pnj_for: -2,
constitution: 6,
pnj_con: -2,
pnj_def: 10,
pv: 8,
dmnbde: 1,
dmde: 4,
dm: 0,
},
4: {
token: "https://s3.amazonaws.com/files.d20.io/images/250181751/L0JdDzCXjJnOlbUPJufU7A/thumb.png?1634133729",
avatar: "https://s3.amazonaws.com/files.d20.io/images/250181765/aELyP7xCTkSddnD6-8Kllw/max.jpg?1634133738",
taille: 'moyenne',
force: 10,
pnj_for: 0,
constitution: 10,
pnj_con: 0,
pnj_def: 12,
pv: 15,
dmnbde: 1,
dmde: 6,
dm: 0,
},
7: {
token: "https://s3.amazonaws.com/files.d20.io/images/250303501/Kj38iSV6T0BIZMhGm74xqA/thumb.png?1634197833",
avatar: "https://s3.amazonaws.com/files.d20.io/images/250303491/pzSiwMs-tZqTiluuFIz8tA/max.jpg?1634197823",
taille: 'moyenne',
force: 16,
pnj_for: 3,
constitution: 16,
pnj_con: 3,
pnj_def: 14,
pv: 30,
dmnbde: 1,
dmde: 6,
dm: 3,
},
10: {
token: "https://s3.amazonaws.com/files.d20.io/images/250303495/k0LqXjurwtySR-aZQjsI1Q/thumb.png?1634197828",
avatar: "https://s3.amazonaws.com/files.d20.io/images/250303482/COH6jsSUovSjVQfvbjU51Q/max.jpg?1634197812",
taille: 'grande',
force: 22,
pnj_for: 6,
constitution: 22,
pnj_con: 6,
pnj_def: 16,
pv: 50,
dmnbde: 2,
dmde: 6,
dm: 6,
}
};
//!cof-animation-des-objets lid niveau [tid]
// la cible optionelle correspond à un token existant non associé à un personnage
function animationDesObjets(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Problème de parse options", msg.content);
return;
}
if (cmd.length < 3) {
error("Usage : !cof-animation-des-objets @{selected|token_id} niveau", msg.content);
return;
}
let lanceur = persoOfId(cmd[1], cmd[1], options.pageId);
if (lanceur === undefined) {
error("Token incorrect", cmd);
return;
}
let niveauObjet = parseInt(cmd[2]);
if (isNaN(niveauObjet) || niveauObjet < 1) {
error("Le niveau de l'objet animé doit être un nombre positif", cmd);
return;
}
if (niveauObjet > 10) niveauObjet = 10;
let stats = objetsAnimes[niveauObjet];
while (stats === undefined) {
niveauObjet--;
stats = objetsAnimes[niveauObjet];
}
let niveau = ficheAttributeAsInt(lanceur, 'niveau', 1);
let dejaAnime = attributeAsInt(lanceur, 'niveauDesObjetsAnimes', 0);
let playerId = getPlayerIdFromMsg(msg);
if (dejaAnime + niveauObjet > niveau) {
sendPlayer(msg, "Impossible d'animer plus d'objets pour le moment : somme des niveau animés = " + dejaAnime, playerId);
return;
}
let evt = {
type: "Animation d'objet",
};
addEvent(evt);
if (limiteRessources(lanceur, options, 'animationObjet', "animation d'un objet", evt)) return;
let tokenSize = 70;
switch (stats.taille) {
case 'très petite':
tokenSize = 35;
break;
case 'petite':
tokenSize = 50;
break;
case 'grande':
tokenSize = 105;
break;
}
let tokenObjet;
let aPartirDExistant;
if (cmd.length > 3) {
tokenObjet = getObj('graphic', cmd[3]);
aPartirDExistant = (tokenObjet !== undefined);
}
let pageId = lanceur.token.get('pageid');
tokenObjet = tokenObjet ||
createObj('graphic', {
name: 'Objet animé',
subtype: 'token',
pageid: pageId,
imgsrc: stats.token,
left: lanceur.token.get('left'),
top: lanceur.token.get('top'),
width: tokenSize,
height: tokenSize,
layer: 'objects',
showname: 'true',
showplayers_bar1: 'true',
light_hassight: 'true',
light_losangle: 0,
has_bright_light_vision: true,
has_limit_field_of_vision: true,
limit_field_of_vision_total: 0,
});
if (tokenObjet === undefined) {
error("Impossible de créer le token", stats);
return;
}
toFront(tokenObjet);
let persoObjet = {
nom: 'Objet animé par ' + nomPerso(lanceur),
attributesFiche: {
type_personnage: 'PNJ',
niveau: niveauObjet,
force: stats.force,
pnj_for: stats.pnj_for,
dexterite: 10,
pnj_dex: 0,
constitution: stats.constitution,
pnj_con: stats.pnj_con,
intelligence: 2,
pnj_int: -4,
sagesse: 2,
pnj_sag: -4,
charisme: 2,
pnj_cha: -4,
pnj_def: stats.pnj_def,
pnj_init: 10
},
pv: stats.pv,
attaques: [{
nom: 'Frapper',
atk: computeArmeAtk(lanceur, '@{ATKMAG}'),
typedegats: 'contondant',
dmnbde: stats.dmnbde,
dmde: stats.dmde,
dm: stats.dm
}],
attributes: [{
name: 'objetAnime',
current: 5 + modCarac(lanceur, 'intelligence'),
max: getInit(),
lie: options.mana !== undefined
}, {
name: 'objetAnimePar',
current: idName(lanceur)
}, {
name: 'predicats_script',
current: 'nonVivant'
}]
};
let charObjet =
createCharacter(persoObjet.nom, playerId, stats.avatar, tokenObjet, persoObjet, evt, lanceur);
evt.tokens = [tokenObjet];
evt.characters = [charObjet];
if (aPartirDExistant)
setPredicate({
token: tokenObjet,
charId: charObjet.id
}, 'animeAPartirDExistant', evt);
initiative([{
_id: lanceur.token.id,
}, {
_id: tokenObjet.id
}], evt);
setTokenAttr(lanceur, 'niveauDesObjetsAnimes', dejaAnime + niveauObjet, evt);
let allies = alliesParPerso[lanceur.charId] || new Set();
allies.add(charObjet.charId);
alliesParPerso[lanceur.charId] = allies;
}
//!cof-soigner-affaiblissement carac valeur
function soignerAffaiblissement(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Problème de parse options", msg.content);
return;
}
if (cmd.length < 3) {
error("Usage : !cof-soigner-affaiblissement carac valeur", msg.content);
return;
}
let carac;
switch (cmd[1]) {
case 'FOR':
case 'force':
carac = 'force';
break;
case 'DEX':
case 'dexterite':
case 'dexterité':
case 'dextérité':
case 'dextérite':
carac = 'dexterite';
break;
case 'CON':
case 'constution':
carac = 'constitution';
break;
case 'INT':
case 'intelligence':
carac = 'intelligence';
break;
case 'SAG':
case 'sagesse':
carac = 'sagess';
break;
case 'CHA':
case 'charisme':
carac = 'charisme';
break;
default:
error("Caractéristique " + carac + " non reconnue", cmd);
return;
}
let valeur = parseInt(cmd[2]);
if (isNaN(valeur) || valeur < 1) {
error("La valeur de soin d'affaiblissement doit être un nombre positif", cmd);
return;
}
let lanceur = options.lanceur;
getSelected(msg, function(selected, playerId) {
if (selected === undefined || selected.length === 0) {
sendPlayer(msg, "Pas de cible sélectionée pour le soin d'affaiblissement", playerId);
return;
}
if (lanceur === undefined) {
if (options.portee) {
error("Impossible de savoir l'origine du soin", options);
return;
}
if (selected.length == 1) {
lanceur = persoOfId(selected[0]._id);
if (lanceur) {
options.lanceur = lanceur;
}
}
}
let evt = {
type: "Soin d'affaiblissement de " + carac
};
addEvent(evt);
if (limiteRessources(lanceur, options, carac, carac, evt)) return;
iterSelected(selected, function(perso) {
if (options.portee !== undefined) {
if (options.puissantPortee) options.portee = options.portee * 2;
let dist = distanceCombat(lanceur.token, perso.token);
if (dist > options.portee) {
sendPerso(lanceur, " est trop loin de " + nomPerso(perso));
return;
}
}
let malus = attributeAsInt(perso, 'affaiblissementde' + carac, 0);
if (malus === 0) {
sendPerso(perso, "n'a pas d'affaiblissement " + deCarac(carac));
return;
}
diminueAffaiblissement(perso, carac, valeur, evt, malus);
});
});
}
//!cof-affaiblir-carac carac valeur
function parseAffaiblirCarac(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Problème de parse options", msg.content);
return;
}
if (cmd.length < 3) {
error("Usage : !cof-affaiblir-carac carac valeur", msg.content);
return;
}
let carac;
switch (cmd[1]) {
case 'FOR':
case 'force':
carac = 'force';
break;
case 'DEX':
case 'dexterite':
case 'dexterité':
case 'dextérité':
case 'dextérite':
carac = 'dexterite';
break;
case 'CON':
case 'constution':
carac = 'constitution';
break;
case 'INT':
case 'intelligence':
carac = 'intelligence';
break;
case 'SAG':
case 'sagesse':
carac = 'sagess';
break;
case 'CHA':
case 'charisme':
carac = 'charisme';
break;
case 'RAND':
case 'rand':
case 'random':
carac = 'random';
break;
default:
error("Caractéristique " + carac + " non reconnue", cmd);
return;
}
let valeur = parseDice(cmd[2]);
if (!valeur || !dePositif(valeur)) {
error("La valeur d'affaiblissement doit être un nombre positif", cmd);
return;
}
let lanceur = options.lanceur;
getSelected(msg, function(selected, playerId) {
if (selected === undefined || selected.length === 0) {
sendPlayer(msg, "Pas de cible sélectionée pour l'affaiblissement", playerId);
return;
}
if (lanceur === undefined) {
if (options.portee) {
error("Impossible de savoir l'origine de l'affaiblissement", options);
return;
}
if (selected.length == 1) {
lanceur = persoOfId(selected[0]._id);
if (lanceur) {
options.lanceur = lanceur;
}
}
}
let cibles = [];
iterSelected(selected, function(perso) {
if (options.portee !== undefined) {
if (options.puissantPortee) options.portee = options.portee * 2;
let dist = distanceCombat(lanceur.token, perso.token);
if (dist > options.portee) {
sendPerso(lanceur, " est trop loin de " + nomPerso(perso));
return;
}
}
if (options.type && immuniseAuType(perso, options.type, lanceur, options)) {
sendPerso(perso, "ne semble pas affecté par " + stringOfType(options.type));
return;
}
cibles.push(perso);
});
if (cibles.length == 0) {
return;
}
affaiblirCarac(playerId, cibles, carac, valeur, options);
}, options);
}
function randomCarac() {
switch (randomInteger(6)) {
case 1:
return 'force';
case 2:
return 'dexterite';
case 3:
return 'constitution';
case 4:
return 'intelligence';
case 5:
return 'sagesse';
case 6:
return 'charisme';
}
error("Erreur interne dans randomCarac");
}
function randomCaracForId(id, options) {
if (options.randomCaracs) {
if (options.randomCaracs[id]) return options.randomCaracs[id];
} else {
options.randomCaracs = {};
}
let carac = randomCarac();
options.randomCaracs[id] = carac;
return carac;
}
//carac est une caractéristique entière
//valeur est soit un nombre, soit le résultat de parseDice
function affaiblirCaracPerso(perso, carac, valeur, expliquer, evt) {
let nomAttr = 'affaiblissementde' + carac;
let valeurText = valeur;
if (isNaN(valeur)) {
let rid = valeur.id;
if (rid === undefined) {
error("Résultat de parseDice sans id", valeur);
return;
}
if (perso.affaiblirCaracRoll && perso.affaiblirCaracRoll[rid]) {
let r = perso.affaiblirCaracRoll[rid];
valeur = r.val;
valeurText = r.roll;
} else {
let r = rollDePlus(valeur);
valeur = r.val;
valeurText = r.roll;
perso.affaiblirCaracRoll = perso.affaiblirCaracRoll || {};
perso.affaiblirCaracRoll[rid] = r;
}
}
let malus = addToAttributeAsInt(perso, nomAttr, 0, valeur, evt);
let cn = caracNormale(perso, carac);
if (malus > cn) {
valeur += cn - malus;
valeurText = valeur;
setTokenAttr(perso, nomAttr, cn, evt);
}
if (valeur < 1) return;
expliquer("perd " + valeurText + " points de " + carac);
if (carac == 'constitution') {
if (malus >= cn) {
mort(perso, expliquer, evt);
return;
}
let perteMod = Math.floor(valeur / 2);
if (valeur % 2 == 1) {
if ((cn - malus) % 2 == 1) perteMod++;
}
if (perteMod > 0) {
let niveau = ficheAttributeAsInt(perso, 'niveau', 1);
let pvPerdus = niveau * perteMod;
let bar1 = parseInt(perso.token.get("bar1_value"));
let pvmax = parseInt(perso.token.get("bar1_max"));
if (isNaN(bar1) || isNaN(pvmax)) {
error("Affaiblissement de constitution sur un token sans points de vie", perso);
return;
}
let attrpvMaxNormaux = tokenAttribute(perso, 'pvMaxNormaux');
if (attrpvMaxNormaux.length === 0) {
setTokenAttr(perso, 'pvMaxNormaux', pvmax, evt);
}
pvmax -= pvPerdus;
if (pvmax < 1) {
pvPerdus += pvmax - 1;
pvmax = 1;
}
bar1 -= pvPerdus;
if (bar1 < 0) bar1 = 0;
updateCurrentBar(perso, 1, bar1, evt, pvmax);
if (bar1 === 0) mort(perso, expliquer, evt);
}
} else { //autre caractéristiques
if (malus >= cn) {
setState(perso, 'renverse', true, evt);
setState(perso, 'assomme', true, evt);
}
}
}
// valeur peut être un nombre ou le résultat de parseDice
function affaiblirCarac(playerId, cibles, carac, valeur, options) {
const evt = {
type: 'affaiblissement',
action: {
titre: "Affaiblissement de " + carac,
playerId,
cibles,
carac,
valeur,
options,
}
};
let lanceur = options.lanceur;
let explications = options.messages || [];
let whisper = '';
if (options.secret) {
let player;
if (playerId) player = getObj('player', playerId);
if (player !== undefined) {
whisper = '/w "' + player.get('displayname') + '" ';
}
}
addEvent(evt);
if (limiteRessources(lanceur, options, carac, carac, evt)) return;
explications.forEach(function(e) {
sendChat('', e);
});
cibles.forEach(function(perso) {
let car = carac;
if (carac == 'random') car = randomCaracForId(perso.token.id, options);
let expliquer = function(s) {
sendPerso(perso, s);
};
if (options.save) {
let saveOpts = {
msgPour: " pour résister à un affaiblissement de " + car,
msgRate: ", raté.",
attaquant: lanceur,
rolls: options.rolls,
chanceRollId: options.chanceRollId,
type: options.type
};
let saveId = 'affaiblissement' + car + "_" + perso.token.id;
save(options.save, perso, saveId, expliquer, saveOpts, evt,
function(reussite, rollText) {
if (!reussite) {
affaiblirCaracPerso(perso, car, valeur, expliquer, evt);
}
});
} else {
affaiblirCaracPerso(perso, car, valeur, expliquer, evt);
}
});
}
// !cof-fin-reaction-violente token_id
function finReactionViolente(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Problème de parse options", msg.content);
return;
}
if (cmd.length < 2) {
error("Usage : !cof-fin-reaction-violente token_id", msg.content);
return;
}
let perso = persoOfId(cmd[1]);
if (perso === undefined) {
error("Le premier argument de !cof-fin-reatcion-violente n'est pas un token valide", cmd);
return;
}
let playerId = getPlayerIdFromMsg(msg);
let attr = tokenAttribute(perso, 'reactionViolente');
if (attr.length === 0) {
sendPlayer(msg, "Action inutile, " + perso.token.ge('name') + " n'est pas sous l'effet d'une réaction violente", playerId);
return;
}
attr = attr[0];
let duree = parseInt(attr.get('current'));
if (isNaN(duree) || duree < 1) duree = 1;
let evt = {
type: "Fin de réaction violente",
deletedAttributes: [attr]
};
addEvent(evt);
attr.remove();
sendPerso(perso, "prend sur " + onGenre(perso, 'lui', 'elle') + " pour contenir sa réaction violente");
options.ignoreRD = true;
let de = 6;
if (predicateAsBool(perso, 'sangFroid')) de = 4;
let degats = rollDePlus(de, {
nbDes: duree
});
let dmg = {
type: 'normal',
total: degats.val,
display: degats.roll
};
dealDamage(perso, dmg, [], evt, false, options, undefined,
function(dmgDisplay, dmgFinal) {
sendPerso(perso, "s'inflige " + dmgDisplay + " DM");
});
}
//!cof-explosion correspond à !cof-attack token token --explosion pour chaque token sélectionné
function attaqueExplosion(msg) {
if (!msg.content) return;
let index = msg.content.indexOf(' ');
if (index < 1) {
error("Il manque le label de l'attaque à utiliser pour !cof-explosion", msg.content);
return;
}
let args_msg = msg.content.substring(index);
//On va ensuite enlever tout ce qui vient après --target
index = args_msg.indexOf(' --target ');
if (index > 0) args_msg = args_msg.substring(0, index);
args_msg += ' --explosion';
getSelected(msg, function(selected, playerId) {
iterSelected(selected, function(perso) {
let id = perso.token.id;
let msga = {...msg,
content: '!cof-attack ' + id + ' ' + id + args_msg
};
parseAttack(msga);
});
}, {
ignoreAllies: true,
ignoreDisque: true
}); //On ignore les options d'alliés dans le getSelected
}
const listeEffetsAuD20 = {
foudresDuTemps: {
min: 13,
max: 13,
fct: foudreDuTemps,
nomFin: "aux foudres du temps ",
nomActivation: "Les foudres du temps sont actives",
messageFin: "Les personnages ne sont plus sous l'effet des foudres du temps",
},
tremblementsDeTerre: {
min: 13,
max: 13,
fct: tremblementDeTerre,
nomFin: "aux tremblements de terre",
nomActivation: "Les tremblements de terre sont actifs",
messageFin: "Les personnages sortent de la zone des tremblements de terre",
}
};
// !cof-effet-chaque-d20 effet (fin|une valeur min [ une valeur max])
function setEffetChaqueD20(msg) {
let playerId = getPlayerIdFromMsg(msg);
if (!playerIsGM(playerId)) {
sendPlayer(msg, "Commande réservée au MJ", playerId);
return;
}
let cmd = msg.content.split(' ');
cmd = cmd.filter(function(c) {
return c.trim() !== '';
});
if (cmd.length < 2) {
error("Il manque l'effet ", cmd);
return;
}
let typeEffet = cmd[1];
let evDefaut = listeEffetsAuD20[typeEffet];
if (!evDefaut) {
error("Effet au d20 " + typeEffet + " non connu.", cmd);
return;
}
let min = evDefaut.min;
let max = evDefaut.max;
let evenements = stateCOF.effetAuD20;
if (evenements && evenements[typeEffet]) {
min = evenements[typeEffet].min;
max = evenements[typeEffet].max;
}
if (cmd.length > 2) {
switch (cmd[2]) {
case 'true':
case 'oui':
break;
case 'non':
case 'sortir':
case 'false':
case 'fin':
max = 1;
min = 20;
break;
default:
min = parseInt(cmd[2]);
if (isNaN(min)) {
error("Le premier argument de !cof-effet-chaque-d20 est invalide", cmd);
return;
}
if (cmd.length > 3) {
max = parseInt(cmd[3]);
if (isNaN(max)) {
error("Le second argument de !cof-effet-chaque-d20 est invalide", cmd);
max = min;
}
}
}
}
if (min < 1) min = 1;
if (max > 20) max = 20;
if (max >= min) {
if (!evenements) {
evenements = {};
stateCOF.effetAuD20 = evenements;
}
let ev = evenements[typeEffet];
if (!ev) {
ev = {...evDefaut
};
evenements[typeEffet] = ev;
}
ev.min = min;
ev.max = max;
let message = ev.nomActivation + " pour des jets ";
if (min == max) message += "de " + min;
else message += "entre " + min + " et " + max;
sendPlayer('GM', message);
} else {
if (!evenements || !evenements[typeEffet]) {
sendPlayer('GM', "L'effet " + typeEffet + " n'est pas actif");
return;
}
sendPlayer('GM', evenements[typeEffet].messageFin);
delete stateCOF.effetAuD20[typeEffet];
if (_.isEmpty(stateCOF.effetAuD20)) delete stateCOF.effetAuD20;
}
}
function agrandirPage(msg) {
let cmd = msg.content.split(' ');
if (cmd.length < 1) {
error("Il manque le facteur d'agrandissement", cmd);
return;
}
let facteur = parseFloat(cmd[1]);
if (isNaN(facteur) || facteur <= 0) {
error("Facteur incorrect", cmd);
return;
}
let playerId = getPlayerIdFromMsg(msg);
if (!playerIsGM(playerId)) {
sendPlayer(msg, "Commande réservée aux MJs", playerId);
return;
}
let pageId = getPageId(playerId);
if (!pageId) {
error("Impossible de trouver la page", playerId);
return;
}
let page = getObj('page', pageId);
if (page === undefined) {
error("Impossible de trouver la page correspondant à l'id", pageId);
return;
}
let agrandir = function(o, field) {
o.set(field, o.get(field) * facteur);
};
let move = function(o) {
agrandir(o, 'top');
agrandir(o, 'left');
};
let scale = function(o) {
agrandir(o, 'width');
agrandir(o, 'height');
};
scale(page);
let objects = findObjs({
_type: 'graphic',
_pageid: pageId
});
objects.forEach(function(o) {
move(o);
if (o.get('layer') == 'map') scale(o);
});
let paths = findObjs({
_type: 'path',
_pageid: pageId
});
paths.forEach(function(p) {
move(p);
agrandir(p, 'scaleX');
agrandir(p, 'scaleY');
});
let texts = findObjs({
_type: 'text',
_pageid: pageId
});
texts.forEach(function(t) {
move(t);
});
sendPlayer('GM', "Agrandissement terminé");
}
function decoincer(msg) {
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
sendPlayer(msg, "Pas de token sélectionné", playerId);
return;
}
if (!playerIsGM(playerId)) {
sendPlayer(msg, "Action réservée au MJ", playerId);
return;
}
const evt = {
type: "Décoincer",
tokens: []
};
let pageId;
iterSelected(selected, function(perso) {
if (attributeAsBool(perso, 'decoince')) {
sendPlayer(msg, nomPerso(perso) + " peut déjà être déplacé", playerId);
return;
}
if (!perso.token.get('lockMovement')) {
sendPlayer(msg, nomPerso(perso) + "n'est pas bloqué", playerId);
return;
}
let nom = 'decoince ' + perso.token.get('name');
pageId = pageId || perso.token.get('pageid');
let tokenBougeAttr = tokenAttribute(perso, 'bougeGraceA');
if (tokenBougeAttr.length > 0) {
tokenBougeAttr = tokenBougeAttr[0];
let tokenBouge = getObj('graphic', tokenBougeAttr.get('current'));
if (tokenBouge === undefined) {
//On cherche un token de nom decoince + nom du perso
tokenBouge = findObjs({
_type: 'graphic',
_pageid: pageId,
represents: perso.charId,
name: nom
});
if (tokenBouge.length > 0) {
tokenBouge = tokenBouge[0];
tokenBougeAttr.set('current', tokenBouge.id);
} else {
tokenBougeAttr.remove();
}
}
if (tokenBouge) {
if (tokenBouge.get('pageid') == pageId) {
toFront(tokenBouge);
return;
}
tokenBouge.remove();
tokenBougeAttr.remove();
}
}
let tokenFields = {
_pageid: pageId,
represents: perso.charId,
layer: perso.token.get('layer'),
left: perso.token.get('left'),
top: perso.token.get('top'),
rotation: perso.token.get('rotation'),
width: perso.token.get('width'),
height: perso.token.get('height'),
name: nom,
aura1_radius: 0,
aura1_color: "#EE9911",
showplayers_aura1: false,
showplayers_name: false,
showplayers_bar1: false,
showplayers_bar2: false,
showplayers_bar3: false,
imgsrc: IMG_INVISIBLE,
};
let tokenBouge = createObj('graphic', tokenFields);
if (!tokenBouge) {
error("Impossible de créer de token pour décoincer " + nomPerso(perso), tokenFields);
return;
}
evt.tokens.push(tokenBouge);
toFront(tokenBouge);
setTokenAttr(perso, 'bougeGraceA', tokenBouge.id, evt);
});
if (pageId) sendPlayer(msg, "Penser à supprimer le token invisible quand vous aurez terminé le déplacement", playerId);
});
}
function pauseGame() {
if (stateCOF.pause) stateCOF.pause = false;
else stateCOF.pause = true;
let tokens = findObjs({
_type: 'graphic',
_subtype: 'token',
layer: 'objects'
});
let charTreated = new Set();
let charTreatedBlocked = new Set();
tokens.forEach(function(token) {
let charId = token.get('represents');
if (charId === '') return;
let character = getObj('character', charId);
if (character === undefined) return;
let charControlledby = character.get('controlledby');
if (charControlledby === '') return;
let controlledByPlayer = charControlledby.split(',').some(function(pid) {
return pid == 'all' || !playerIsGM(pid);
});
if (!controlledByPlayer) return;
if (stateCOF.pause) token.set('lockMovement', true);
else {
let linked = token.get('bar1_link') !== '';
if (linked && charTreated.has(charId)) {
if (!charTreatedBlocked.has(charId)) token.set('lockMovement', false);
} else {
const perso = {
token,
charId
};
if (linked) {
charTreated.add(charId);
if (persoImmobilise(perso)) {
charTreatedBlocked.add(charId);
return;
}
} else if (persoImmobilise(perso)) return;
token.set('lockMovement', false);
enleveDecoince(perso);
}
}
});
let macros = findObjs({
_type: 'macro'
});
let macro = macros.find(function(m) {
let action = m.get('action');
return action == '!cof-pause';
});
if (stateCOF.pause) {
if (macro) macro.set('name', PLAY);
sendChat('COF', "Jeu en pause");
} else {
if (macro) macro.set('name', PAUSE);
sendChat('COF', "Fin de la pause");
}
}
//!cof-sentir-la-corruption @{selected|token_id} @{target|token_id}
function parseSentirLaCorruption(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Problème de parse options", msg.content);
return;
}
if (cmd.length < 3) {
error("Usage : !cof-sentir-la-corruption token_id token_id", msg.content);
return;
}
let chasseur = persoOfId(cmd[1]);
if (chasseur === undefined) {
error("Le premier argument de !cof-sentir-la-corruption n'est pas un token valide", msg.content);
return;
}
let cible = persoOfId(cmd[2]);
if (cible === undefined) {
error("Le deuxième argument de !cof-sentir-la-corruption n'est pas un token valide", msg.content);
return;
}
let portee = 30;
if (options.portee !== undefined) portee = options.portee;
if (distanceCombat(chasseur.token, cible.token) > portee) {
sendPerso(chasseur, " est trop loin de " + nomPerso(cible) + " pour sentir " + onGenre(cible, "s'il", "si elle") + " est corrompu" + eForFemale(cible));
return;
}
let playerId = getPlayerIdFromMsg(msg);
sentirLaCorruption(playerId, chasseur, cible, options);
}
function sentirLaCorruption(playerId, chasseur, cible, options) {
const evt = {
type: "Sentir la corruption",
action: {
playerId,
chasseur,
cible,
options,
}
};
addEvent(evt);
if (limiteRessources(chasseur, options, 'sentirLaCorruption', 'sentir la corruption', evt)) return;
let optionsDisplay = {
secret: options.secret
};
let display = startFramedDisplay(playerId, 'Sentir la corruption', chasseur, optionsDisplay);
testCaracteristique(chasseur, 'SAG', 15, 'sentirLaCorruptionChasseur', options, evt,
function(tr, explications) {
let msgRes = "Jet de SAG : " + tr.texte;
explications.forEach(function(m) {
addLineToFramedDisplay(display, m, 80);
});
let endDisplay = function() {
if (options.messages) {
options.messages.forEach(function(m) {
addLineToFramedDisplay(display, m);
});
}
if (display.retarde) {
addFramedHeader(display, playerId, true);
sendFramedDisplay(display);
addFramedHeader(display, undefined, 'gm');
sendFramedDisplay(display);
} else sendFramedDisplay(display);
};
if (tr.reussite) {
addLineToFramedDisplay(display, msgRes + tr.modifiers);
if (estHumanoide(cible)) {
testCaracteristique(cible, 'INT', 15, 'sentirLaCorruptionCible', options, evt,
function(tr, explications) {
let msgRes = "Jet d'INT de " + nomPerso(cible) + " : " + tr.texte;
//On n'affiche pas les possibilités de rerolls, sinon il faudrait un bouton pour "continuer" afin de ne pas afficher le résultat.
addLineToFramedDisplay(display, msgRes + tr.modifiers);
explications.forEach(function(m) {
addLineToFramedDisplay(display, m, 80);
});
msgRes = nomPerso(chasseur);
if (tr.reussite || !predicateAsBool(cible, 'corrompu')) {
msgRes += " ne sent aucune corruption chez " + nomPerso(cible);
} else {
msgRes += " sent de la corruption chez " + nomPerso(cible);
}
addLineToFramedDisplay(display, msgRes);
effetsSpeciaux(chasseur, cible, options);
endDisplay();
});
} else {
let msgRes = nomPerso(chasseur);
if (predicateAsBool(cible, 'corrompu'))
msgRes += " sent de la corruption chez " + nomPerso(cible);
else
msgRes += " ne sent aucune corruption chez " + nomPerso(cible);
addLineToFramedDisplay(display, msgRes + tr.modifiers);
effetsSpeciaux(chasseur, cible, options);
endDisplay();
}
} else {
addLineToFramedDisplay(display, msgRes + tr.rerolls + tr.modifiers);
addLineToFramedDisplay(display, nomPerso(chasseur) + " ne réussit pas à sentir la corruption.");
endDisplay();
}
});
}
//!cof-creer-baies nbre
function creerBaies(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Problème de parse options", msg.content);
return;
}
let nbBaies;
if (cmd.length > 1) {
nbBaies = parseInt(cmd[1]);
if (isNaN(nbBaies) || nbBaies < 1) {
sendPlayer(msg, "Aucune baie créée. la commnde en demandait " + cmd[1]);
return;
}
}
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
sendPlayer(msg, "Utilisation de !cof-creer-baies sans sélection de token", playerId);
return;
}
const evt = {
type: "Création de baies"
};
addEvent(evt);
iterSelected(selected, function(druide) {
if (limiteRessources(druide, options, 'creationDeBaies', "créer des baies", evt)) return;
let niveau = ficheAttributeAsInt(druide, 'niveau', 1);
let mangerBaie = "!cof-consommer-baie " + niveau + " --limiteParJour 1 baieMagique";
let nb;
if (nbBaies) {
nb = {
val: nbBaies,
roll: nbBaies + ''
};
} else {
nb = rollDePlus(6, {
bonus: modCarac(druide, 'sagesse')
});
}
ajouterConsommable(druide, 'Baie magique', nb.val, mangerBaie, evt);
sendPerso(druide, "crée " + nb.roll + " baies magiques");
});
});
}
function doRetourBoomerang(lanceur, label, evt) {
if (stateCOF.combat && stateCOF.options.affichage.val.init_dynamique.val) {
threadSync++;
activateRoundMarker(threadSync, lanceur.token);
}
let weaponStats = getWeaponStats(lanceur, label);
if (!weaponStats || !weaponStats.armeDeJet) {
error("Il semble que l'arme ne soit pas une arme de jet", label);
return;
}
let attrName = weaponStats.prefixe + 'armejetqte';
let attr = findObjs({
_type: 'attribute',
_characterid: lanceur.charId,
name: attrName
}, {
caseInsensitive: true
});
evt.attributes = evt.attributes || [];
let max = 1;
if (attr.length > 0) {
attr = attr[0];
if (attr.length > 1) {
error("Plus d'un attribut pour la quantité d'armes de jet", attr);
attr[1].remove();
}
max = parseInt(attr.get('max'));
if (isNaN(max) || max < 1) {
error("Maximum de " + weaponStats.name + " mal formé, vérifier sur la fiche", attr);
max = 1;
}
evt.attributes.push({
attribute: attr,
current: weaponStats.nbArmesDeJet,
max: max
});
} else {
attr = createObj('attribute', {
characterid: lanceur.charId,
name: attrName,
current: 1,
max: 1
});
evt.attributes.push({
attribute: attr,
});
}
if (weaponStats.nbArmesDeJet >= max) {
error(nomPerso(lanceur) + " a déjà toutes ses armes de jet (" + max + ")", weaponStats);
return;
}
attr.set('current', weaponStats.nbArmesDeJet + 1);
sendPerso(lanceur, "rattrape son " + weaponStats.name);
}
//!cof-retour-boomerang id label
function retourBoomerang(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Problème de parse options", msg.content);
return;
}
if (cmd.length < 3) {
error("Il manque des arguments à !cof-retour-boomerang", cmd);
return;
}
let lanceur = persoOfId(cmd[1]);
if (!lanceur) {
error("L'id du token ayan lancé les armes est incorrecte", cmd);
return;
}
let evt = {
type: "Retour arme de jet"
};
addEvent(evt);
doRetourBoomerang(lanceur, cmd[2], evt);
}
//Pour ouvrir une porte sans event, en particulier en cas de pause
// !cof-open-door id
function openDoor(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Problème de parse options", msg.content);
return;
}
if (cmd.length < 2) {
error("Il manque un argument à !cof-open-door", cmd);
return;
}
let door = getObj('door', cmd[1]);
if (door === undefined) {
error("Impossible de trouver la porte", cmd);
return;
}
door.set('isOpen', true);
}
//Demande de bouger son token jusqu'à destination
function attaqueLigneBouger(msg) {
const playerId = getPlayerIdFromMsg(msg);
if (stateCOF.pause && !playerIsGM(playerId)) {
sendPlayer(msg, "Le jeu est en pause", playerId);
return;
}
let args = msg.content.split(' ');
if (args.length < 2) {
error("Il manque un argument à !cof-attack-line", args);
return;
}
const attaquant = persoOfId(args[1]);
if (attaquant === undefined) {
error("Le premier argument de !cof-attack-line n'est pas un token valide", args[1]);
return;
}
let token = attaquant.token;
let restArgs = args.slice(2).join(' ');
let command = '!cof-attack-line-from ' + token.get('left') + ' ' + token.get('top') + ' ' + token.id + ' ' + restArgs;
sendPlayer(msg, "Déplacer votre token jusqu'au bout de la ligne (l'emplacement doit être libre), et " + boutonSimple(command, "cliquer ici"), playerId);
return;
}
//En partant de from, retourne la première position sur le segment [from, to]
// à distance dist de pt. S'il n'y en a pas, retourne la projection de pt sur le segment
function positionLigne(from, to, token, dist) {
let fx = from.x;
let fy = from.y;
let tx = to.x;
let ty = to.y;
let x = token.get('left');
let y = token.get('top');
let a = (tx - fx) * (tx - fx) + (ty - fy) * (ty - fy);
let b = 2 * ((fx - x) * (tx - fx) + (fy - y) * (ty - fy));
let c = (fx - x) * (fx - x) + (fy - y) * (fy - y) - dist * dist;
let delta = b * b - 4 * a * c;
if (delta >= 0) {
let k = (b + Math.sqrt(delta)) / (2 * a);
if (k > 0) k = 0;
if (k < -1) k = -1;
return [fx - k * (tx - fx), fy - k * (ty - fy)];
}
//sinon on projette
let ps = (x - fx) * (tx - fx) + (y - fy) * (tx - fy) + 0.0;
let k = ps / Math.sqrt(a);
if (k < 0) k = 0;
if (k > 1) k = 1;
return [fx + k * (tx - fx), fy + k * (ty - fy)];
}
//!cof-attack-line-from left top id
function attaqueLigne(msg) {
const playerId = getPlayerIdFromMsg(msg);
if (stateCOF.pause && !playerIsGM(playerId)) {
sendPlayer(msg, "Le jeu est en pause", playerId);
return;
}
let optArgs = msg.content.split(' --');
let args = optArgs[0].split(' ');
args = args.filter(function(a) {
return a !== '';
});
optArgs.shift();
if (args.length < 4) {
error("Pas assez d'arguments pour !cof-attack-line: " + msg.content, args);
return;
}
let originLeft = parseInt(args[1]);
let originTop = parseInt(args[2]);
if (isNaN(originLeft) || isNaN(originTop)) {
error("Coordonnées non numériques dans !co-attack-line", args);
return;
}
const attaquant = persoOfId(args[3]);
if (attaquant === undefined) {
error("Le premier argument de !cof-attack-line n'est pas un token valide", args[3]);
return;
}
let destLeft = attaquant.token.get('left');
let destTop = attaquant.token.get('top');
//On remet l'attaquant à sa place
attaquant.token.set('left', originLeft);
attaquant.token.set('top', originTop);
let combat = stateCOF.combat;
if (stateCOF.options.affichage.val.init_dynamique.val && roundMarker &&
combat) {
if ((!stateCOF.chargeFantastique && combat.activeTokenId == attaquant.token.id) ||
(stateCOF.chargeFantastique && stateCOF.chargeFantastique.activeTokenId == attaquant.token.id)) {
roundMarker.set('left', originLeft);
roundMarker.set('top', originTop);
}
}
let pageId = attaquant.token.get('pageid');
let scale = computeScale(pageId);
let restArgs = '';
if (args.length > 4) {
restArgs = args.slice(4).join(' ');
}
let ptDest = {
x: destLeft,
y: destTop
};
let ptOrigin = {
x: originLeft,
y: originTop
};
let tropLoin = false;
//On cherche si argument --distanceMax, pas utilisé par cof-attack
optArgs = optArgs.filter(function(cmd) {
if (!cmd.startsWith('distanceMax')) return true;
let a = cmd.split(' ');
if (a.length < 2) {
error("Il manque un argument à --distanceMax", cmd);
return false;
}
let distanceMax = parseInt(a[1]);
if (isNaN(distanceMax) || distanceMax < 0) {
error("la distance max n'est pas un nombre positif", cmd);
return false;
}
let distancePix = distancePoints(ptOrigin, ptDest);
let distance = ((distancePix / PIX_PER_UNIT) * scale);
if (distance > distanceMax) {
sendPlayer(msg, "Ligne d'attaque trop grande, choisir une destination plus proche");
tropLoin = true;
}
return false;
});
if (optArgs.length > 0) {
restArgs = restArgs + ' --' + optArgs.join(' --');
}
if (tropLoin) return;
//On regarde si l'emplacement est bien vide.
let tokens = findObjs({
_pageid: pageId,
_type: 'graphic',
_subtype: 'token',
layer: attaquant.token.get('layer')
});
tokens = tokens.filter(function(tok) {
return tok.id != attaquant.token.id && tok.get('represents');
});
let attRayon = tokenSize(attaquant.token, 0);
let overLap = tokens.some(function(tok) {
let pt = pointOfToken(tok);
let distancePix = distancePoints(pt, ptDest);
return distancePix < attRayon + tokenSize(tok, 0);
});
if (overLap) {
sendPlayer(msg, "La place n'est pas libre", playerId);
return;
}
//Puis s'il n'y a pas d'obstacles sur le trajet
let page = getObj('page', pageId);
let murs = getWalls(page, pageId);
if (murs) {
let pc = {
x: originLeft,
y: originTop
};
if (obstaclePresent(destLeft, destTop, pc, murs)) {
sendPlayer(msg, "Il y a des obstacles sur le trajet", playerId);
return;
}
}
//On détermine les cibles sur le trajet
let cibles = [];
tokens.forEach(function(tok) {
let cible = {
token: tok,
charId: tok.get('represents')
};
if (nePeutPlusPrendreDM(cible, {})) return; //pas de dégâts aux morts
let distToTrajectory = distancePixTokenSegment(tok, ptOrigin, ptDest);
if (distToTrajectory > attRayon + tokenSize(tok, 0))
return;
cible.tokName = tok.get('name');
let tokChar = getObj('character', cible.charId);
if (tokChar === undefined) return;
cible.name = tokChar.get('name');
let pt = pointOfToken(tok);
cible.distanceOrigine = distancePoints(ptOrigin, pt);
cibles.push(cible);
});
if (cibles.length === 0) {
sendPlayer(msg, "Aucune cible valide sur le trajet", playerId);
return;
}
const evt = {
type: "Attaque en ligne"
};
addEvent(evt);
let explications = [];
combat = entrerEnCombat(attaquant, [], explications, evt);
explications.forEach(function(m) {
sendPerso(attaquant, m);
});
combat.attackId = combat.attackId || 0;
combat.attackCallbacks = combat.attackCallBacks || {};
//On trie les cibles selon leur distance à l'origine.
cibles.sort(function(c1, c2) {
return c1.distanceOrigine - c2.distanceOrigine;
});
combat.attackId++;
let firstAttack = combat.attackId;
cibles.forEach(function(cible) {
let nextAttack = combat.attackId + 1;
let dist = attRayon + tokenSize(cible.token, 0);
let [left, top] = positionLigne(ptOrigin, ptDest, cible.token, dist);
let comAttaque = '!cof-attack ' + attaquant.token.id + ' ' + cible.token.id + ' ' + restArgs + ' --attackId ' + nextAttack;
let comSkip = '!cof-skip-attack ' + nextAttack;
let m = boutonSimple(comAttaque, "Attaquer " + cible.tokName) + " ou " +
boutonSimple(comSkip, "Continuer");
combat.attackCallbacks[combat.attackId] = function(evt) {
moveTokenWithUndo(attaquant.token, left, top, evt);
sendPlayer(msg, m, playerId);
};
combat.attackId++;
});
combat.attackCallbacks[combat.attackId] = function() {
attaquant.token.set('left', destLeft);
attaquant.token.set('top', destTop);
};
combat.attackCallbacks[firstAttack]();
}
function skipAttack(msg) {
let args = msg.content.split(' ');
if (args.length < 2) {
error("Il manque l'id dans !cof-skip-attack", args);
return;
}
let options = {
attackId: args[1]
};
let evt = {
type: "skip attack"
};
addEvent(evt);
attackCallback(options, evt);
}
//!cof-vision-nocturne distance [distance vue normale]
function ajouterVisionNocturne(msg) {
let options = parseOptions(msg);
if (options === undefined) return;
let cmd = options.cmd;
if (cmd === undefined) {
error("Problème de parse options", msg.content);
return;
}
if (cmd.length < 2) {
error("Il manque un argument à !cof-vision-nocturne", cmd);
return;
}
let distance = parseInt(cmd[1]);
if (isNaN(distance) || distance < 0) {
error("Distance de vue incorrecte", cmd);
return;
}
let pageId = options.pageId;
getSelected(msg, function(selected, playerId) {
if (selected.length === 0) {
sendPlayer(msg, "Utilisation de !cof-vision-nocturne sans sélection de token", playerId);
return;
}
const evt = {
type: 'vision nocturne',
};
addEvent(evt);
iterSelected(selected, function(perso) {
let token = perso.token;
setToken(token, 'has_night_vision', true, evt);
setToken(token, 'night_vision_effect', 'Nocturnal', evt);
setToken(token, 'night_vision_distance', distance, evt);
pageId = pageId || token.get('pageid');
});
forceLightingRefresh(pageId);
});
}
const listeDesRunesMortes = {
Melianil: {
description: "+2 en attaque et +1d6 aux DM (à tous les sorts ou aux attaques au contact avec le bâton)",
},
Isulys: {
description: "une fois par tour, par une action d'attaque, peut produire un rayon d’énergie négative d’une portée de 40 mètres. Sur test d’attaque magique réussit, la victime subit [2d6 + Mod. %SAGINT] DM et doit utiliser un d12 à tous ses tests au lieu d’un d20 pendant un tour.",
},
Bryniza: {
description: "permet de voir parfaitement dans le noir à une portée de 30 m, et comme dans la pénombre jusqu'à 50 m. Au prix d'une action limitée, le porteur peut utiliser le sort Détection de la magie 3 fois par jour.",
activation: "Les yeux de %NOM deviennent d'un noir d'encre.",
fin: "Les yeux de %NOM redeviennent normaux.",
},
Lizura: {
description: "immunité au souffle de Swarathnak, à l'acide et au poison.",
activation: "La boue noire du bâton s'écoule le long du bras de %NOM et recouvre tout son corps à l'exception des yeux.",
fin: "La boue se retire dans le bâton, %NOM retrouve une apparence normale.",
memeEffetQue: 'Mitrah',
},
Mitrah: {
description: "RD 5 à tous les DM.",
activation: "La boue noire du bâton s'écoule le long du bras de %NOM et recouvre tout son corps à l'exception des yeux.",
fin: "La boue se retire dans le bâton, %NOM retrouve une apparence normale.",
memeEffetQue: 'Lizura',
},
};
const styleRuneActive =
'display: inline-block; border-radius: 5px; padding: 0 4px; background-color: #80bf40; color: #020401;';
//!cof-gerer-runes-mortes token_id
function gererRunesMortes(msg) {
let cmd = msg.content.split(' ');
if (cmd.length < 2) {
error("il manque l'id du personnage qui gère ses runes mortes", cmd);
return;
}
let perso = persoOfId(cmd[1]);
if (perso === undefined) return;
if (!predicateAsBool(perso, 'batonDesRunesMortes')) {
sendPerso(perso, "ne porte pas le bâton des runes mortes");
return;
}
let niveau = ficheAttributeAsInt(perso, 'niveau', 1);
let playerId = getPlayerIdFromMsg(msg);
let display = startFramedDisplay(playerId, "Runes mortes", perso, {
chuchote: true
});
let caracSag = "de SAG";
if (modCarac(perso, 'intelligence') > modCarac(perso, 'sagesse'))
caracSag = "d'INT";
if (cmd.length > 3) {
const evt = {
type: "gestion des runes mortes",
};
addEvent(evt);
switch (cmd[2]) {
case 'activer':
{
let er = listeDesRunesMortes[cmd[3]];
if (!er) {
error("Rune " + cmd[3] + " non reconnue", cmd);
break;
}
//Vérification qu'on n'a pas déjà le max de runes
let runesLibres = Math.ceil(niveau / 4);
for (let rune in listeDesRunesMortes) {
if (attributeAsBool(perso, 'rune' + rune)) runesLibres--;
}
if (runesLibres < 1) {
sendPerso(perso, "ne peut pas activer plus de runes", true);
break;
}
let attr = tokenAttribute(perso, 'rune' + cmd[3]);
if (attr.length > 0) {
if (attr[0].get('current') == 'false') {
let donne = attr[0].get('max');
if (donne) {
sendPerso(perso, "a donné " + cmd[3] + " à " + donne + ". Impossible de l'activer.", true);
break;
}
} else {
sendPerso(perso, cmd[3] + " est déjà active", true);
break;
}
}
let msg;
if (er.activation) {
if (!er.memeEffetQue || !attributeAsBool(perso, 'rune' + er.memeEffetQue))
msg = er.activation.replace('%NOM', nomPerso(perso));
}
setTokenAttr(perso, 'rune' + cmd[3], true, evt, {
msg
});
sendPerso(perso, er.description.replace('%SAGINT', caracSag), true);
if (cmd[3] == 'Bryniza') {
//On met la vision dans le noir
let token = perso.token;
let pageId = token.get('pageid');
let visionNoir = predicateAsInt(perso, 'visionDansLeNoir', 0);
if (visionNoir <= 30) {
let page = getObj('page', pageId);
let udl = page && page.get('dynamic_lighting_enabled');
if (udl) {
let vs = scaleDistance(perso, 50);
token.set('has_night_vision', true);
token.set('night_vision_effect', 'Dimming');
token.set('night_vision_distance', vs);
page.set('force_lighting_refresh', true);
}
}
}
break;
}
case 'desactiver':
{
let er = listeDesRunesMortes[cmd[3]];
if (!er) {
error("Rune " + cmd[3] + " non reconnue", cmd);
break;
}
if (!attributeAsBool(perso, 'rune' + cmd[3])) {
sendPerso(perso, cmd[3] + " n'est pas activée.", true);
break;
}
let msg;
if (er.fin) {
if (!er.memeEffetQue || !attributeAsBool(perso, 'rune' + er.memeEffetQue))
msg = er.fin.replace('%NOM', nomPerso(perso));
}
removeTokenAttr(perso, 'rune' + cmd[3], evt, {
msg
});
if (cmd[3] == 'Bryniza') {
//On enlève la vision dans le noir
let token = perso.token;
let pageId = token.get('pageid');
let visionNoir = predicateAsInt(perso, 'visionDansLeNoir', 0);
if (visionNoir <= 30) {
let page = getObj('page', pageId);
let udl = page && page.get('dynamic_lighting_enabled');
if (udl) {
if (visionNoir === 0) {
token.set('has_night_vision', false);
} else {
let vs = scaleDistance(perso, visionNoir);
token.set('night_vision_distance', vs);
}
page.set('force_lighting_refresh', true);
}
}
}
break;
}
case 'donner':
{
if (cmd.length < 4) {
error("Il faut préciser le token à qui donner la rune", cmd);
break;
}
let cible = persoOfId(cmd[4]);
if (!cible) {
error("Impossible de trouver à qui donner la rune", cmd);
break;
}
let attr = tokenAttribute(perso, 'rune' + cmd[3]);
if (attr.length > 0) {
if (attr[0].get('current') == 'false') {
let donne = attr[0].get('max');
if (donne) {
sendPerso(perso, "a déjà donné " + cmd[3] + " à " + donne + ".", true);
break;
}
} else {
sendPerso(perso, "Il faut d'abord désactiver " + cmd[3] + " avant de la donner", true);
break;
}
}
let er = listeDesRunesMortes[cmd[3]];
if (!er) {
error("Rune " + cmd[3] + " non reconnue", cmd);
break;
}
let msg = nomPerso(perso) + " donne une rune à " + nomPerso(cible);
let maxVal = nomPerso(cible);
setTokenAttr(perso, 'rune' + cmd[3], 'false', evt, {
msg,
maxVal
});
sendPerso(perso, "peut maintenant communiquer par télépathie avec " + nomPerso(cible), true);
sendPerso(cible, "peut maintenant communiquer par télépathie avec " + nomPerso(perso), true);
break;
}
case 'recuperer':
{
let er = listeDesRunesMortes[cmd[3]];
if (!er) {
error("Rune " + cmd[3] + " non reconnue", cmd);
break;
}
let attr = tokenAttribute(perso, 'rune' + cmd[3]);
if (attr.length === 0) {
sendPerso(perso, "a déjà " + cmd[3], true);
break;
}
if (attr[0].get('current') != 'false') {
sendPerso(perso, "a déjà " + cmd[3], true);
break;
}
let donne = attr[0].get('max');
if (!donne) {
sendPerso(perso, "a déjà " + cmd[3], true);
break;
}
let msg = nomPerso(perso) + " rappelle une rune sur le bâton";
removeTokenAttr(perso, 'rune' + cmd[3], evt, {
msg
});
sendPerso(perso, "ne peut plus communiquer par télépathie avec " + donne, true);
break;
}
default:
error("Action de gestion des runes " + cmd[2] + " non reconnue", cmd);
}
}
let nombreDeRunes = 0;
let etatDesRunes = [];
for (let rune in listeDesRunesMortes) {
let descr = listeDesRunesMortes[rune].description.replace('%SAGINT', caracSag);
let er = {
nom: rune,
descr
};
let attr = tokenAttribute(perso, "rune" + rune);
if (attr.length > 0) {
let actif = attr[0].get('current') != 'false';
if (actif) {
nombreDeRunes++;
er.actif = true;
} else {
let donne = attr[0].get('max');
if (donne) er.donne = donne;
}
}
etatDesRunes.push(er);
}
let maxActives = false;
if (niveau < 2) maxActives = true;
else maxActives = nombreDeRunes >= Math.ceil(niveau / 4);
etatDesRunes.forEach(function(er) {
let rune = er.nom;
let ligne = 'title="' + er.descr + '"> ' + rune + '';
let action =
"!cof-bourse fixer ?{Nouveau montant de pièces " + piece + " ?} " + finAction;
line += boutonSimple(action, val) + '' + nom + '';
line += ' ';
action = "!cof-bourse depenser ?{Pièces " + piece + " à dépenser ?} " + finAction;
line += boutonSimple(action, 'Dépenser');
action = "!cof-bourse gagner ?{Pièces " + piece + " ?} " + finAction;
line += boutonSimple(action, 'Gagner');
line += '
');
note = note.replace(/<\/p>/g, '');
let lignes = note.trim().split('
');
lignes.forEach(function(ligne) {
ligne = ligne.trim();
let header = ligne.split(':');
if (header.length > 1) {
let c = header.shift().trim().toUpperCase();
if (c != 'FOR' && c != 'CON' && c != 'DEX' && c != 'INT' && c != 'SAG' && c != 'CHA') return;
carac = c;
ligne = header.join(':').trim();
}
if (ligne.length === 0) return;
if (carac === undefined) {
log("Compétences sans caractéristique associée");
return;
}
let comps = ligne.split(/, |\/| /);
comps.forEach(function(comp) {
if (comp.length === 0) return;
listeCompetences[carac].push(comp);
listeCompetences.nombre++;
});
});
let compToCarac = {};
listeCompetences.FOR.forEach(function(c) {
compToCarac[c] = 'FOR';
compToCarac[c.toLowerCase()] = 'FOR';
});
listeCompetences.CON.forEach(function(c) {
compToCarac[c] = 'CON';
compToCarac[c.toLowerCase()] = 'CON';
});
listeCompetences.DEX.forEach(function(c) {
compToCarac[c] = 'DEX';
compToCarac[c.toLowerCase()] = 'DEX';
});
listeCompetences.INT.forEach(function(c) {
compToCarac[c] = 'INT';
compToCarac[c.toLowerCase()] = 'INT';
});
listeCompetences.SAG.forEach(function(c) {
compToCarac[c] = 'SAG';
compToCarac[c.toLowerCase()] = 'SAG';
});
listeCompetences.CHA.forEach(function(c) {
compToCarac[c] = 'CHA';
compToCarac[c.toLowerCase()] = 'CHA';
});
attrs.forEach(function(a) {
let attrName = a.get('name');
switch (attrName) {
case 'RACE':
a.set('name', 'race');
return;
case 'PROFIL':
a.set('name', 'profil');
return;
case 'NIVEAU':
a.set('name', 'niveau');
return;
case 'SEXE':
a.set('name', 'sexe');
return;
case 'AGE':
a.set('name', 'age');
return;
case 'TAILLE':
a.set('name', 'taille');
return;
case 'POIDS':
a.set('name', 'poids');
return;
case 'FORCE':
a.set('name', 'force');
return;
case 'DEXTERITE':
a.set('name', 'dexterite');
return;
case 'CONSTITUTION':
a.set('name', 'constitution');
return;
case 'INTELLIGENCE':
a.set('name', 'intelligence');
return;
case 'SAGESSE':
a.set('name', 'sagesse');
return;
case 'CHARISME':
a.set('name', 'charisme');
return;
}
//Les compétences
let charId = a.get('characterid');
//On ne bouge les compétences que pour les persos de type PJ
let typePerso = findObjs({
_type: 'attribute',
_characterid: charId,
name: 'type_personnage',
}, {
caseInsensitive: true
});
if (typePerso.length > 0 && typePerso[0].get('current') != 'PJ') return;
if (compToCarac[attrName] === undefined) return;
let prefix = 'repeating_competences_' + generateRowID() + '_comp_';
let attrSpec = {
characterid: charId
};
attrSpec.name = prefix + 'nom';
attrSpec.current = attrName;
createObj('attribute', attrSpec);
attrSpec.name = prefix + 'bonus';
attrSpec.current = a.get('current');
let attrBonus = createObj('attribute', attrSpec);
attrSpec.name = prefix + 'bonusTotal';
createObj('attribute', attrSpec);
attrSpec.name = prefix + 'carac';
attrSpec.current = compToCarac[attrName];
createObj('attribute', attrSpec);
let attrMalus;
if ((attrSpec.current == 'DEX' && attrName != 'crochetage' && attrName != 'désamorçage') ||
(attrSpec.current == 'CON' && attrName == 'survie') ||
attrName == 'natation' || attrName == 'escalade') {
attrSpec.name = prefix + 'malus';
attrSpec.current = 'armure';
attrMalus = createObj('attribute', attrSpec);
attrBonus.setWithWorker('current', a.get('current'));
attrMalus.setWithWorker('current', 'armure');
} else if (attrName == 'perception' || attrName == 'vigilance') {
attrSpec.name = prefix + 'malus';
attrSpec.current = 'casque';
attrMalus = createObj('attribute', attrSpec);
attrBonus.setWithWorker('current', a.get('current'));
attrMalus.setWithWorker('current', 'casque');
}
a.remove();
});
}); //end hand.get(notes)
} else {
attrs.forEach(function(a) {
let attrName = a.get('name');
switch (attrName) {
case 'RACE':
a.set('name', 'race');
return;
case 'PROFIL':
a.set('name', 'profil');
return;
case 'NIVEAU':
a.set('name', 'niveau');
return;
case 'SEXE':
a.set('name', 'sexe');
return;
case 'AGE':
a.set('name', 'age');
return;
case 'TAILLE':
a.set('name', 'taille');
return;
case 'POIDS':
a.set('name', 'poids');
return;
case 'FORCE':
a.set('name', 'force');
return;
case 'DEXTERITE':
a.set('name', 'dexterite');
return;
case 'CONSTITUTION':
a.set('name', 'constitution');
return;
case 'INTELLIGENCE':
a.set('name', 'intelligence');
return;
case 'SAGESSE':
a.set('name', 'sagesse');
return;
case 'CHARISME':
a.set('name', 'charisme');
return;
}
});
}
log("Mise à jour des attributs de compétence effectué");
}
if (version < 2.17) {
let macros = findObjs({
_type: 'macro'
}).concat(findObjs({
_type: 'ability'
}));
macros.forEach(function(m) {
let macro = m.get("action");
let newMacro = macro.replace("!cof-tenebres @{selected|token_id} --disque @{target|token_id} 5 20",
"!cof-tenebres @{selected|token_id} @{target|token_id}");
if (macro !== newMacro)
m.set("action", newMacro);
});
log("Mise à jour des ability Ténèbres effectuée");
}
let updateReset = function(a, nom, typ) {
let c = parseInt(a.get('current'));
let m = parseInt(a.get('max'));
if (c === m) return;
if (isNaN(m)) return;
a.set('current', m);
createObj('attribute', {
name: 'limitePar' + typ + '__' + nom,
characterid: a.get('characterid'),
current: c
});
};
if (version < 2.18) {
if (state.COFantasy.combat) {
let attrs = findObjs({
_type: 'attribute',
});
attrs.forEach(function(a) {
let nom = a.get('name');
switch (nom) {
case 'chairACanon':
case 'attaqueEnTraitre':
case 'paradeDeProjectiles':
case 'prouesse':
case 'devierLesCoups':
updateReset(a, nom, 'Tour');
return;
case 'increvable':
case 'esquiveFatale':
case 'interventionDivine':
case 'petitVeinard':
case 'prescience':
case 'kiai':
updateReset(a, nom, 'Combat');
return;
default:
return;
}
});
}
log("Mise à jour des attributs effectuée");
}
if (version < 3.00) {
let macros = findObjs({
_type: 'macro'
});
characters.forEach(function(c) {
let cid = c.id;
let abilities = findObjs({
_type: 'ability',
_characterid: cid,
});
let actionsDuTour;
let actionAbilities = abilities.filter(function(a) {
let an = a.get('name');
if (an.startsWith('#') && an.endsWith('#')) {
if (actionsDuTour) return true;
if (an == '#TurnAction#' || an == '#Actions#') {
actionsDuTour = a;
return false;
}
return true;
}
return false;
});
let listeARemplir = 1;
let abilitiesInList = new Set();
actionAbilities.forEach(function(a) {
if (listeARemplir > 4) return;
let listeActions = a.get('action')
.replace(/\n/gm, '').replace(/\r/gm, '')
.replace(/%#([^#]*)#/g, '\n!cof-liste-actions $1')
.replace(/\/\/%/g, '\n\/\/__pc')
.replace(/\/\/#/g, '\n\/\/__mc')
.replace(/\/\/!/g, '\n\/\/__cmd')
.replace(/%/g, '\n%').replace(/#/g, '\n#').replace(/!/g, '\n!')
.split('\n');
if (listeActions.length === 0) return;
let n = 0;
let attrRang = findObjs({
_type: 'attribute',
_characterid: cid,
name: 'maxrangaction' + listeARemplir
}, {
caseInsensitive: true
});
if (attrRang && attrRang.length > 0) {
attrRang = attrRang[0];
n = parseInt(attrRang.get('current'));
if (isNaN(n) || n < 0) n = 0;
} else {
attrRang = createObj('attribute', {
name: 'maxrangaction' + listeARemplir,
current: 0,
characterid: cid
});
}
listeActions.forEach(function(action) {
action = action.trim();
if (action === '') return;
if (n === 0 && action.startsWith('!options')) {
createObj('attribute', {
name: 'optionslisteactions' + listeARemplir,
current: action.substring(8).trim(),
characterid: cid,
});
createObj('attribute', {
name: 'action' + listeARemplir + 'optflag',
current: 'on',
characterid: cid,
});
return;
}
let pref = 'repeating_actions' + listeARemplir + '_' + generateRowID() + '_';
n++;
if (action.startsWith('//')) {
action = action.substring(2);
if (action.startsWith('__pc')) {
action = action.substr(4);
abilitiesInList.add(action.split(' ')[0]);
action = '%' + action;
} else if (action.startsWith('__mc')) action = '#' + action.substr(4);
else if (action.startsWith('__cmd')) action = '!' + action.substr(5);
createObj('attribute', {
name: pref + 'actiontitre',
current: action,
characterid: cid,
});
createObj('attribute', {
name: pref + 'actionmontree',
current: 0,
characterid: cid,
});
createObj('attribute', {
name: pref + 'actionoptflag',
current: 'off',
characterid: cid,
});
createObj('attribute', {
name: pref + 'actionrang',
current: n,
characterid: cid,
});
return;
}
if (action.startsWith('!cof-liste-actions ')) {
action = action.substring(19);
createObj('attribute', {
name: pref + 'actiontype',
current: 'liste',
characterid: cid,
});
} else if (action.startsWith('%')) {
let abName = action.split(' ')[0].substr(1);
abilitiesInList.add(abName);
}
createObj('attribute', {
name: pref + 'actiontitre',
current: action,
characterid: cid,
});
createObj('attribute', {
name: pref + 'actionoptflag',
current: 'off',
characterid: cid,
});
createObj('attribute', {
name: pref + 'actionrang',
current: n,
characterid: cid,
});
});
if (n === 0) return;
let an = a.get('name');
createObj('attribute', {
name: 'nomlisteaction' + listeARemplir,
current: an.substring(1, an.length - 1),
characterid: cid,
});
attrRang.set('current', n);
listeARemplir++;
a.remove();
});
let n = 0;
let attrRang = findObjs({
_type: 'attribute',
_characterid: cid,
name: 'maxrangaction'
}, {
caseInsensitive: true
});
if (attrRang && attrRang.length > 0) {
attrRang = attrRang[0];
n = parseInt(attrRang.get('current'));
if (isNaN(n) || n < 0) n = 0;
} else {
attrRang = createObj('attribute', {
name: 'maxrangaction',
current: 0,
characterid: cid
});
}
if (actionsDuTour) {
let actions = actionsDuTour.get('action')
.replace(/\n/gm, '').replace(/\r/gm, '')
.replace(/%#([^#]*)#/g, '\n!cof-liste-actions $1')
.replace(/\/\/%/g, '\n\/\/__pc')
.replace(/\/\/#/g, '\n\/\/__mc')
.replace(/\/\/!/g, '\n\/\/__cmd')
.replace(/%/g, '\n%').replace(/#/g, '\n#').replace(/!/g, '\n!')
.split('\n');
actions.forEach(function(action) {
action = action.trim();
if (action === '') return;
let pref;
let actionMontree = true;
if (action.startsWith('//')) {
action = action.substring(2);
if (action.startsWith('__pc')) action = '%' + action.substr(4);
else if (action.startsWith('__mc')) action = '#' + action.substr(4);
else if (action.startsWith('__cmd')) action = '!' + action.substr(5);
actionMontree = false;
}
if (action.startsWith('!cof-liste-actions ')) {
n++;
pref = 'repeating_actions_' + generateRowID() + '_';
createObj('attribute', {
name: pref + 'actionoptflag',
current: 'off',
characterid: cid,
});
createObj('attribute', {
name: pref + 'actionrang',
current: n,
characterid: cid,
});
createObj('attribute', {
name: pref + 'actiontitre',
current: action.substring(19),
characterid: cid,
});
createObj('attribute', {
name: pref + 'actiontype',
current: 'liste',
characterid: cid,
});
if (!actionMontree) createObj('attribute', {
name: pref + 'actionmontree',
current: 0,
characterid: cid,
});
return;
}
let actionCommands = action.split(' ');
actionCommands = actionCommands.filter(function(c) {
return c !== '';
});
let actionCmd = actionCommands[0];
let actionText = actionCmd.replace(/-/g, ' ').replace(/_/g, ' ');
let found = false;
switch (actionCmd.charAt(0)) {
case '%':
// Ability
actionCmd = actionCmd.substr(1);
actionText = actionText.substr(1);
abilities.forEach(function(abilitie, index) {
if (found) return;
if (abilitie.get('name') === actionCmd) {
// l'ability existe
found = true;
n++;
pref = 'repeating_actions_' + generateRowID() + '_';
createObj('attribute', {
name: pref + 'actionoptflag',
current: 'off',
characterid: cid,
});
createObj('attribute', {
name: pref + 'actionrang',
current: n,
characterid: cid,
});
if (!actionMontree) createObj('attribute', {
name: pref + 'actionmontree',
current: 0,
characterid: cid,
});
if (abilitiesInList.has(actionCmd) || abilitie.get('istokenaction')) {
//On garde le texte partagé de l'ability.
createObj('attribute', {
name: pref + 'actiontitre',
current: action,
characterid: cid,
});
return;
}
let command = abilitie.get('action').trim();
if (actionCommands.length > 1) {
//On rajoute les options de l'ability
command += action.substr(action.indexOf(' '));
}
createObj('attribute', {
name: pref + 'actiontitre',
current: actionText,
characterid: cid,
});
createObj('attribute', {
name: pref + 'actioncode',
current: command,
characterid: cid,
});
//On peut effacer l'ability
abilitie.remove();
}
});
return;
case '#':
n++;
pref = 'repeating_actions_' + generateRowID() + '_';
createObj('attribute', {
name: pref + 'actionoptflag',
current: 'off',
characterid: cid,
});
createObj('attribute', {
name: pref + 'actionrang',
current: n,
characterid: cid,
});
if (!actionMontree) createObj('attribute', {
name: pref + 'actionmontree',
current: 0,
characterid: cid,
});
// Macro
//D'abord le cas de #Attaque, car le nom affiché est celui de l'arme
if (actionCmd == '#Attaque' && actionCommands.length > 1) {
createObj('attribute', {
name: pref + 'actiontitre',
current: action,
characterid: cid,
});
return;
}
actionCmd = actionCmd.substr(1);
actionText = actionText.substr(1);
macros.forEach(function(macro, index) {
if (found) return;
if (macro.get('name') === actionCmd) {
found = true;
let command = macro.get('action').trim();
if (actionCommands.length > 1) {
//On rajoute les options de la macro
command += action.substr(action.indexOf(' '));
}
createObj('attribute', {
name: pref + 'actiontitre',
current: actionText,
characterid: cid,
});
createObj('attribute', {
name: pref + 'actioncode',
current: command,
characterid: cid,
});
}
});
return;
default:
n++;
pref = 'repeating_actions_' + generateRowID() + '_';
createObj('attribute', {
name: pref + 'actionoptflag',
current: 'off',
characterid: cid,
});
createObj('attribute', {
name: pref + 'actionrang',
current: n,
characterid: cid,
});
createObj('attribute', {
name: pref + 'actiontitre',
current: action,
characterid: cid,
});
if (!actionMontree) createObj('attribute', {
name: pref + 'actionmontree',
current: 0,
characterid: cid,
});
}
});
actionsDuTour.remove();
} else if (state.COFantasy.options &&
state.COFantasy.options.affichage &&
state.COFantasy.options.affichage.val &&
state.COFantasy.options.affichage.val.actions_par_defaut.val) {
//Par défaut, on montrait la liste de toutes les abilities
abilities.forEach(function(a) {
n++;
let pref = 'repeating_actions_' + generateRowID() + '_';
createObj('attribute', {
name: pref + 'actionoptflag',
current: 'off',
characterid: cid,
});
createObj('attribute', {
name: pref + 'actionrang',
current: n,
characterid: cid,
});
let nom = a.get('name');
if (nom.startsWith('#') && nom.endsWith('#')) {
createObj('attribute', {
name: pref + 'actiontitre',
current: nom.substring(1, nom.length - 2),
characterid: cid,
});
createObj('attribute', {
name: pref + 'actiontype',
current: 'liste',
characterid: cid,
});
} else if (a.get('istokenaction')) {
createObj('attribute', {
name: pref + 'actiontitre',
current: '%' + nom,
characterid: cid,
});
} else { //On copie l'ability et on l'efface
let actionText = nom.replace(/-/g, ' ').replace(/_/g, ' ');
let command = a.get('action').trim();
createObj('attribute', {
name: pref + 'actiontitre',
current: actionText,
characterid: cid,
});
createObj('attribute', {
name: pref + 'actioncode',
current: command,
characterid: cid,
});
a.remove();
}
});
}
if (n > 0) attrRang.set('current', n);
});
log("Mise à jour des listes d'action effectuée");
//Ensuite les prédicats booléens
let predicates = '^(' +
'actionLibre|agripper|ambidextreDuelliste|animal|argumentDeTaille' +
'|armureProtection|aucuneActionCombat|baroudHonneur' +
'|botteMortelle|bouclierPsi|briseurDOs|bucheron|champion' +
'|chasseurEmerite|chatimentDuMale|chimiste|ciblesMultiples' +
'|combatEnPhalange|combatKinetique|commandant|controleDuMetabolisme' +
'|creatureArtificielle|crocEnJambe|defenseIntuitive|démon|devorer' +
'|durACuire|ecuyer|elfeNoir|enchainement|energieDeLaMort' +
'|estUneIllusion|exsangue|fée|fievreChene|frappeChirurgicale|gober' +
'|graceFeline|graceFelineVoleur|grosMonstreGrosseArme|grosseTete' +
'|hachesEtMarteaux|hausserLeTon|horsDePortee|humanoide' +
'|ignorerLaDouleur|immuniteAuxArmes|immuniteAuxSournoises' +
'|immuniteSaignement|increvableHumain|inderacinable|insecte' +
'|insensibleAffaibli|instinctDeSurvieHumain|intelligenceDuCombat' +
'|invisibleEnCombat|invulnerable|joliCoup|langageSombreHetre' +
'|liberteDAction|manoeuvreDuelliste|mauvais|memePasMal|monture' +
'|montureLoyale|montureMagique|morsureDuSerpent|mortVivant' +
'|nAbandonneJamais|natureNourriciereBaies|nonVivant' +
'|ordreDuChevalierDragon|peauDePierre|peutEnrager|projection' +
'|proprioception|protectionDMZone|quadrupede|raillerieImpossible' +
'|reduireLaDistance|riposte|saisirEtBroyer|sangDeFerIf' +
'|sansEsprit|sansPeur|scienceDuCritique|secondSouffle|sensAffutes' +
'|sergent|tirParabolique|tourDeForce|tropPetit|ventreMou' +
'|vieArtificielle' +
'|diviseEffet_.*|immunite_.*|protectionDMZone_.*' +
')$';
let predicateRegExp = new RegExp(predicates);
let parChar = {};
findObjs({
_type: 'attribute',
}).forEach(function(attribute) {
let nom = attribute.get('name');
if (predicateRegExp.test(nom)) {
let charId = attribute.get('characterid');
parChar[charId] = parChar[charId] || new Set();
parChar[charId].add(nom);
attribute.remove();
} else {
switch (nom) {
case 'immunitePeur':
{
let charId = attribute.get('characterid');
parChar[charId] = parChar[charId] || new Set();
parChar[charId].add('immunite_peur');
attribute.remove();
}
break;
case 'immuniteContreSurprise':
{
let charId = attribute.get('characterid');
parChar[charId] = parChar[charId] || new Set();
parChar[charId].add('immunite_surpris');
attribute.remove();
}
break;
case 'geant':
{
let charId = attribute.get('characterid');
parChar[charId] = parChar[charId] || new Set();
parChar[charId].add('géant');
attribute.remove();
}
break;
}
}
});
for (let charId in parChar) {
let pred = parChar[charId];
let predText = '';
for (let p of pred) {
predText += p + ' ';
}
let attr = findObjs({
_tyoe: 'attribute',
_characterid: charId,
name: 'predicats_script'
}, {
caseInsensitive: true
});
if (attr.length === 0) {
createObj('attribute', {
name: 'predicats_script',
current: predText,
characterid: charId
});
} else {
attr = attr[0];
attr.set('current', predText + attr.get('current'));
}
}
log("Transformation des attributs booléens en prédicats");
}
let getPredicateAttr = function(charId) {
let attr = findObjs({
_type: 'attribute',
_characterid: charId,
name: 'predicats_script'
}, {
caseInsensitive: true
});
let predText = '';
if (attr.length === 0) {
attr = createObj('attribute', {
name: 'predicats_script',
current: '',
characterid: charId
});
} else {
attr = attr[0];
predText = attr.get('current');
if (predText !== '') predText += '\n';
}
return {
attr,
predText
};
};
if (version < 3.01) {
let predicates = '^(' +
'absorptionEnergie|adaptable|armureDeVent|attaqueEnMeute' +
'|cavalierEmerite|coefPVMana|DEF_magie|dentellesEtRapiere' +
'|esquiveVoleur|faireMouche|frenesie|initEnMain' +
'|increvable|instinctDeSurvie|interventionDivine|loupParmiLesLoups' +
'|piquresDInsectes|prouesse|radarMental|RD_critique|reflexesFelins' +
'|siphonDesAmes|siphonDesAmesPrioritaire|tirPrecis|vetementsSacres' +
'|visionDansLeNoir|voieDeLaConjuration|voieDeLArcEtDuCheval' +
'|voieDeLaMagieElementaire|voieDeLaSurvie|voieDeLaTelekinesie' +
'|voieDeLHonneur|voieDesElixirs|voieDesMutations|voieDesRunes' +
'|voieDesSoins|voieDesVegetaux|voieDuGuerisseur' +
'|voieDuMeneurDHomme|voieDuMetal|voieDuSoldat|vulnerableCritique' +
'|initEnMain.*' +
')$';
let predicateRegExp = new RegExp(predicates);
let parChar = {};
findObjs({
_type: 'attribute',
}).forEach(function(attribute) {
let nom = attribute.get('name');
if (predicateRegExp.test(nom)) {
let charId = attribute.get('characterid');
parChar[charId] = parChar[charId] || {};
parChar[charId][nom] = attribute.get('current');
attribute.remove();
}
});
for (let charId in parChar) {
let pred = parChar[charId];
let {
attr,
predText
} = getPredicateAttr(charId);
for (let p in pred) {
let val = pred[p];
if (val === 'true') predText += p + ' ';
else predText += p + ':' + val + ' ';
}
attr.set('current', predText);
}
log("Transformation des attributs numériques en prédicats");
}
if (version < 3.02) {
const regPJ = new RegExp("^(repeating_armes_[^_]*_)(.*)$");
const regPNJ = new RegExp("^(repeating_pnjatk_[^_]*_)(.*)$");
characters.forEach(function(c) {
const charId = c.id;
let reg;
const attrType = findObjs({
_type: 'attribute',
_characterid: charId,
name: 'type_personnage'
}, {
caseInsensitive: true
});
if (attrType.length === 0 || attrType[0].get('current') == 'PJ') {
reg = regPJ;
} else {
reg = regPNJ;
}
let attributes = findObjs({
_type: 'attribute',
_characterid: charId,
});
let munitions = {}; //map de nom de munition d'attaques vers attribut
let attaques = {};
attributes.forEach(function(attr) {
const nom = attr.get('name');
if (nom.length > 9 && nom.startsWith('munition_')) {
let fin = nom.substring(9);
munitions[fin] = attr;
return;
}
const m = reg.exec(nom);
if (!m) return;
attaques[m[1]] = attaques[m[1]] || {};
attaques[m[1]][m[2]] = attr;
});
_.forEach(attaques, function(att, pref) {
if (att.armetypeattaque === undefined) return;
if (att.armetypeattaque.get('current') != 'Arme de jet') return;
let options = [];
if (att.armeoptions !== undefined) {
options = att.armeoptions.get('current');
if (options.startsWith('-')) options = ' ' + options;
options = options.split(' --');
}
let typesMunitions = 0;
let current = 1;
let max = 1;
let taux = 0;
let attrMunition;
let optionsSansMunitions = options.filter(function(opt) {
opt = opt.trim();
let cmd = opt.split(' ');
if (cmd.length < 2) return true;
if (cmd[0] != 'munition') return true;
let ma = munitions[cmd[1]];
if (ma === undefined) return true;
attrMunition = ma;
typesMunitions++;
max = parseInt(ma.get('max'));
if (isNaN(max) || max < 0) max = 1;
current = parseInt(ma.get('current'));
if (isNaN(current) || current < 0 || current > max) current = max;
if (cmd.length > 2) {
taux = parseInt(cmd[2]);
if (isNaN(taux) || taux < 0 || taux > 100) taux = 0;
} else {
taux = 100; //La valeur par défaut en option.
}
return false;
});
if (typesMunitions == 1) {
let qte = att.armejetqte;
if (qte === undefined) {
if (current !== 1 || max !== 1) {
createObj('attribute', {
characterid: charId,
name: pref + 'armejetqte',
current: current,
max: max
});
}
} else {
qte.set('current', current);
qte.set('max', max);
}
let at = att.armejettaux;
if (at === undefined) {
if (taux !== 0) {
createObj('attribute', {
characterid: charId,
name: pref + 'armejettaux',
current: taux
});
}
} else {
at.set('current', taux);
}
att.armeoptions.set('current', optionsSansMunitions.join(' --'));
attrMunition.remove();
} else { // On ne sait pas faire la traduction
let mod = att.armemodificateurs;
if (mod === undefined) {
createObj('attribute', {
characterid: charId,
name: pref + 'armemodificateurs',
current: 'retourneEnMain'
});
} else {
mod.set('current', mod.get('current') + ' retourneEnMain');
}
}
});
});
log("Mise à jour des armes de jet effectuée");
}
if (version < 3.03) {
findObjs({
_type: 'attribute',
}).forEach(function(attribute) {
let nom = attribute.get('name');
if (nom != 'armeParDefaut') return;
let charId = attribute.get('characterid');
let arme = attribute.get('current');
attribute.remove();
let {
attr,
predText
} = getPredicateAttr(charId);
predText += 'armeParDefaut';
if (arme.trim() !== '') predText += ':' + arme;
attr.set('current', predText);
});
log("Mise à jour de prédicats effectuée");
}
if (version < 3.04) {
let attrs = findObjs({
_type: 'attribute',
});
if (state.COFantasy.combat) {
attrs.forEach(function(a) {
let nom = a.get('name');
switch (nom) {
case 'resistanceALaMagieBarbare':
case 'esquiveAcrobatique':
case 'paradeMagistrale':
case 'paradeAuBouclier':
updateReset(a, nom, 'Tour');
return;
default:
return;
}
});
}
let predicates = '^(' +
'armureLourdeGuerrier|attaqueEnTraitre|bonusFeinte|chairACanon' +
'|corpsElementaire|devierLesCoups' +
'|esquiveAcrobatique|esquiveFatale|fauchage|frappeDuVide|interchangeable|kiai|pacifisme|pacteSanglant|paradeAuBouclier|paradeMagistrale' +
'|paradeDeProjectiles|petitVeinard|porteurDuBouclierDeGrabuge|prescience|resistanceALaMagie' +
'|resistanceALaMagieBarbare|riposteGuerrier|tirFatal|traquenard|voieOutreTombe' +
'|bonusSaveContre_.*|eclaire_.*' +
')$';
let predicateRegExp = new RegExp(predicates);
let parChar = {};
attrs.forEach(function(attribute) {
let nom = attribute.get('name');
if (predicateRegExp.test(nom)) {
let charId = attribute.get('characterid');
parChar[charId] = parChar[charId] || {};
parChar[charId][nom] = attribute.get('current');
if (nom == 'bonusFeinte') {
let nbDesFeinte = attribute.get('max');
if (nbDesFeinte !== '') parChar[charId].nbDesFeinte = nbDesFeinte;
} else if (nom == 'pacifisme') {
parChar[charId][nom] = attribute.get('max');
} else if (nom == 'traquenard') {
parChar[charId][nom] = 'true';
} else if (nom == 'fauchage') {
let tailleFauchage = attribute.get('max');
if (tailleFauchage !== '')
parChar[charId].tailleFauchage = tailleFauchage;
} else if (nom.startsWith('eclaire_')) {
let eclaireFaible = attribute.get('max');
if (eclaireFaible !== '') {
let nomFaible = nom.replace('eclaire_', 'eclaireFaible_');
parChar[charId][nomFaible] = eclaireFaible;
}
}
attribute.remove();
}
if (nom.startsWith('charge_')) {
let charId = attribute.get('characterid');
parChar[charId] = parChar[charId] || {};
parChar[charId][nom] = attribute.get('current');
//On n'efface pas, l'attribut reste pour la valeur courante de charge
} else if (nom == 'defierLaMort') {
let charId = attribute.get('characterid');
parChar[charId] = parChar[charId] || {};
parChar[charId][nom] = 'true';
}
});
for (let charId in parChar) {
let pred = parChar[charId];
let {
attr,
predText
} = getPredicateAttr(charId);
for (let p in pred) {
let val = pred[p];
if (val === 'true' || val === '' || val == 1) predText += p + ' ';
else predText += p + ':' + val + ' ';
}
attr.set('current', predText);
}
log("Transformation d'attributs de combat en prédicats");
}
if (version < 3.05) {
let attrs = findObjs({
_type: 'attribute',
});
attrs.forEach(function(a) {
let name = a.get('name');
if (name == 'pnj_armure' || name == 'pnj_bouclier') a.remove();
});
log("Suppression des attributs pnj d'armure et de bouclier");
}
if (version < 3.07) {
//Collecte des persos ayant un prédicat charge_ ou eclaire_
let charIds = {};
let attrs = findObjs({
_type: 'attribute',
});
const change = /(charge_|eclaire_|initEnMain)/;
attrs.forEach(function(a) {
let n = a.get('name');
if (n.match(/^repeating_(armes|pnjatk)_[^_]*_armespec$/)) {
let spec = a.get('current');
if (spec.includes('DEF ')) {
spec = spec.replace(/\bDEF\b\s*\+/g, 'DEF:');
a.set('current', spec);
}
return;
}
if (n == 'armeBonusDef' || n == 'armeGaucheBonusDef') {
a.remove();
return;
}
if (n == 'predicats_script') {
let pred = a.get('current');
if (pred === '') return;
if (change.test(pred)) {
charIds[a.get('characterid')] = pred;
}
}
});
let setSpecial = function(nom, pred, predicates, attaques, charId) {
let l = nom.length + 1;
let label = pred.substring(l);
let att = attaques[label];
if (att === undefined) return;
let special = att.armespec;
if (special === undefined || special === '') special = nom;
else special += ' ' + nom;
let valeur = parseInt(predicates[pred]);
if (!isNaN(valeur)) special += ':' + valeur;
att.armespec = special;
let attrName = att.prefixe + 'armespec';
let attr = findObjs({
_type: 'attribute',
_characterid: charId,
name: attrName
});
if (attr.length > 0) {
attr[0].set('current', special);
} else {
createObj('attribute', {
name: attrName,
current: special,
characterid: charId
});
}
};
for (let charId in charIds) {
let predicates = predicateOfRaw(charIds[charId]);
let perso = {
charId
};
const attaques = listAllAttacks(perso);
let initEnMainToDo = true;
for (let pred in predicates) {
if (pred.startsWith('charge_')) {
setSpecial('charge', pred, predicates, attaques, charId);
} else if (pred.startsWith('eclaire_')) {
setSpecial('eclaire', pred, predicates, attaques, charId);
} else if (pred.startsWith('eclaireFaible_')) {
setSpecial('eclaireFaible', pred, predicates, attaques, charId);
} else if (initEnMainToDo && pred.startsWith('initEnMain')) {
let label = pred.substring(10);
let att = attaques[label];
if (att === undefined) continue;
let newPred = 'plusViteQueSonOmbre:';
let arme = getWeaponStats(perso, label);
if (!arme.poudre && arme.arbalete) newPred += 'arbalete';
let valeur = parseInt(predicates[pred]);
if (isNaN(valeur) || valeur == 10) {
if (newPred.endsWith(':')) newPred = newPred.substring(0, newPred.length - 1);
} else {
newPred += valeur;
}
let {
attr,
predText
} = getPredicateAttr(charId);
predText = predText.replace(/initEnMain.*:\s*\d+\b/, newPred);
predText = predText.replace(/initEnMain.*:\s*\d+\b/g, '');
attr.set('current', predText);
initEnMainToDo = false;
}
}
}
log("Copie des prédicats de charge et éclairage vers les attaques des armes");
}
if (version < 3.08) {
let attrs = findObjs({
_type: 'attribute',
});
attrs.forEach(function(a) {
let n = a.get('name');
if (!n.startsWith('poisonRapide_')) return;
let cur = 'rapide ' + a.get('current');
let nouveauNom = 'enduitDePoison' + n.substring(12);
a.set('name', nouveauNom);
a.set('current', cur);
});
log("Changement de nom d'attributs pour les armes enduites de poison");
}
if (version < 3.09) {
if (state.COFantasy.combat === true) {
state.COFantasy.combat = {
pageId: state.COFantasy.combat_pageid,
};
}
state.COFantasy.combat_pageid = undefined;
state.COFantasy.activeTokenId = undefined;
state.COFantasy.activeTokenName = undefined;
state.COFantasy.tour = undefined;
state.COFantasy.init = undefined;
state.COFantasy.usureOff = undefined;
}
if (version < 3.10) {
let attrs_preds = findObjs({
_type: 'attribute',
name: 'predicats_script'
});
attrs_preds.forEach(function(a) {
let preds = a.get('current');
if (preds.includes('laissez-le-moi')) {
preds = preds.replace('laissez-le-moi', 'laissezLeMoi');
a.set('current', preds);
}
});
log("Changement du prédicat laissez-le-moi");
}
if (version < 3.11) {
//aura sera passé en actions plus tard
let predicates = '^(' +
'attributsDeStatut|chairACanonDe|defDeriveeDe|dmSiToucheContact' +
'|ecuyerDe|ennemiJure|entrerEnCombatAvec|familier|guetteur' +
'|initiativeDeriveeDe|lienEpique|messageSiCritique|montureDe' +
'|PVPartagesAvec|surveillance|tokenFormeDArbre|vitaliteSurnaturelle' +
')$';
let predicateRegExp = new RegExp(predicates);
let parChar = {};
findObjs({
_type: 'attribute',
}).forEach(function(attribute) {
let nom = attribute.get('name');
if (predicateRegExp.test(nom)) {
let charId = attribute.get('characterid');
parChar[charId] = parChar[charId] || getPredicateAttr(charId);
let text = parChar[charId].predText;
text += nom + '::' + attribute.get('current');
let m = attribute.get('max');
if (m) {
switch (nom) {
case 'vitaliteSurnaturelle':
text += '/' + m;
break;
case 'dmSiToucheContact':
text += ' --' + m;
break;
default:
log("Attribut " + nom + " avec un champ max " + m);
text += '\n// max: ' + m;
}
}
parChar[charId].predText = text + '\n';
attribute.remove();
}
});
for (let charId in parChar) {
parChar[charId].attr.set('current', parChar[charId].predText);
}
log("Transformation des attributs complexes en prédicats");
}
if (version < 3.12) {
if (state.COFantasy.foudresDuTemps) {
state.COFantasy.foudresDuTemps = undefined;
sendChat('COF', "/w GM Fin des foudres du temps.");
}
}
if (version < 3.13) {
let evt = {
type: "version 3.13"
};
characters.forEach(function(c) {
let perteDeSubstance = charPredicateAsBool(c.id, 'perteDeSubstance');
if (!perteDeSubstance) return;
perteDeSubstance = toInt(perteDeSubstance, 0);
if (!perteDeSubstance) return;
let perso = {
charId: c.id
};
setTokenAttr(perso, 'perteDeSubstance', perteDeSubstance, evt, {
charAttr: true
});
});
log("Mise à jour effectuée");
}
if (version < 3.14) {
let evt = {
type: "version 3.14",
attributes: [],
};
let charsWithGachette = new Set();
let attrs = findObjs({
_type: 'attribute'
});
attrs.forEach(function(attr) {
let n = attr.get('name');
if (n.match(/^repeating_(armes|pnjatk)_[^_]*_armeoptions/)) {
let c = attr.get('current');
if (c && c.includes('--asDeLaGachette')) {
evt.attributes.push({
attribute: attr,
current: c
});
let cid = attr.get('characterid');
charsWithGachette.add(cid);
c = c.replace(/--asDeLaGachette/g, '').trim();
attr.set('current', c);
}
} else if (n.match(/^repeating_(armes|pnjatk)_[^_]*_armemodificateurs/)) {
let c = attr.get('current');
if (c && c.includes('asDeLaGachette')) {
evt.attributes.push({
attribute: attr,
current: c
});
let cid = attr.get('characterid');
charsWithGachette.add(cid);
c = c.replace(/asDeLaGachette/g, '').trim();
attr.set('current', c);
}
}
});
charsWithGachette.forEach(function(charId) {
let predicats = ficheAttribute({
charId
}, 'predicats_script', '');
if (predicats === '') predicats = 'asDeLaGachette';
else predicats += '\nasDeLaGachette';
setFicheAttr({
charId
}, 'predicats_script', predicats, evt);
});
if (charsWithGachette.size > 0)
log("Mise à jour des prédicats asDeLaGachette effectuée.");
}
if (version < 3.15) {
const evt = {
type: "version 3.14",
};
removeAllAttributes('armeEnMain', evt);
log("Suppression des attributs obsolètes d'arme en main");
}
}
function changePredicats(attr, prev) {
let curPred = attr.get('current');
let prevPred = prev.current;
if (curPred.includes('attaqueEnMeute') != prevPred.includes('attaqueEnMeute')) {
recomputeAllies();
}
}
return {
apiCommand,
nextTurn,
destroyToken,
moveToken,
changeHandout,
addToken,
changeMarker,
changeTokenLock,
changePlayerPage,
setStateCOF,
scriptVersionToCharacter,
changeDoor,
updateVersion,
changePredicats,
};
}();
on('ready', function() {
const scriptVersion = '3.15';
on('add:token', COFantasy.addToken);
on("change:campaign:playerpageid", COFantasy.changePlayerPage);
state.COFantasy = state.COFantasy || {
combat: false,
eventId: 0,
version: scriptVersion,
};
COFantasy.setStateCOF();
const characters = findObjs({
_type: 'character',
});
COFantasy.updateVersion(state.COFantasy.version, characters);
state.COFantasy.version = scriptVersion;
const handout = findObjs({
_type: 'handout'
});
handout.forEach(function(hand) {
COFantasy.changeHandout(hand);
});
//Vérification de la version sur les fiches
characters.forEach(function(c) {
COFantasy.scriptVersionToCharacter(c, 11);
});
COF_loaded = true;
let load_msg = "COFantasy " + scriptVersion;
if (COF_BETA) load_msg += ' beta';
log(load_msg + " loaded");
});
on("chat:message", function(msg) {
"use strict";
if (COF_loaded && msg.type == "api" && msg.content.startsWith('!cof-')) {
if (COF_BETA) {
COFantasy.apiCommand(msg);
} else {
try {
COFantasy.apiCommand(msg);
} catch (e) {
sendChat('COF', "Erreur durant l'exécution de " + msg.content);
log("Erreur durant l'exécution de " + msg.content);
log(msg);
let errMsg = e.name;
if (e.lineNumber) errMsg += " at " + e.lineNumber;
else if (e.number) errMsg += " at " + e.number;
errMsg += ': ' + e.message;
sendChat('COF', errMsg);
log(errMsg);
}
}
}
});
on("change:handout", COFantasy.changeHandout);
on("destroy:handout", function(prev) {
COFantasy.changeHandout(undefined, prev);
});
on("change:campaign:turnorder", COFantasy.nextTurn);
on('destroy:token', COFantasy.destroyToken);
on('change:graphic:lockMovement', COFantasy.changeTokenLock);
on('change:graphic:statusmarkers', COFantasy.changeMarker);
on('change:token', COFantasy.moveToken);
on('add:character', function(c) {
if (COF_loaded) {
COFantasy.scriptVersionToCharacter(c);
}
});
on("change:door:isOpen", COFantasy.changeDoor);
on("change:attribute", function(attr, prev) {
if (!COF_loaded) return;
let predicats = state.COFantasy.predicats;
if (!predicats) return;
let n = attr.get("name");
if (n == 'predicats_script' || n.includes('armepredicats') || n.includes('effetarmure') || n == 'maindroite' || n == 'maingauche' || n == 'torseequipe' || n == 'teteequipe') {
predicats[attr.get('characterid')] = undefined;
COFantasy.changePredicats(attr, prev);
}
});