// jshint undef:true
// jshint eqeqeq:false
/* globals Set */
/* globals getAttrByName */
/* globals findObjs */
/* globals _ */
/* globals createObj */
/* globals log */
/* globals sendChat */
/* globals state */
/* globals Campaign */
/* globals getObj */
/* globals randomInteger */
/* globals spawnFx */
/* globals spawnFxBetweenPoints */
/* globals VecMath */
/* globals on */
/* globals toFront */
/* globals playerIsGM */
// Needs the Vector Math scripty
var COF_loaded = false;
var COFantasy = COFantasy || function() {
"use strict";
var DEF_MALUS_APRES_TOUR_5 = true;
var IMAGE_OMBRE = "https://s3.amazonaws.com/files.d20.io/images/2781735/LcllgIHvqvu0HAbWdXZbJQ/thumb.png?13900368485";
var IMAGE_DOUBLE = "https://s3.amazonaws.com/files.d20.io/images/33854984/q10B3KtWsCxcMczLo4BSUw/thumb.png?1496303265";
var HISTORY_SIZE = 150;
var eventHistory = [];
var updateNextInitSet = new Set();
var 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;';
var BS_LABEL_DEFAULT = 'background-color: #777;';
var BS_LABEL_PRIMARY = 'background-color: #337ab7;';
var BS_LABEL_SUCCESS = 'background-color: #5cb85c;';
var BS_LABEL_INFO = 'background-color: #5bc0de;';
var BS_LABEL_WARNING = 'background-color: #f0ad4e;';
var BS_LABEL_DANGER = 'background-color: #d9534f;';
var bs_alert = 'padding: 5px; border: 1px solid transparent; border-radius: 4px;';
var bs_alert_success = 'color: #3c763d; background-color: #dff0d8; border-color: #d6e9c6;';
var bs_alert_danger = 'color: #a94442; background-color: #f2dede; border-color: #ebccd1;';
var aura_token_on_turn = false;
// List of states:
var cof_states = {
mort: 'status_dead',
surpris: 'status_lightning-helix',
assome: 'status_pummeled',
renverse: 'status_back-pain',
aveugle: 'status_bleeding-eye',
affaibli: 'status_half-heart',
etourdi: 'status_half-haze',
paralyse: 'status_fishing-net',
ralenti: 'status_snail',
endormi: 'status_sleepy',
apeure: 'status_screaming'
};
function etatRendInactif(etat) {
var res =
etat == 'mort' || etat == 'surpris' || etat == 'assome' ||
etat == 'etourdi' || etat == 'paralyse' || etat == 'endormi' ||
etat == 'apeure';
return res;
}
function error(msg, obj) {
log(msg);
log(obj);
sendChat("COFantasy", msg);
}
// retourne un tableau contenant la liste des ID de joueurs controlant le personnage lié au Token
function getPlayerIds(perso) {
var player_ids = [];
var character = getObj('character', perso.charId);
var char_controlledby = character.get('controlledby');
if (char_controlledby === '') return player_ids;
_.each(char_controlledby.split(","), function(controlledby) {
var player = getObj('player', controlledby);
if (player === undefined) return;
player_ids.push(controlledby);
});
return player_ids;
}
function get_picto_style_from_command(full_command, tokenId) {
var style = '';
var picto_style = {
picto: '',
style: style
};
if (full_command === undefined) return picto_style;
var picto = '';
var command = full_command.split(' ');
// Pictos : https://wiki.roll20.net/CSS_Wizardry#Pictos
switch (command[0]) {
case "#Attaque":
case "!cof-attack":
case "!cof-attaque":
var portee = 0;
if (command[1] !== undefined && command[3] !== undefined) {
var attackLabel = command[3];
var this_weapon = [];
try {
this_weapon = JSON.parse(attackLabel);
} catch (e) {
log('Erreur parsing the Array');
}
if (Array.isArray(this_weapon)) {
portee = this_weapon[4];
} else {
var token = getObj('graphic', tokenId);
if (token !== undefined) {
var att = getAttack(attackLabel, token.get('name'), token.get('represents'));
if (att !== undefined) {
portee = getPortee(token.get('represents'), att.attackPrefix);
}
}
}
}
if (full_command.indexOf('-sort') !== -1 || full_command.indexOf('-magic') !== -1 || full_command.indexOf('-magique') !== -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 "!cof-lancer-sort":
picto = 'g ';
style = 'background-color:#9900ff';
break;
case "!cof-soin":
case "!cof-transe-guerison":
picto = 'k ';
style = 'background-color:#ffe599;color:#333';
break;
case "!cof-effet":
case "!cof-effet-temp":
case "!cof-effet-combat":
picto = 'S ';
style = 'background-color:#4a86e8';
break;
case "!cof-enduire-poison":
picto = 'i ';
style = 'background-color:#05461c';
break;
case "!cof-surprise":
picto = 'e ';
style = 'background-color:#4a86e8';
break;
case "!cof-recharger":
picto = '0 ';
style = 'background-color:#e69138';
break;
case "!cof-action-defensive":
picto = 'b ';
style = 'background-color:#cc0000';
break;
case "!cof-attendre":
picto = 't ';
style = 'background-color:#999999';
break;
case "!cof-aoe": //deprecated
case "!cof-dmg":
picto = '\' ';
style = 'background-color:#cc0000';
break;
case "!cof-peur":
picto = '` ';
style = 'background-color:#B445FE';
break;
case "!cof-consommables":
picto = 'b ';
style = 'background-color:#ce0f69';
break;
default:
picto = '';
style = '';
break;
}
picto_style.picto = picto;
picto_style.style = style;
return picto_style;
}
function getState(personnage, etat) {
var token = personnage.token;
var charId = personnage.charId;
var res = false;
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');
}
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
var de = parseInt(getAttrByName(charId, 'ETATDE'));
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;
}
}
var 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;
}
function affectToken(token, field, value, evt) {
evt.affectes = evt.affectes || {};
var 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 setState(personnage, etat, value, evt) {
var token = personnage.token;
var charId = personnage.charId;
var aff =
affectToken(token, 'statusmarkers', token.get('statusmarkers'), evt);
if (value && etatRendInactif(etat) && isActive(personnage))
removeFromTurnTracker(token.id, evt);
token.set(cof_states[etat], value);
if (etat == 'aveugle') {
// We also change vision of the token
if (aff.prev.light_losangle === undefined)
aff.prev.light_losangle = token.get('light_losangle');
if (value) token.set('light_losangle', 0);
else token.set('light_losangle', 360);
} else if (value && etat == 'mort') {
var allToks =
findObjs({
_type: "graphic",
_pageid: token.get('pageid'),
_subtype: "token",
layer: "objects"
});
allToks.forEach(function(tok) {
if (tok.id == token.id) return;
var ci = tok.get('represents');
if (ci === '') return;
var p = {
token: tok,
charId: ci
};
if (getState(p, 'mort')) return;
if (charAttributeAsBool(ci, 'siphonDesAmes')) {
soigneToken(p, randomInteger(6), evt,
function(s) {
sendChar(ci, "siphone l'âme de " + token.get('name') +
". Il récupère " + s + " pv.");
},
function() {
sendChar(ci, "est déjà au maximum de point de vie. Il laisse échapper l'âme de " + token.get('name'));
});
}
});
}
if (token.get('bar1_link') !== "") {
if (charId === '') {
error("token with a linked bar1 but representing no character", token);
return;
}
if (etat == 'affaibli') { //special case due to new character sheet
var attr =
findObjs({
_type: 'attribute',
_characterid: charId,
name: 'ETATDE'
});
if (value) {
if (attr.length === 0) {
attr =
createObj('attribute', {
characterid: charId,
name: 'ETATDE',
current: 12
});
if (_.has(evt, 'attributes'))
evt.attributes.push({
attribute: attr,
current: null
});
else evt.attributes = [{
attribute: attr,
current: null
}];
} else {
attr = attr[0];
if (parseInt(attr.get('current')) != 12) {
if (_.has(evt, 'attributes'))
evt.attributes.push({
attribute: attr,
current: 20
});
else evt.attributes = [{
attribute: attr,
current: 20
}];
attr.set('current', 12);
}
}
} else {
if (attr.length > 0) {
attr = attr[0];
if (parseInt(attr.get('current')) != 20) {
if (_.has(evt, 'attributes'))
evt.attributes.push({
attribute: attr,
current: 12
});
else evt.attributes = [{
attribute: attr,
current: 12
}];
attr.set('current', 20);
}
}
}
} else {
var attrEtat =
findObjs({
_type: 'attribute',
_characterid: charId,
name: etat
});
if (value) {
if (attrEtat.length === 0) {
attrEtat =
createObj('attribute', {
characterid: charId,
name: etat,
current: value
});
if (_.has(evt, 'attributes'))
evt.attributes.push({
attribute: attrEtat,
current: null
});
else evt.attributes = [{
attribute: attrEtat,
current: null
}];
}
} else {
if (attrEtat.length > 0) {
attrEtat[0].remove();
if (_.has(evt, 'deletedAttributes')) {
evt.deletedAttributes.push(attrEtat[0]);
} else {
evt.deletedAttributes = [attrEtat[0]];
}
}
}
}
}
if (!value && etatRendInactif(etat) && isActive(personnage) ||
etat == 'aveugle') updateInit(token, evt);
}
function logEvents() {
var l = eventHistory.length;
log("Historique de taille " + l);
eventHistory.forEach(function(evt, i) {
log("evt " + i);
log(evt);
});
}
function addEvent(evt) {
evt.id = state.COFantasy.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() {
var l = eventHistory.length;
if (l === 0) return undefined;
return eventHistory[l - 1];
}
//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 === null) attr.attribute.remove();
else {
attr.attribute.set('current', attr.current);
if (attr.max) attr.attribute.set('max', attr.max);
}
});
}
// deletedAttributes have a quadratic cost in the size of the history
if (evt.deletedAttributes) {
evt.deletedAttributes.forEach(function(attr) {
var oldId = attr.id;
var nameDel = attr.get('name');
log("Restoring attribute " + nameDel);
var newAttr =
createObj('attribute', {
characterid: 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(attr) {
if (attr.attribute.id == oldId) attr.attribute = newAttr;
});
}
});
});
}
if (_.has(evt, 'combat')) state.COFantasy.combat = evt.combat;
if (_.has(evt, 'combat_pageid')) state.COFantasy.combat_pageid = evt.combat_pageid;
if (_.has(evt, 'tour')) state.COFantasy.tour = evt.tour;
if (_.has(evt, 'init')) state.COFantasy.init = evt.init;
if (_.has(evt, 'activeTokenId')) state.COFantasy.activeTokenId = evt.activeTokenId;
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);
return;
}
function undoTokenEffect(evt) {
_.each(evt.affectes, function(aff) {
var prev = aff.prev;
var tok = aff.affecte;
if (prev === undefined || tok === undefined) {
error("Pas d'état précédant", aff);
return;
}
_.each(prev, function(val, key) {
tok.set(key, val);
});
sendChat("COF", "État de " + tok.get("name") + " restauré.");
});
}
function caracOfMod(m) {
switch (m) {
case 'FOR':
return 'FORCE';
case 'DEX':
return 'DEXTERITE';
case 'CON':
return 'CONSTITUTION';
case 'INT':
return 'INTELLIGENCE';
case 'SAG':
return 'SAGESSE';
case 'CHA':
return 'CHARISME';
default:
return;
}
}
function modCarac(perso, carac) {
var res = Math.floor((charAttributeAsInt(perso, carac, 10) - 10) / 2);
return res;
}
//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 tokenOfId(id, name, pageId) {
var token = getObj('graphic', id);
if (token === undefined) {
if (name === undefined) return undefined;
if (pageId === undefined) {
pageId = Campaign().get('playerpageid');
}
var tokens = findObjs({
_type: 'graphic',
_subtype: 'token',
_pageid: pageId,
name: name
});
if (tokens.length === 0) 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];
}
var 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
};
}
// on, remplace tous les selected par @{character name|attr}
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
}
//ressource est optionnel, et si présent doit être un attribut
function bouton(action, text, perso, ressource, button_style, button_title) {
if (action === undefined || action.trim().length === 0) return text;
else action = action.trim();
if (action.indexOf('#') !== -1) {
// Cette commande contient au moins une macro donc on va le remplacer par sa commande (action)
// Toutes les Macros
var macros = findObjs({
_type: 'macro'
});
var command_words = action.split("\n");
//chaque ligne
_.each(command_words, function(line, j) {
var line_words = line.split(' ');
//chaque mot
_.each(line_words, function(word, k) {
// si le mot commence par #
if (word.startsWith('#')) {
// on recherche une macro qui s'appelle pareil (sans le #)
var this_macro = macros.filter(function(obj) {
return (obj.get('name').trim() == word.substring(1));
});
if (this_macro.length == 1) {
// macro trouvé
// on replace dans la commande initiale
action = action.replace(word, this_macro[0].get('action'));
}
}
});
});
}
var token = perso.token;
var character = getObj('character', perso.charId);
switch (action.charAt(0)) {
case '!':
if (!action.startsWith('!
') && ressource) action += " --decrAttribute " + ressource.id;
break;
default:
if (ressource) {
action = "!cof-utilise-consommable " + token.id + " " + ressource.id + " --message " + action;
} else {
action = "!cof-lancer-sort 0 " + action;
}
}
var picto_style = get_picto_style_from_command(action, token.id);
if (action.indexOf('@{selected') !== -1 && character !== undefined) {
// cas spécial pour @{selected|token_id} où l'on remplace toutes les occurences par token.id
action = action.replace(new RegExp(escapeRegExp('@{selected|token_id}'), 'g'), token.id);
var tmp = action.split('@{selected');
_.each(tmp, function(elem, i) {
if (elem.startsWith('|')) {
// attribut demandé
var attribute_name = elem.substring(0, elem.indexOf("}")).substr(1);
action = action.replace(new RegExp(escapeRegExp('@{selected|' + attribute_name + '}'), 'g'), '@{' + character.get('name') + '|' + attribute_name + '}');
}
});
}
if (action.indexOf('@{target|') == -1 &&
action.indexOf('cof-lancer-sort') == -1 &&
action.indexOf('cof-surprise') == -1 &&
action.indexOf('cof-attack') == -1) {
//Si on n'a pas de cible, on fait comme si le token était sélectionné.
var add_token = " --target " + token.id;
if (action.indexOf(' --message ') != -1) action = action.replace(' --message ', add_token + ' --message ');
else action += add_token;
}
text = picto_style.picto + text;
button_style = ' style="' + picto_style.style + '"';
if (button_title !== undefined) button_title = ' title="' + button_title + '"';
else button_title = '';
action = action.replace(/%/g, '%').replace(/\)/g, ')').replace(/\?/g, '?').replace(/@/g, '@').replace(/\[/g, '[').replace(/]/g, ']').replace(/"/g, '"');
action = action.replace(/\'/g, '''); // escape quotes
return '' + text + '';
}
function jetPerso(perso, caracteristique, difficulte, titre, playerId, options) {
options = options || {};
var evt = options.evt || {
type: "Jet de " + caracteristique
};
var display = startFramedDisplay(playerId, titre, perso);
if (difficulte === undefined) {
jetCaracteristique(perso, caracteristique, options, function(rt) {
addLineToFramedDisplay(display, "Résultat : " + rt.texte);
addStatistics(playerId, ["Jet de carac", caracteristique], rt.roll);
// Maintenant, on diminue la malédiction si le test est un échec
var attrMalediction = tokenAttribute(perso, 'malediction');
if (attrMalediction.length > 0) {
if (rt.echecCritique)
diminueMalediction(perso, evt, attrMalediction);
else if (!rt.critique) {
var action = "!cof-resultat-jet " + state.COFantasy.eventId;
var ligne = "L'action est-elle ";
ligne += bouton(action + " reussi", "réussie", perso);
ligne += " ou " + bouton(action + " rate", "ratée", perso);
ligne += " ?";
addLineToFramedDisplay(display, ligne);
evt.personnage = perso;
evt.attenteResultat = true;
}
}
addEvent(evt);
sendChat('', endFramedDisplay(display));
});
} else {
if (options.chance) options.bonus = options.chance * 10;
testCaracteristique(perso, caracteristique, difficulte, options, evt,
function(tr) {
addLineToFramedDisplay(display, "Résultat : " + tr.texte);
addEvent(evt);
if (tr.reussite) {
addLineToFramedDisplay(display, "C'est réussi.");
} else {
var msgRate = "C'est raté.";
evt.personnage = perso;
evt.action = {
caracteristique: caracteristique,
difficulte: difficulte,
titre: titre,
playerId: playerId,
options: options
};
evt.type = 'jetPerso';
var pc = attributeAsInt(perso, 'PC', 0);
if (pc > 0) {
options.roll = options.roll || tr.roll;
msgRate += ' ' +
bouton("!cof-bouton-chance " + evt.id, "Chance", perso) +
" (reste " + pc + " PC)";
}
if (charAttributeAsBool(perso, 'runeDEnergie') && (caracteristique == 'FOR' || caracteristique == 'CON' || caracteristique == 'DEX')) {
msgRate += ' ' + bouton("!cof-bouton-rune-energie " + evt.id, "Rune d'énergie", perso);
}
addLineToFramedDisplay(display, msgRate);
}
sendChat('', endFramedDisplay(display));
});
}
}
function jet(msg) {
// Les arguments pour cof-jet sont :
// - Caracteristique (FOR, DEX, CON, INT, SAG, CHA)
// Les tokens sélectionnés sont ceux qui doivent faire le jet
var opts = msg.content.split(' --');
var cmd = opts.shift().split(' ');
if (cmd.length < 2) {
error("Pas assez d'arguments pour !cof-jet: " + msg.content, cmd);
return;
}
var caracteristique = cmd[1];
if (isNotCarac(caracteristique)) {
error("Caracteristique '" + caracteristique + "' non reconnue (FOR, DEX, CON, INT, SAG, CHA).", cmd);
return;
}
var difficulte;
if (cmd.length > 2) {
difficulte = parseInt(cmd[2]);
if (isNaN(difficulte)) difficulte = undefined;
}
var options = {
bonusAttrs: []
};
opts.forEach(function(o) {
var args = o.split(' ');
switch (args[0]) {
case "nom":
if (args.length < 2) {
error("Il manque un argument à l'option " + args[0], opts);
return;
}
options.nom = args[1];
options.bonusAttrs.push(args[1].toLowerCase());
return;
case "attribut":
if (args.length < 2) {
error("Il manque un argument à l'option " + args[0], opts);
return;
}
options.bonusAttrs.push(args[1]);
return;
case 'bonus':
if (args.length < 2) {
error("Il manque un argument à l'option " + args[0], opts);
return;
}
var bonus = parseInt(args[1]);
if (isNaN(bonus)) {
error("Le bonus doit être un nombre", opts);
return;
}
options.bonus = (options.bonus || 0) + bonus;
return;
}
});
var titre = "Jet de ";
if (options.nom) titre += options.nom;
else titre += caracOfMod(caracteristique);
titre += "";
if (options.bonus)
titre += " (" + ((options.bonus > 0) ? '+' : '') + options.bonus + ")";
if (difficulte !== undefined) titre += " difficulté " + difficulte;
getSelected(msg, function(selected) {
if (selected.length === 0) {
sendPlayer(msg, "!cof-jet sans sélection de token");
log("!cof-jet requiert de sélectionner des tokens");
return;
}
iterSelected(selected, function(perso) {
jetPerso(perso, caracteristique, difficulte, titre, msg.playerid, options);
}); //fin de iterSelected
}); //fin de getSelected
}
function resultatJet(msg) {
var args = msg.content.split(' ');
if (args.length < 3) {
error("La fonction !cof-resultat-jet n'a pas assez d'arguments", args);
return;
}
var evt = findEvent(args[1]);
if (evt === undefined) {
error("Le jet est trop ancien ou éte annulé", args);
return;
}
if (evt.personnage === undefined) {
error("Erreur interne ", evt);
return;
}
if (evt.attenteResultat) {
var message = evt.type + " ";
if (args[2] == 'rate') {
diminueMalediction(evt.personnage, evt);
message += "raté.";
} else message += "réussi.";
sendChar(evt.personnage.charId, message);
evt.attenteResultat = undefined;
} else {
sendPlayer(msg, "Résultat déjà décidé");
}
}
function parseAttack(msg) {
// Arguments to cof-attack should be:
// - attacking token
// - target token
// - attack number (referring to the character sheet)
// - some optional arguments, preceded by --
var optArgs = msg.content.split(" --");
var args = optArgs[0].split(" ");
optArgs.shift();
if (args.length < 4) {
error("Pas assez d'arguments pour !cof-attack: " + msg.content, args);
return;
}
var attaquant = tokenOfId(args[1]);
if (attaquant === undefined) {
error("Le premier argument de !cof-attack n'est pas un token valide" + msg.content, args[1]);
return;
}
var targetToken = getObj("graphic", args[2]);
if (targetToken === undefined) {
error("le second argument de !cof-attack doit être un token" + msg.content, args[2]);
return;
}
var attackLabel = args[3];
// Optional arguments
var options = {
additionalDmg: []
};
var lastEtat; //dernier de etats et effets
optArgs.forEach(function(arg) {
arg = arg.trim();
var cmd = arg.split(" ");
if (cmd.length === 0) cmd = [arg];
switch (cmd[0]) {
case "auto":
case "tempDmg":
case "poudre":
case "strigeSuce":
case "semonce":
case "pointsVitaux":
case "pressionMortelle":
case "reroll1":
case "reroll2":
case "tirDouble":
case "tranchant":
case "percant":
case "contondant":
case "m2d20":
case "avecd12":
case "traquenard":
case "affute":
case "vampirise":
case "mainsDEnergie":
case "tirDeBarrage":
case "ignoreObstacles":
case "enflamme":
case "asDeLaGachette":
case "sortilege":
case "malediction":
case "test":
case "argent":
case "pietine":
case "tueurDeGeants":
case "demiAuto":
case "feinte":
case "artificiel":
options[cmd[0]] = true;
return;
case "imparable": //deprecated
options.m2d20 = true;
return;
case "magique":
var niveauMagie = 1;
if (cmd.length > 1) {
niveauMagie = parseInt(cmd[1]);
if (isNaN(niveauMagie) || niveauMagie < 1) {
error("Le niveau de magie doit être au moins 1", cmd);
niveauMagie = 1;
}
}
options.magique = niveauMagie;
return;
case "si":
options.conditionAttaquant = parseCondition(cmd.slice(1));
return;
case "plus":
if (cmd.length < 2) {
sendChat("COF", "Il manque un argument à l'option --plus de !cof-attack");
return;
}
var val = arg.substring(arg.indexOf(' ') + 1);
options.additionalDmg.push({
value: val
});
break;
case "effet":
if (cmd.length < 3) {
error("Il manque un argument à l'option --effet de !cof-attack", cmd);
return;
}
if (!estEffetTemp(cmd[1])) {
error(cmd[1] + " n'est pas un effet temporaire répertorié", cmd);
return;
}
var duree;
duree = parseInt(cmd[2]);
if (isNaN(duree) || duree < 1) {
error(
"Le deuxième argument de --effet doit être un nombre positif",
cmd);
return;
}
options.effets = options.effets || [];
lastEtat = {
effet: cmd[1],
duree: duree
};
options.effets.push(lastEtat);
return;
case "etatSi":
case "etat":
if (cmd.length < 3 && cmd[0] == 'etatSi') {
error("Il manque un argument à l'option --etatSi de !cof-attack", cmd);
return;
} else if (cmd.length < 2) {
error("Il manque un argument à l'option --etat de !cof-attack", cmd);
return;
}
var etat = cmd[1];
if (!_.has(cof_states, etat)) {
error("Etat non reconnu", cmd);
return;
}
var condition = 'toujoursVrai';
if (cmd[0] == 'etatSi') {
condition = parseCondition(cmd.slice(2));
if (condition === undefined) return;
}
options.etats = options.etats || [];
lastEtat = {
etat: etat,
condition: condition
};
options.etats.push(lastEtat);
return;
case "peur":
if (cmd.length < 3) {
error("Il manque un argument à l'option --peur de !cof-attack", cmd);
return;
}
options.peur = {
seuil: parseInt(cmd[1]),
duree: parseInt(cmd[2])
};
if (isNaN(options.peur.seuil)) {
error("Le premier argument de --peur doit être un nombre (le seuil)", cmd);
}
if (isNaN(options.peur.duree) || options.peur.duree <= 0) {
error("Le deuxième argument de --peur doit être un nombre positif (la durée)", cmd);
}
return;
case "feu":
case "froid":
case "acide":
case "electrique":
case "sonique":
case "poison":
case "maladie":
var l = options.additionalDmg.length;
if (l > 0) {
options.additionalDmg[l - 1].type = cmd[0];
} else {
options.type = cmd[0];
}
return;
case "nature":
case "naturel":
options.nature = true;
return;
case "sournoise":
case "de6Plus": //deprecated
if (cmd.length < 2) {
sendChat("COF", "Il manque un argument à l'option --sournoise de !cof-attack");
return;
}
if (options.sournoise === undefined) options.sournoise = 0;
options.sournoise += parseInt(cmd[1]);
if (isNaN(options.sournoise) || options.sournoise < 0) {
error("L'option --sournoise de !cof-attack attend un argument entier positif", cmd);
return;
}
break;
case "fx":
if (cmd.length < 2) {
sendChat("COF", "Il manque un argument à l'option --fx de !cof-attack");
return;
}
options.fx = cmd[1];
break;
case "targetFx":
if (cmd.length < 2) {
sendChat("COF", "Il manque un argument à l'option --targetFx de !cof-attack");
return;
}
options.targetFx = cmd[1];
break;
case 'psave':
var psaveopt = options;
if (cmd.length > 3 && cmd[3] == 'local') {
var psavel = options.additionalDmg.length;
if (psavel > 0) {
psaveopt = options.additionalDmg[psavel - 1];
}
}
var psaveParams = parseSave(cmd);
if (psaveParams) {
psaveopt.partialSave = psaveParams;
psaveopt.attaquant = attaquant;
}
return;
case 'save':
if (lastEtat) {
if (lastEtat.save) {
error("Redéfinition de la condition de save pour un effet", optArgs);
}
var saveParams = parseSave(cmd);
if (saveParams) {
lastEtat.save = saveParams;
return;
}
return;
}
error("Pas d'effet auquel appliquer le save", optArgs);
return;
case 'saveParTour':
if (lastEtat) {
if (lastEtat.saveParTour) {
error("Redéfinition de la condition de save pour un effet", optArgs);
}
var saveParTourParams = parseSave(cmd);
if (saveParTourParams) {
lastEtat.saveParTour = saveParTourParams;
return;
}
return;
}
error("Pass d'effet auquel appliquer le save", optArgs);
return;
case "mana":
if (cmd.length < 2) {
error("Usage : --mana coût", cmd);
return;
}
var mana = parseInt(cmd[1]);
if (isNaN(mana) || mana < 1) {
error("Le coût en mana doit être un nombre positif");
return;
}
if (options.mana === undefined) options.mana = 0;
options.mana += mana;
break;
case "bonusAttaque":
case "bonusContreBouclier":
case "bonusCritique":
if (cmd.length < 2) {
error("Usage : --" + cmd[0] + " b", cmd);
return;
}
var bAtt = parseInt(cmd[1]);
if (isNaN(bAtt)) {
error("Le bonus (" + cmd[0] + ") doit être un nombre");
return;
}
if (options[cmd[0]] === undefined) options[cmd[0]] = 0;
options[cmd[0]] += bAtt;
return;
case "puissant":
if (cmd.length < 2) {
options.puissant = true;
return;
}
switch (cmd[1]) {
case 'oui':
case 'Oui':
options.puissant = true;
return;
case 'non':
case 'Non':
options.puissant = false;
return;
default:
options.puissant = attributeAsBool(attaquant, cmd[1] + "Puissant");
}
return;
case "rate":
case "touche":
case "critique":
case "echecCritique":
if (options.triche === undefined) {
options.triche = cmd[0];
} else {
error("Option incompatible", optArgs);
}
return;
case 'munition':
if (cmd.length < 2) {
error("Pour les munitions, il faut préciser le nom", cmd);
return;
}
var tauxPertes = 100; //Par défaut, les munitions sont perdues
if (cmd.length > 2)
tauxPertes = parseInt(cmd[2]);
if (isNaN(tauxPertes) || tauxPertes < 0 || tauxPertes > 100) {
error("Le taux de pertes des munitions doit être un nombre entre 0 et 100");
tauxPertes = 100;
}
options.munition = {
nom: cmd[1],
taux: tauxPertes
};
return;
case "ligne":
if (options.aoe) {
error("Deux options pour définir une aoe", args);
return;
}
options.aoe = {
type: 'ligne'
};
return;
case "disque":
if (options.aoe) {
error("Deux options pour définir une aoe", args);
return;
}
if (cmd.length < 2) {
error("Il manque le rayon du disque", cmd);
return;
}
options.aoe = {
type: 'disque',
rayon: parseInt(cmd[1])
};
if (isNaN(options.aoe.rayon) || options.aoe.disque < 0) {
error("le rayon du disque n'est pas un nombre positif", cmd);
options.aoe = undefined;
}
return;
case "cone":
if (options.aoe) {
error("Deux options pour définir une aoe", args);
return;
}
var angle = 90;
if (cmd.length > 1) {
angle = parseInt(cmd[1]);
if (isNaN(angle) || angle < 0 || angle > 360) {
error("Paramètre d'angle du cone incorrect", cmd);
angle = 90;
}
}
options.aoe = {
type: 'cone',
angle: angle
};
return;
case 'target':
if (cmd.length < 2) {
error("Il manque l'id de la cible", cmd);
return;
}
var targetS = tokenOfId(cmd[1]);
if (targetS === undefined) {
error("Cible supplémentaire invalide", cmd);
return;
}
if (targetToken.id == targetS.token.id) return;
targetS.tokName = targetS.token.get('name');
options.ciblesSupplementaires = options.ciblesSupplementaires || [];
options.ciblesSupplementaires.push(targetS);
return;
case 'limiteParJour':
if (cmd.length < 2) {
error("Il manque la limite journalière", cmd);
return;
}
var limiteParJour = parseInt(cmd[1]);
if (isNaN(limiteParJour) || limiteParJour < 1) {
error("La limite journalière doit être un nombre positif", cmd);
return;
}
options.limiteParJour = limiteParJour;
if (cmd.length > 2) {
options.limiteParJourRessource = cmd[2];
}
return;
case 'limiteParCombat':
if (cmd.length < 2) {
options.limiteParCombat = 1;
return;
}
var limiteParCombat = parseInt(cmd[1]);
if (isNaN(limiteParCombat) || limiteParCombat < 1) {
error("La limite par combat doit être un nombre positif", cmd);
return;
}
options.limiteParCombat = limiteParCombat;
if (cmd.length > 2) {
options.limiteParCombatRessource = cmd[2];
}
return;
case 'decrAttribute':
if (cmd.length < 2) {
error("Erreur interne d'une commande générée par bouton", cmd);
return;
}
var attr = getObj('attribute', cmd[1]);
if (attr === undefined) {
log("Attribut à changer perdu");
log(cmd);
return;
}
options.decrAttribute = attr;
return;
case "incrDmgCoef":
options.dmgCoef = (options.dmgCoef || 1);
if (cmd.length > 1) {
var incrDmgCoef = parseInt(cmd[1]);
if (isNaN(incrDmgCoef)) {
error("L'option --incrDmgCoef attend un entier", cmd);
return;
}
options.dmgCoef += incrDmgCoef;
return;
}
options.dmgCoef++; //Par défaut, incrémente de 1
return;
default:
sendChat("COF", "Argument de !cof-attack '" + arg + "' non reconnu");
}
});
attack(msg.playerid, attaquant, targetToken, attackLabel, options);
}
function sendPlayer(origin, msg) {
var dest = origin;
if (origin.who) {
if (playerIsGM(origin.playerid)) dest = 'GM';
else dest = origin.who;
}
sendChat('COF', '/w "' + dest + '" ' + msg);
}
function sendChar(charId, msg) {
var dest = '';
if (charId) dest = 'character|' + charId;
sendChat(dest, msg);
}
// Fait dépenser de la mana, et si pas possible, retourne false
function depenseMana(personnage, cout, msg, evt) {
var token = personnage.token;
var charId = personnage.charId;
var manaAttr = findObjs({
_type: 'attribute',
_characterid: charId,
name: 'PM'
});
var hasMana = false;
if (manaAttr.length > 0) {
var manaMax = parseInt(manaAttr[0].get('max'));
hasMana = !isNaN(manaMax) && manaMax > 0;
}
if (hasMana) {
var bar2 = parseInt(token.get("bar2_value"));
if (isNaN(bar2)) bar2 = 0;
if (bar2 < cout) {
msg = msg || '';
sendChar(charId, " n'a pas assez de points de mana pour " + msg);
return false;
}
affectToken(token, 'bar2_value', bar2, evt);
updateCurrentBar(token, 2, bar2 - cout);
return true;
}
sendChar(charId, " n'a pas de points de mana, action impossible");
return false;
}
function parseSave(cmd) {
if (cmd.length < 3) {
error("Usage : --psave carac seuil", cmd);
return;
}
var carac1;
var carac2;
if (cmd[1].length == 3) {
carac1 = cmd[1];
if (isNotCarac(cmd[1])) {
error("Le premier argument de save n'est pas une caractéristique", cmd);
return;
}
} else if (cmd[1].length == 6) { //Choix parmis 2 caracs
carac1 = cmd[1].substr(0, 3);
carac2 = cmd[1].substr(3, 3);
if (isNotCarac(carac1) || isNotCarac(carac2)) {
error("Le premier argument de save n'est pas une caractéristique", cmd);
return;
}
} else {
error("Le premier argument de save n'est pas une caractéristique", cmd);
return;
}
var res = {
carac: carac1,
carac2: carac2,
seuil: parseInt(cmd[2])
};
if (isNaN(res.seuil)) {
error("Le deuxième argument de --psave n'est pas un nombre", cmd);
return;
}
return res;
}
function parseCondition(args) {
if (args.length < 2) {
error("condition non reconnue", args);
return undefined;
}
switch (args[0]) {
case "etat":
if (_.has(cof_states, args[1])) {
return {
type: 'etat',
etat: args[1],
text: args[1]
};
}
return {
type: 'attribut',
attribute: args[1],
text: args[1]
};
case "deAttaque":
var valeurDeAttaque = parseInt(args[1]);
if (isNaN(valeurDeAttaque)) {
error("La condition de dé d'attaque doit être un nombre", args);
// on continue exprès pour tomber dans le cas par défaut
} else {
return {
type: 'deAttaque',
seuil: valeurDeAttaque,
text: args[1]
};
}
/* falls through */
default:
return {
type: args[0],
attribute: args[1],
text: args[1]
};
}
}
function testCondition(cond, attaquant, cibles, deAttaque) {
if (cond == 'toujoursVrai') return true;
switch (cond.type) {
case "moins":
// Au cas où on utilise les MOD au lieu de l'attribut de base:
var attribute = caracOfMod(cond.attribute);
if (attribute) cond.attribute = attribute;
var attackerAttr = charAttributeAsInt(attaquant.charId, cond.attribute, 0);
var resMoins = true;
cibles.forEach(function(target) {
if (resMoins) {
var targetAttr = charAttributeAsInt(target.charId, cond.attribute, 0);
if (targetAttr >= attackerAttr) resMoins = false;
}
});
return resMoins;
case "etat":
return (getState(attaquant, cond.etat));
case "attribut":
return (charAttributeAsBool(attaquant.charId, cond.attribute));
case "deAttaque":
if (deAttaque === undefined) {
error("Condition de dé d'attque non supportée ici", cond);
return true;
}
if (deAttaque < cond.seuil) return false;
return true;
default:
error("Condition non reconnue", cond);
}
return false;
}
// bonus d'attaque d'un token, indépendament des options
// Mise en commun pour attack et attaque-magique
function bonusDAttaque(personnage, explications, evt) {
explications = explications || [];
var charId = personnage.charId;
var tempAttkMod; // Utilise la barre 3 de l'attaquant
tempAttkMod = parseInt(personnage.token.get("bar3_value"));
if (tempAttkMod === undefined || isNaN(tempAttkMod) || tempAttkMod === "") {
tempAttkMod = 0;
}
var attBonus = tempAttkMod;
var fortifie = attributeAsInt(personnage, 'fortifie', 0);
if (fortifie > 0) {
attBonus += 3;
fortifie--;
explications.push("Effet du fortifiant => +3 en Attaque. Il sera encore actif pour " + fortifie + " tests");
if (fortifie === 0) {
removeTokenAttr(personnage, 'fortifie', evt);
} else {
setTokenAttr(personnage, 'fortifie', fortifie, evt);
}
}
attBonus += charAttributeAsInt(charId, 'actionConcertee', 0);
if (attributeAsBool(personnage, 'chant_des_heros')) {
attBonus += 1;
explications.push("Chant des héros => +1 en Attaque");
}
if (attributeAsBool(personnage, 'benediction')) {
attBonus += 1;
explications.push("Bénédiction => +1 en Attaque");
}
if (attributeAsBool(personnage, 'strangulation')) {
var malusStrangulation =
1 + attributeAsInt(personnage, 'dureeStrangulation', 0);
attBonus -= malusStrangulation;
explications.push("L'attaquant est étranglé => -" + malusStrangulation + " en Attaque");
}
if (getState(personnage, 'renverse')) {
attBonus -= 5;
explications.push("Attaquant à terre => -5 en Attaque");
}
var attrPosture = tokenAttribute(personnage, 'postureDeCombat');
if (attrPosture.length > 0) {
attrPosture = attrPosture[0];
var posture = attrPosture.get('max');
var postureVal;
if (posture.startsWith('ATT')) {
postureVal = parseInt(attrPosture.get('current'));
attBonus -= postureVal;
explications.push("Posture de combat => -" + postureVal + " en Attaque");
} else if (posture.endsWith('ATT')) {
postureVal = parseInt(attrPosture.get('current'));
attBonus += postureVal;
explications.push("Posture de combat => +" + postureVal + " en Attaque");
}
}
if (attributeAsBool(personnage, 'danseIrresistible')) {
attBonus -= 4;
explications.push("En train de danser => -4 en Attaque");
}
if (aUnCapitaine(personnage, evt)) {
attBonus += 2;
explications.push("Un capitaine donne des ordres => +2 en Attaque");
}
if (attributeAsBool(personnage, 'forceDeGeant')) {
attBonus += 2;
explications.push("Force de géant => +2 en Attaque");
}
if (attributeAsBool(personnage, 'nueeDInsectes')) {
attBonus -= 2;
explications.push("Nuée d’insectes => -2 en Attaque");
}
if (attributeAsBool(personnage, 'etatExsangue')) {
attBonus -= 2;
explications.push("Exsangue => -2 en Attaque");
}
if (attributeAsBool(personnage, 'armeBrulante')) {
attBonus -= 2;
explications.push("Arme brûlante => -2 en Attaque");
}
if (attributeAsBool(personnage, 'marcheSylvestre')) {
attBonus += 2;
explications.push("Marche sylvestre : +2 en Attaque");
}
if (attributeAsBool(personnage, 'prisonVegetale')) {
attBonus -= 2;
explications.push("Prison végétale : -2 en Attaque");
}
if (attributeAsBool(personnage, 'masqueDuPredateur')) {
var bonusMasque = modCarac(personnage, 'SAGESSE');
attBonus += bonusMasque;
explications.push("Masque du prédateur : +" + bonusMasque + " en Attaque et DM");
}
if (attributeAsBool(personnage, 'rageDuBerserk')) {
attBonus += 2;
explications.push("Rage du berserk : +2 en Attaque et +1d6 aux DM");
}
if (attributeAsBool(personnage, 'armeSecreteBarde')) {
attBonus -= 10;
explications.push("Déstabilisé par une action de charme => -10 en Attaque");
}
return attBonus;
}
function rollNumber(s) {
return parseInt(s.substring(3, s.indexOf(']')));
}
function getAttack(attackLabel, tokName, charId) {
// Get attack number (does not correspond to the position in sheet !!!)
var attackNumber = 0;
var attPrefix, weaponName;
while (true) {
attPrefix = "repeating_armes_$" + attackNumber + "_";
weaponName = getAttrByName(charId, attPrefix + "armenom");
if (weaponName === undefined || weaponName === "") {
error("Arme " + attackLabel + " n'existe pas pour " + tokName, charId);
return;
}
var weaponLabel = weaponName.split(' ', 1)[0];
if (weaponLabel == attackLabel) {
weaponName = weaponName.substring(weaponName.indexOf(' ') + 1);
return {
attackPrefix: attPrefix,
weaponName: weaponName
};
}
attackNumber++;
}
}
function surveillance(personnage) {
var surveillance = findObjs({
_type: 'attribute',
_characterid: personnage.charId,
name: 'surveillance'
});
if (surveillance.length > 0) {
var compagnon = surveillance[0].get('current');
var compToken = findObjs({
_type: 'graphic',
_subtype: 'token',
_pageid: personnage.token.get('pageid'),
layer: 'objects',
name: compagnon
});
var compagnonPresent = false;
compToken.forEach(function(tok) {
var compCharId = tok.get('represents');
if (compCharId === '') return;
if (isActive({
token: tok,
charId: compCharId
})) compagnonPresent = true;
return;
});
return compagnonPresent;
}
return false;
}
function tokenInit(perso, evt) {
var persoMonte = tokenAttribute(perso, 'estMontePar');
if (persoMonte.length > 0) {
var cavalier = tokenOfId(persoMonte[0].get('current'), persoMonte[0].get('max'), perso.token.get('pageid'));
if (cavalier !== undefined) return tokenInit(cavalier, evt);
}
var charId = perso.charId;
var init = parseInt(getAttrByName(charId, 'DEXTERITE'));
init += charAttributeAsInt(perso, 'INIT_DIV', 0);
if (getState(perso, 'aveugle')) init -= 5;
// Voie du compagnon animal rang 2 (surveillance)
if (surveillance(perso)) init += 5;
// Voie du chef d'armée rang 2 (Capitaine)
if (aUnCapitaine(perso, evt)) init += 2;
if (charAttributeAsBool(perso, 'graceFeline')) {
init += modCarac(perso, 'CHARISME');
}
if (attributeAsBool(perso, 'masqueDuPredateur')) {
init += modCarac(perso, 'SAGESSE');
}
// Voie du pistolero rang 1 (plus vite que son ombre)
attributesInitEnMain(charId).forEach(function(em) {
var armeL = labelInitEnMain(em);
if (charAttributeAsInt(charId, "charge_" + armeL, 0) > 0) {
var initBonus = parseInt(em.get('current'));
if (isNaN(initBonus) || initBonus < 0) {
error("initBonusEnMain incorrect :" + initBonus, em);
return;
}
init += initBonus;
}
});
return init;
}
//fonction avec callback, mais synchrone
function soigneToken(perso, soins, evt, callTrue, callMax) {
var token = perso.token;
var bar1 = parseInt(token.get("bar1_value"));
var pvmax = parseInt(token.get("bar1_max"));
if (isNaN(bar1) || isNaN(pvmax)) {
error("Soins sur un token sans points de vie", token);
return;
}
if (bar1 >= pvmax) {
if (callMax) callMax();
return;
}
if (soins < 0) soins = 0;
affectToken(token, 'bar1_value', bar1, evt);
if (bar1 === 0) {
if (attributeAsBool(perso, 'etatExsangue')) {
removeTokenAttr(perso, 'etatExsangue', evt, "retrouve des couleurs");
}
}
if (attributeAsBool(perso, 'vieArtificielle')) {
soins = Math.floor(soins / 2);
}
bar1 += soins;
var soinsEffectifs = soins;
if (bar1 > pvmax) {
soinsEffectifs -= (bar1 - pvmax);
bar1 = pvmax;
}
updateCurrentBar(token, 1, bar1);
if (callTrue) callTrue(soinsEffectifs);
}
function defenseOfToken(attaquant, target, pageId, evt, options) {
options = options || {};
var tokenName = target.tokName;
var explications = target.messages;
var defense = 10;
if (target.defautCuirasse === undefined) {
defense += charAttributeAsInt(target, 'DEFARMURE', 0) * charAttributeAsInt(target, 'DEFARMUREON', 1);
defense += charAttributeAsInt(target, 'DEFBOUCLIER', 0) * charAttributeAsInt(target, 'DEFBOUCLIERON', 1);
if (attributeAsBool(target, 'armureDuMage')) {
if (defense > 12) defense += 2; // On a déjà une armure physique, ça ne se cumule pas.
else defense += 4;
}
defense += charAttributeAsInt(target, 'DEFDIV', 0);
} // Dans le cas contraire, on n'utilise pas ces bonus
defense += modCarac(target, 'DEXTERITE');
// Malus de défense global pour les longs combats
if (DEF_MALUS_APRES_TOUR_5)
defense -= (Math.floor((state.COFantasy.tour - 1) / 5) * 2);
// Autres modificateurs de défense
defense += attributeAsInt(target, 'defenseTotale', 0);
defense += attributeAsInt(target, 'pacifisme', 0);
if (attributeAsBool(target, 'peau_d_ecorce')) {
defense += charAttributeAsInt(target, 'voieDesVegetaux', 0);
}
if (getState(target, 'surpris')) defense -= 5;
if (getState(target, 'renverse')) defense -= 5;
if (getState(target, 'aveugle')) defense -= 5;
if (getState(target, 'etourdi') || attributeAsBool(target, 'peurEtourdi'))
defense -= 5;
defense += attributeAsInt(target, 'bufDEF', 0);
defense += attributeAsInt(target, 'actionConcertee', 0);
if (charAttributeAsInt(target, 'DEFARMUREON', 1) === 0) {
defense += charAttributeAsInt(target, 'vetementsSacres', 0);
defense += charAttributeAsInt(target, 'armureDeVent', 0);
if (!options.distance)
defense += charAttributeAsInt(target, 'dentellesEtRapiere', 0);
}
if (charAttributeAsBool(target, 'graceFeline')) {
defense += modCarac(target, 'CHARISME');
}
if (attributeAsBool(target, 'armeSecreteBarde')) {
defense -= 10;
}
var attrsProtegePar = findObjs({
_type: 'attribute',
_characterid: target.charId,
});
attrsProtegePar.forEach(function(attr) {
var attrName = attr.get('name');
if (attrName.startsWith('protegePar_')) {
var nameProtecteur = attr.get('max');
if (attr.get('bar1_link') === '') {
if (attrName != 'protegePar_' + nameProtecteur + '_' + tokenName) return;
} else if (attrName != 'protegePar_' + nameProtecteur) return;
var protecteur = tokenOfId(attr.get('current'), nameProtecteur, pageId);
if (protecteur === undefined) {
removeTokenAttr(target, 'protegePar_' + nameProtecteur, evt);
sendChar(target.charId, "ne peut pas être protégé par " + nameProtecteur + " car aucun token le représentant n'est sur la page");
return;
}
if (!isActive(protecteur)) {
explications.push(nameProtecteur + " n'est pas en état de protéger " +
tokenName);
return;
}
var distTargetProtecteur = distanceCombat(target.token, protecteur.token, pageId);
if (distTargetProtecteur > 0) {
explications.push(nameProtecteur + " est trop loin de " +
tokenName + " pour le protéger");
return;
}
if (charAttributeAsInt(protecteur.charId, 'DEFBOUCLIERON', 1) === 0) {
var sujet = onGenre(protecteur.charId, 'il', 'elle');
explications.push(nameProtecteur +
" ne porte pas son bouclier, " + sujet + " ne peut pas proteger " +
tokenName);
return;
}
var defBouclierProtecteur = charAttributeAsInt(protecteur.charId, 'DEFBOUCLIER', 0);
defense += defBouclierProtecteur;
explications.push(nameProtecteur + " protège " +
tokenName + " de son bouclier (+" + defBouclierProtecteur + "DEF)");
}
});
var attrPosture = tokenAttribute(target, 'postureDeCombat');
if (attrPosture.length > 0) {
attrPosture = attrPosture[0];
var posture = attrPosture.get('max');
var postureVal;
if (posture.startsWith('DEF')) {
postureVal = parseInt(attrPosture.get('current'));
defense -= postureVal;
explications.push("Posture de combat => -" + postureVal + " DEF");
} else if (posture.endsWith('DEF')) {
postureVal = parseInt(attrPosture.get('current'));
defense += postureVal;
explications.push("Posture de combat => +" + postureVal + " DEF");
}
}
var instinctSurvie = charAttributeAsInt(target, 'instinctDeSurvie', 0);
if (instinctSurvie > 0 && target.token.get('bar1_value') <= instinctSurvie)
defense += 5;
if (attributeAsBool(target, 'danseIrresistible')) {
defense -= 4;
explications.push("En train de danser => -4 DEF");
}
if (options.sortilege)
defense += charAttributeAsInt(target.charId, 'DEF_magie', 0);
if (attributeAsBool(target, 'marcheSylvestre')) {
defense += 2;
explications.push("Marche sylvestre => +2 DEF");
}
if (attributeAsBool(target, 'prisonVegetale')) {
defense -= 2;
explications.push("Prison végétale => -2 DEF");
}
if (attributeAsBool(target, 'protectionContreLeMal') &&
estMauvais(attaquant)) {
defense += 2;
explications.push("Protection contre le mal => +2 DEF");
}
if (attributeAsBool(target, 'rageDuBerserk')) {
defense -= 4;
explications.push("Rage du berserk => -4 DEF");
}
var combatEnPhalange = charAttributeAsBool(target, 'combatEnPhalange');
if (combatEnPhalange || attributeAsBool(target, 'esquiveFatale')) {
var tokensContact = findObjs({
_type: 'graphic',
_subtype: "token",
_pageid: pageId,
layer: 'objects'
});
tokensContact = tokensContact.filter(function(tok) {
if (tok.id == target.token.id) return false;
if (distanceCombat(target.token, tok, pageId) === 0) return true;
return false;
});
var tokensAllies = [];
var tokensEnnemis = [];
var allies = alliesParPerso[target.charId] || new Set();
tokensContact.forEach(function(tok) {
var ci = tok.get('represents');
if (ci === '') return; //next token au contact
if (!isActive({
token: tok,
charId: ci
})) return;
if (allies.has(ci)) tokensAllies.push(tok);
else tokensEnnemis.push(tok);
});
target.ennemisAuContact = tokensEnnemis;
if (combatEnPhalange) {
var defensePhalange = 0;
tokensEnnemis.forEach(function(tokE) {
var alliesAuContact = tokensAllies.filter(function(tokA) {
if (distanceCombat(tokE, tokA, pageId) === 0) return true;
return false;
});
if (alliesAuContact.length > defensePhalange)
defensePhalange = alliesAuContact.length;
});
if (defensePhalange > 0) {
defense += defensePhalange;
explications.push("Combat en phalange => +" + defensePhalange + " DEF");
}
}
}
return defense;
}
//Bonus en Attaque qui ne dépendent pas du défenseur
function bonusAttaqueA(attaquant, name, weaponName, evt, explications, options) {
var attBonus = 0;
if (options.bonusAttaque) attBonus += options.bonusAttaque;
attBonus += bonusDAttaque(attaquant, explications, evt);
if (getState(attaquant, 'aveugle')) {
if (options.distance) {
if (charAttributeAsBool(attaquant, 'tirAveugle')) {
explications.push("Attaquant aveuglé, mais il sait tirer à l'aveugle");
} else {
attBonus -= 10;
explications.push("Attaquant aveuglé => -10 en Attaque à distance");
}
} else {
attBonus -= 5;
explications.push("Attaquant aveuglé => -5 en Attaque");
}
}
if (options.tirDouble) {
attBonus += 2;
explications.push(name + " tire avec 2 " + weaponName + "s à la fois !");
}
if (options.chance) {
attBonus += options.chance;
var pc = options.chance / 10;
explications.push(pc + " point" + ((pc > 1) ? "s" : "") + " de chance dépensé => +" + options.chance + " en Attaque");
}
if (options.semonce) {
attBonus += 5;
}
if (attributeAsBool(attaquant, 'baroudHonneurActif')) {
attBonus += 5;
explications.push(name + " porte une dernière attaque et s'effondre");
mort(attaquant, evt);
removeTokenAttr(attaquant, 'baroudHonneurActif', evt);
}
if (options.sortilege && attributeAsBool(attaquant, 'zoneDeSilence')) {
attBonus -= 2;
explications.push("Zone de silence => -2 en Attaque Magique");
}
if (!options.distance && (attributeAsBool(attaquant, 'aCheval') || attributeAsBool(attaquant, 'monteSur'))) {
attBonus += charAttributeAsInt(attaquant, 'cavalierEmerite');
explications.push("A cheval => +2 en Attaque");
}
if (options.frappeDuVide) {
attBonus += 2;
explications.push("Frappe du vide => +2 en Attaque et +1d6 DM");
}
return attBonus;
}
//Bonus d'attaque qui dépendent de la cible
// si options.aoe, target doit avoir un champ tokName
function bonusAttaqueD(attaquant, target, portee, pageId, evt, explications, options) {
var attackingCharId = attaquant.charId;
attaquant.tokName = attaquant.tokName || attaquant.token.get('name');
var attackerTokName = attaquant.tokName;
var attBonus = 0;
if (options.mainsDEnergie) {
if (options.aoe) error("Mains d'énergie n'est pas compatible avec les AOE", options.aoe);
// Check if target wears armor
var targetArmorDef = parseInt(getAttrByName(target.charId, "DEFARMURE"));
if (isNaN(targetArmorDef) || targetArmorDef === 0) {
attBonus += 2;
explications.push("Mains d'énergie => +2 en Attaque (cible sans armure)");
} else {
var bonusMain = Math.min(5, 2 + targetArmorDef);
attBonus += bonusMain;
explications.push("Mains d'énergie => +" + bonusMain + " en Attaque");
}
}
if (options.aoe === undefined && options.auto === undefined && portee > 0) {
attBonus -=
malusDistance(attaquant, target.token, target.distance, portee, pageId,
explications, options.ignoreObstacles);
}
var chasseurEmerite =
charAttributeAsBool(attackingCharId, 'chasseurEmerite') &&
charOfType(target.charId, "animal");
if (chasseurEmerite) {
attBonus += 2;
var explChasseurEmerite = "Chasseur émérite => +2 en Attaque et aux DM";
if (options.aoe) explChasseurEmerite += " contre " + target.tokName;
explications.push(explChasseurEmerite);
target.chasseurEmerite = true;
}
var ennemiJure = findObjs({
_type: 'attribute',
_characterid: attackingCharId,
name: 'ennemiJure'
});
if (ennemiJure.length === 0) ennemiJure = false;
else ennemiJure = raceIs(target, ennemiJure[0].get('current'));
if (ennemiJure) {
var ejSag = modCarac(attaquant, 'SAGESSE');
attBonus += ejSag;
var explEnnemiJure = "Attaque sur ennemi juré => +" + ejSag + " en attaque et +1d6 aux DM";
if (options.aoe) explEnnemiJure += " contre " + target.tokName;
explications.push(explEnnemiJure);
target.ennemiJure = true;
}
if (options.argent) {
if (estMortVivant(target) || raceIs(target, 'demon') || raceIs(target, 'démon')) {
attBonus += 2;
explications.push("Arme en argent => +2 en attaque et +1d6 aux DM");
target.argent = true;
}
}
if (options.bonusContreBouclier) {
if (charAttributeAsInt(target, 'DEFBOUCLIERON', 1) &&
charAttributeAsInt(target, 'DEFBOUCLIER', 0) > 0) {
attBonus += options.bonusContreBouclier;
explications.push("L'adversaire porte un bouclier => " + ((options.bonusContreBouclier > 0) ? '+' : '') + options.bonusContreBouclier + " en attaque");
}
}
if (options.tueurDeGeants && estUnGeant(target)) {
attBonus += 2;
explications.push("Tueur de géant => +2 att. et 2d6 DM");
target.tueurDeGeants = true;
}
if (attributeAsBool(target, 'feinte_' + attaquant.tokName)) {
attBonus += 5;
explications.push("Feinte => +5 en attaque");
}
if (options.contact) {
if (attributeAsBool(target, 'criDeGuerre') &&
charAttributeAsInt(attackingCharId, 'FORCE', 10) <= charAttributeAsInt(target.charId, 'FORCE', 10) &&
parseInt(attaquant.token.get("bar1_max")) <= parseInt(target.token.get("bar1_max"))) {
attBonus -= 2;
explications.push("Effrayé => -2 en Attaque");
}
}
if (charAttributeAsBool(attaquant, 'combatEnPhalange')) {
var tokensContact = findObjs({
_type: 'graphic',
_subtype: "token",
_pageid: pageId,
layer: 'objects'
});
//On compte tokens au contact de l'attaquant et du défenseur et alliés de l'attaquant
var allies = alliesParPerso[attaquant.charId];
if (allies) {
var alliesAuContact = 0;
tokensContact.forEach(function(tok) {
if (tok.id == attaquant.token.id) return;
if (distanceCombat(target.token, tok, pageId) > 0) return;
if (distanceCombat(attaquant.token, tok, pageId) > 0) return;
var ci = tok.get('represents');
if (ci === '') return;
if (!isActive({
token: tok,
charId: ci
})) return;
if (allies.has(ci)) alliesAuContact++;
});
if (alliesAuContact > 0) {
attBonus += alliesAuContact;
explications.push("Combat en phalange => +" + alliesAuContact + " en Attaque");
}
}
}
return attBonus;
}
function computeDice(lanceur, nbDe, dice, plusFort) {
if (plusFort === undefined) plusFort = true;
if (dice === undefined) dice = deTest(lanceur);
if (attributeAsBool(lanceur, 'malediction')) {
if (plusFort) {
if (nbDe > 1) nbDe--;
else {
nbDe = 2;
plusFort = false;
}
} else nbDe++;
}
var de = nbDe + "d" + dice;
if (nbDe > 1) {
if (plusFort) de += "kh1";
else de += "kl1";
}
return de;
}
function diminueMalediction(lanceur, evt, attr) {
var attrMalediction = attr || tokenAttribute(lanceur, 'malediction');
if (attrMalediction.length > 0) {
attrMalediction = attrMalediction[0];
var nbMaudit = parseInt(attrMalediction.get('current'));
if (isNaN(nbMaudit) || nbMaudit < 2) {
evt.deletedAttributes = evt.deletedAttributes || [];
evt.deletedAttributes.push(attrMalediction);
attrMalediction.remove();
} else {
evt.attributes = evt.attributes || [];
evt.attributes.push({
attribute: attrMalediction,
current: nbMaudit
});
attrMalediction.set('current', nbMaudit - 1);
}
}
}
function limiteRessources(personnage, options, defResource, msg, evt) {
if (personnage === undefined) {
personnage = options.lanceur;
}
if (options.mana) {
if (!depenseMana(personnage, options.mana, msg, evt)) {
addEvent(evt);
return true;
}
}
var ressource = defResource;
var utilisations;
if (options.limiteParJour) {
if (options.limiteParJourRessource)
ressource = options.limiteParJourRessource;
ressource = "limiteParJour_" + ressource;
utilisations =
attributeAsInt(personnage, ressource, options.limiteParJour);
if (utilisations === 0) {
sendChar(personnage.charId, "ne peut plus faire cette action ajourd'hui");
addEvent(evt);
return true;
}
setTokenAttr(personnage, ressource, utilisations - 1, evt);
}
if (options.limiteParCombat) {
if (!state.COFantasy.combat) {
sendChar(personnage.charId, "ne peut pas faire cette action en dehors des combats");
addEvent(evt);
return true;
}
if (options.limiteParCombatRessource)
ressource = options.limiteParCombatRessource;
ressource = "limiteParCombat_" + ressource;
utilisations =
attributeAsInt(personnage, ressource, options.limiteParCombat);
if (utilisations === 0) {
sendChar(personnage.charId, "ne peut plus faire cette action pour ce combat");
addEvent(evt);
return true;
}
setTokenAttr(personnage, ressource, utilisations - 1, evt);
}
if (options.dose) {
var nomDose = options.dose.replace(/_/g, ' ');
var doses = attributeAsInt(personnage, 'dose_' + options.dose, 0);
if (doses === 0) {
sendChar(personnage.charId, "n'a plus de " + nomDose);
addEvent(evt);
return true;
}
setTokenAttr(personnage, 'dose_' + options.dose, doses - 1, evt);
}
if (options.limiteAttribut) {
var nomAttr = options.limiteAttribut.nom;
var currentAttr = attributeAsInt(personnage, nomAttr, 0);
if (currentAttr >= options.limiteAttribut.limite) {
sendChar(personnage.charId, options.limiteAttribut.message);
addEvent(evt);
return true;
}
setTokenAttr(personnage, nomAttr, currentAttr + 1, evt);
}
if (options.decrAttribute) {
var attr = options.decrAttribute;
var oldval = parseInt(attr.get('current'));
if (isNaN(oldval) || oldval < 1) {
sendChar(personnage.charId, "ne peut plus faire cela");
return true;
}
evt.attributes = evt.attributes || [];
evt.attributes.push({
attribute: attr,
current: oldval,
max: attr.get('max')
});
attr.set('current', oldval - 1);
}
return false;
}
//targetToken est soit un token, soit une structure avec un champs cibles qui contient toutes les cibles
function attack(playerId, attaquant, targetToken, attackLabel, options) {
// Attacker and target infos
var attackingToken = attaquant.token;
var attackingCharId = attaquant.charId;
attaquant.tokName = attaquant.tokName || attaquant.token.get("name");
var attackerTokName = attaquant.tokName;
var attacker = getObj("character", attackingCharId);
if (attacker === undefined) {
error("Unexpected undefined 1", attacker);
return;
}
attaquant.name = attaquant.name || attacker.get("name");
var attackerName = attaquant.name;
var pageId = attaquant.token.get('pageid');
//Options automatically set by some attributes
if (charAttributeAsBool(attackingCharId, 'fauchage')) {
var seuilFauchage = 10 + modCarac(attaquant, 'FORCE');
options.etats = options.etats || [];
options.etats.push({
etat: 'renverse',
condition: {
type: 'deAttaque',
seuil: 15
},
save: {
carac: 'FOR',
carac2: 'DEX',
seuil: seuilFauchage
}
});
}
var weaponName;
var weaponStats = {};
var attSkillDiv;
var crit;
var portee;
var this_weapon = JSON.parse(attackLabel);
if (Array.isArray(this_weapon)) {
weaponName = this_weapon[0].replace(/_/g, ' ');
weaponStats.attSkill = this_weapon[1][0];
attSkillDiv = this_weapon[1][1];
crit = this_weapon[2];
var DMG = this_weapon[3];
weaponStats.attNbDices = DMG[0];
weaponStats.attDice = DMG[0];
weaponStats.attCarBonus = DMG[0];
weaponStats.attDMBonusCommun = DMG[0];
portee = this_weapon[4];
} else {
//On trouve l'attaque correspondant au label
var att = getAttack(attackLabel, attackerTokName, attackingCharId);
if (att === undefined) return;
var attPrefix = att.attackPrefix;
weaponName = att.weaponName;
weaponStats.attSkill =
getAttrByName(attackingCharId, attPrefix + "armeatk") ||
getAttrByName(attackingCharId, "ATKCAC");
attSkillDiv = getAttrByName(attackingCharId, attPrefix + "armeatkdiv") || 0;
// DMG
weaponStats.attNbDices = getAttrByName(attackingCharId, attPrefix + "armedmnbde") || 1;
weaponStats.attDice = getAttrByName(attackingCharId, attPrefix + "armedmde") || 4;
weaponStats.attCarBonus =
getAttrByName(attackingCharId, attPrefix + "armedmcar") ||
modCarac(attaquant, "FORCE");
weaponStats.attDMBonusCommun = getAttrByName(attackingCharId, attPrefix + "armedmdiv");
crit = getAttrByName(attackingCharId, attPrefix + "armecrit") || 20;
portee = getPortee(attackingCharId, attPrefix);
}
attSkillDiv = parseInt(attSkillDiv);
weaponStats.attNbDices = parseInt(weaponStats.attNbDices);
weaponStats.attDice = parseInt(weaponStats.attDice);
weaponStats.attDMBonusCommun = parseInt(weaponStats.attDMBonusCommun);
crit = parseInt(crit);
if (portee > 0) {
options.distance = true;
if (attributeAsBool(attaquant, 'rageDuBerserk')) {
sendChar(attaquant.charId, "est en rage du berserk, il ne veut attaquer qu'au contact");
return;
}
} else options.contact = true;
//Détermination de la (ou des) cible(s)
var nomCiblePrincipale; //Utilise pour le cas mono-cible
var cibles = [];
if (targetToken.cibles) { //Dans ce cas les cibles sont précisées dans targetToken
cibles = targetToken.cibles;
if (cibles.length === 0) {
error("Attaque sans cible", targetToken);
return;
} else if (cibles.length == 1) targetToken = targetToken.cibles.token;
nomCiblePrincipale = cibles[0].tokName;
} else {
nomCiblePrincipale = targetToken.get('name');
if (options.aoe) {
if (options.targetFx) {
spawnFx(targetToken.get('left'), targetToken.get('top'), options.targetFx, pageId);
}
var distanceTarget = distanceCombat(targetToken, attackingToken, pageId, true, true);
var pta = tokenCenter(attackingToken);
var ptt = tokenCenter(targetToken);
switch (options.aoe.type) {
case 'ligne':
if (distanceTarget < portee) { //la ligne va plus loin que la cible
var scale = portee * 1.0 / distanceTarget;
ptt = [
Math.round((ptt[0] - pta[0]) * scale) + pta[0],
Math.round((ptt[1] - pta[1]) * scale) + pta[1]
];
}
if (targetToken.get('bar1_max') == 0) { // jshint ignore:line
//C'est juste un token utilisé pour définir la ligne
if (options.fx) {
var p1e = {
x: attackingToken.get('left'),
y: attackingToken.get('top'),
};
var p2e = {
x: targetToken.get('left'),
y: targetToken.get('top'),
};
spawnFxBetweenPoints(p1e, p2e, options.fx, pageId);
}
cibles = [];
targetToken.remove(); //On l'enlève, normalement plus besoin
}
var allToks =
findObjs({
_type: "graphic",
_pageid: pageId,
_subtype: "token",
layer: "objects"
});
allToks.forEach(function(obj) {
if (obj.id == attackingToken.id) return; //on ne se cible pas
var objCharId = obj.get('represents');
if (objCharId === '') return;
var cible = {
token: obj,
charId: objCharId
};
if (getState(cible, 'mort')) return; //pas de dégâts aux morts
var pt = tokenCenter(obj);
var distToTrajectory = VecMath.ptSegDist(pt, pta, ptt);
if (distToTrajectory > (obj.get('width') + obj.get('height')) / 2)
return;
cible.tokName = obj.get('name');
var objChar = getObj('character', objCharId);
if (objChar === undefined) return;
cible.name = objChar.get('name');
cibles.push(cible);
});
break;
case 'disque':
if (distanceTarget > portee) {
sendChar(attackingCharId,
"Le centre du disque visé est trop loin pour " + weaponName +
" (distance " + distanceTarget + ", portée " + portee + ")");
return;
}
var allToksDisque =
findObjs({
_type: "graphic",
_pageid: pageId,
_subtype: "token",
layer: "objects"
});
allToksDisque.forEach(function(obj) {
if (portee === 0 && obj.id == attackingToken.id) return; //on ne se cible pas si le centre de l'aoe est soi-même
if (obj.get('bar1_max') == 0) return; // jshint ignore:line
var objCharId = obj.get('represents');
if (objCharId === '') return;
var cible = {
token: obj,
charId: objCharId
};
if (getState(cible, 'mort')) return; //pas de dégâts aux morts
var distanceCentre =
distanceCombat(targetToken, obj, pageId, true);
if (distanceCentre > options.aoe.rayon) return;
var objChar = getObj('character', objCharId);
if (objChar === undefined) return;
cible.name = objChar.get('name');
cible.tokName = obj.get('name');
cibles.push(cible);
});
if (targetToken.get('bar1_max') == 0) { // jshint ignore:line
//C'est juste un token utilisé pour définir le disque
targetToken.remove(); //On l'enlève, normalement plus besoin
}
// La nouvelle portée (pour ne rien éliminer à l'étape suivante
portee += options.aoe.rayon;
break;
case 'cone':
var vecCentre = VecMath.normalize(VecMath.vec(pta, ptt));
var cosAngle = Math.cos(options.aoe.angle * Math.PI / 180.0);
if (targetToken.get('bar1_max') == 0) { // jshint ignore:line
//C'est juste un token utilisé pour définir le cone
if (options.fx) {
var p1eC = {
x: attackingToken.get('left'),
y: attackingToken.get('top'),
};
var p2eC = {
x: targetToken.get('left'),
y: targetToken.get('top'),
};
spawnFxBetweenPoints(p1eC, p2eC, options.fx, pageId);
}
cibles = [];
targetToken.remove(); //On l'enlève, normalement plus besoin
}
var allToksCone =
findObjs({
_type: "graphic",
_pageid: pageId,
_subtype: "token",
layer: "objects"
});
allToksCone.forEach(function(obj) {
if (obj.id == attackingToken.id) return; //on ne se cible pas
var objCharId = obj.get('represents');
if (objCharId === '') return;
var cible = {
token: obj,
charId: objCharId
};
if (getState(cible, 'mort')) return; //pas de dégâts aux morts
var pt = tokenCenter(obj);
var vecObj = VecMath.normalize(VecMath.vec(pta, pt));
if (VecMath.dot(vecCentre, vecObj) < cosAngle) return;
// La distance sera comparée à la portée plus loin
var objChar = getObj('character', objCharId);
if (objChar === undefined) return;
cible.name = objChar.get('name');
cible.tokName = obj.get('name');
cibles.push(cible);
});
break;
default:
error("aoe inconnue", options.aoe);
return;
}
} else {
if (attackingToken.id == targetToken.id) { //même token pour attaquant et cible
sendChar(attackingCharId, "s'attaque lui-même ? Probablement une erreur à la sélection de la cible. On annule");
return;
}
var targetCharId = targetToken.get("represents");
if (targetCharId === "") {
error("Le token ciblé (" + nomCiblePrincipale + ") doit représenter un personnage ", targetToken);
return;
}
var targetChar = getObj("character", targetCharId);
if (targetChar === undefined) {
error("Unexpected undefined 2", targetChar);
return;
}
cibles = [{
token: targetToken,
charId: targetCharId,
name: targetChar.get('name'),
tokName: nomCiblePrincipale
}];
}
if (options.ciblesSupplementaires) {
options.ciblesSupplementaires.forEach(function(c) {
var i = cibles.indexOf(function(t) {
return (t.token.id == c.token.id);
});
if (i < 0) cibles.push(c);
});
}
}
//Les conditions qui peuvent empêcher l'attaque
if (options.conditionAttaquant !== undefined) {
if (!testCondition(options.conditionAttaquant, attaquant, cibles)) {
sendChar(attackingCharId, "ne peut pas utiliser " + weaponName);
return;
}
}
if (options.avecd12 && getState(attaquant, 'affaibli')) {
sendChar(attackingCharId, "ne peut pas utiliser cette capacité quand il est affaibli.");
return;
}
cibles = cibles.filter(function(target) {
if (attributeAsBool(target, 'ombreMortelle')) {
sendChar(attackingCharId, "impossible d'attaquer une ombre");
return false;
}
return true;
});
if (cibles.length === 0) return;
//Prise en compte de la distance
cibles = cibles.filter(function(target) {
target.distance = distanceCombat(attackingToken, target.token, pageId);
if (target.distance > portee && target.esquiveFatale === undefined) {
if (options.aoe || options.auto) return false; //distance stricte
if (target.distance > 2 * portee) return false;
// On peut aller jusqu'à 2x portee si unique cible et jet d'attaque
return true;
}
return true;
});
if (cibles.length === 0) {
if (options.aoe) {
sendChar(attackingCharId, "aucune cible dans l'aire d'effet de " + weaponName);
return;
}
sendChar(attackingCharId, "est hors de portée de " + nomCiblePrincipale + " pour une attaque utilisant " + weaponName);
return;
}
var evt = options.evt || {
type: "Tentative d'attaque"
}; //the event to be stored in history
var explications = [];
var sujetAttaquant = onGenre(attackingCharId, 'il', 'elle');
// Munitions
if (options.munition) {
if (attackingToken.get('bar1_link') === '') {
error("Les munitions ne sont pas supportées pour les tokens qui ne sont pas liées à un personnage", attackingToken);
}
var munitionsAttr = findObjs({
_type: 'attribute',
_characterid: attackingCharId,
name: 'munition_' + options.munition.nom
});
if (munitionsAttr.length === 0) {
error("Pas de munition nommée " + options.munition.nom + " pour " + attackerName);
return; //evt toujours vide
}
munitionsAttr = munitionsAttr[0];
var munitions = munitionsAttr.get('current');
if (munitions < 1 || (options.tirDouble && munitions < 2)) {
sendChar(attackingCharId,
"ne peut pas utiliser cette attaque, car " + sujetAttaquant +
" n'a plus de " + options.munition.nom.replace(/_/g, ' '));
return; //evt toujours vide
}
var munitionsMax = munitionsAttr.get('max');
evt.attributes = evt.attributes || [];
evt.attributes.push({
attribute: munitionsAttr,
current: munitions,
max: munitionsMax
});
//À partir de ce point, tout return doit ajouter evt
munitions--;
if (randomInteger(100) < options.munition.taux) munitionsMax--;
if (options.tirDouble) {
munitions--;
if (randomInteger(100) < options.munition.taux) munitionsMax--;
}
explications.push("Il reste " + munitions + " " +
options.munition.nom.replace(/_/g, ' ') + " à " + attackerTokName);
munitionsAttr.set('current', munitions);
munitionsAttr.set('max', munitionsMax);
}
// Armes chargées
if (options.semonce === undefined && options.tirDeBarrage === undefined) {
var chargesArme = findObjs({
_type: 'attribute',
_characterid: attackingCharId,
name: "charge_" + attackLabel
});
if (chargesArme.length > 0) {
var currentCharge = parseInt(chargesArme[0].get('current'));
if (isNaN(currentCharge) || currentCharge < 1) {
sendChar(attackingCharId, "ne peut pas attaquer avec " + weaponName + " car elle n'est pas chargée");
addEvent(evt);
return;
}
if (options.tirDouble && currentCharge < 2) {
sendChar(attackingCharId,
"ne peut pas faire de tir double avec ses" + weaponName + "s car " +
sujetAttaquant + " n'en a pas au moins 2 chargées");
addEvent(evt);
return;
}
evt.attributes = evt.attributes || [];
evt.attributes.push({
attribute: chargesArme[0],
current: currentCharge
});
if (options.tirDouble) currentCharge -= 2;
else currentCharge -= 1;
chargesArme[0].set('current', currentCharge);
if (currentCharge === 0 &&
charAttributeAsInt(attackingCharId, "initEnMain" + attackLabel, 0) > 0) {
updateNextInit(attackingToken);
}
} else {
if (options.tirDouble) {
sendChar(attackingCharId,
"ne peut pas faire de tir double avec ses" + weaponName + "s car " +
sujetAttaquant + " n'en a pas au moins 2 chargées");
addEvent(evt);
return;
}
}
}
if (limiteRessources(attaquant, options, attackLabel, weaponName, evt))
return;
// Effets quand on rentre en combat
if (!state.COFantasy.combat) {
var selected = [{
_id: attackingToken.id
}];
cibles.forEach(function(target) {
selected.push({
_id: target.token.id
});
});
initiative(selected, evt);
}
var pacifisme_selected = tokenAttribute(attaquant, 'pacifisme');
if (pacifisme_selected.length > 0 && pacifisme_selected[0].get('current') > 0) {
pacifisme_selected[0].set('current', 0);
sendChat("GM", "/w " + attackerName + " " + attackerTokName + " perd son pacifisme");
}
if (options.contact && attributeAsBool(attaquant, 'frappeDuVide')) {
options.frappeDuVide = true;
setTokenAttr(attaquant, 'frappeDuVide', 0, evt);
}
// On commence par le jet d'attaque de base : juste le ou les dés d'attaque
// et le modificateur d'arme et de caractéritiques qui apparaissent dans
// la description de l'attaque. Il faut quand même tenir compte des
// chances de critique
if (isNaN(crit) || crit < 1 || crit > 20) {
error("Le critique n'est pas un nombre entre 1 et 20", crit);
crit = 20;
}
if (charAttributeAsBool(attaquant, 'scienceDuCritique') ||
(!options.distance && !options.sortilege && charAttributeAsBool(attaquant, 'morsureDuSerpent')) ||
(crit == 20 && charAttributeAsBool(attaquant, 'ecuyer'))) crit -= 1;
if (options.bonusCritique) crit -= options.bonusCritique;
if (options.affute) crit -= 1;
var dice = 20;
if (getState(attaquant, 'affaibli')) {
dice = 12;
explications.push("Attaquant affaibli => D12 au lieu de D20 en Attaque");
}
if (options.avecd12) dice = 12;
var nbDe = 1;
if (options.m2d20) nbDe = 2;
var de = computeDice(attaquant, nbDe, dice);
var attackRollExpr = "[[" + de + "cs>" + crit + "cf1]]";
if (isNaN(attSkillDiv)) attSkillDiv = 0;
var attSkillDivTxt = "";
if (attSkillDiv > 0) attSkillDivTxt = " + " + attSkillDiv;
else if (attSkillDiv < 0) attSkillDivTxt += attSkillDiv;
var attackSkillExpr = addOrigin(attackerName, "[[" + weaponStats.attSkill + attSkillDivTxt + "]]");
// toEvaluateAttack inlines
// 0: attack roll
// 1: attack skill expression
// 2: dé de poudre
var toEvaluateAttack = attackRollExpr + " " + attackSkillExpr;
if (options.poudre) toEvaluateAttack += " [[1d20]]";
sendChat(attackerName, toEvaluateAttack, function(resAttack) {
var rollsAttack = options.rollsAttack || resAttack[0];
var afterEvaluateAttack = rollsAttack.content.split(' ');
var attRollNumber = rollNumber(afterEvaluateAttack[0]);
var attSkillNumber = rollNumber(afterEvaluateAttack[1]);
var d20roll = rollsAttack.inlinerolls[attRollNumber].results.total;
var attSkill = rollsAttack.inlinerolls[attSkillNumber].results.total;
evt.type = "Attaque";
evt.succes = true;
evt.action = {
player_id: playerId,
attaquant: attaquant,
cibles: cibles,
attack_label: attackLabel,
rollsAttack: rollsAttack,
options: options
};
addEvent(evt);
// debut de la partie affichage
var action = "Arme : ";
if (options.sortilege) action = "Sort : ";
var label_type = BS_LABEL_INFO;
var target = cibles[0];
if (options.aoe || cibles.length > 1) {
target = undefined;
label_type = BS_LABEL_WARNING;
}
action += "" + weaponName + "";
var display = startFramedDisplay(playerId, action, attaquant, target);
// Cas des armes à poudre
if (options.poudre) {
var poudreNumber = rollNumber(afterEvaluateAttack[2]);
var dePoudre = rollsAttack.inlinerolls[poudreNumber].results.total;
explications.push(
"Dé de poudre : " + buildinline(rollsAttack.inlinerolls[poudreNumber]));
if (dePoudre === 1) {
evt.succes = false;
if (d20roll === 1) {
explications.push(
weaponName + " explose ! L'arme est complètement détruite");
sendChat("", "[[2d6]]", function(res) {
var rolls = res[0];
var explRoll = rolls.inlinerolls[0];
var r = {
total: explRoll.results.total,
type: 'normal',
display: buildinline(explRoll, 'normal')
};
dealDamage(attaquant, r, [], evt, 1, options, explications,
function(dmgDisplay, dmg) {
var dmgMsg = "Dommages pour " + attackerTokName + " : " +
dmgDisplay;
addLineToFramedDisplay(display, dmgMsg);
finaliseDisplay(display, explications, evt);
});
});
} else {
explications.push(
"La poudre explose dans " + weaponName +
". L'arme est inutilisable jusqu'à la fin du combat");
sendChat("", "[[1d6]]", function(res) {
var rolls = res[0];
var explRoll = rolls.inlinerolls[0];
var r = {
total: explRoll.results.total,
type: 'normal',
display: buildinline(explRoll, 'normal')
};
dealDamage(attaquant, r, [], evt, 1, options, explications,
function(dmgDisplay, dmg) {
var dmgMsg = "Dommages pour " + attackerTokName + " : " +
dmgDisplay;
addLineToFramedDisplay(display, dmgMsg);
finaliseDisplay(display, explications, evt);
});
});
}
return;
} else if (d20roll == dePoudre) {
evt.succes = false;
addLineToFramedDisplay(display,
"Attaque : " +
buildinline(rollsAttack.inlinerolls[attRollNumber]));
explications.push(weaponName + " fait long feu, le coup ne part pas");
finaliseDisplay(display, explications, evt);
return;
}
}
//Modificateurs en Attaque qui ne dépendent pas de la cible
var attBonusCommun =
bonusAttaqueA(attaquant, attackerTokName, weaponName, evt,
explications, options);
//Autres conditions qui ne modifient pas que le bonus d'attaque
if (options.contact) {
if (attributeAsBool(attaquant, 'rayon_affaiblissant')) {
options.rayonAffaiblissant = true;
attBonusCommun -= 2;
explications.push("Rayon affaiblissant => -2 en Attaque et aux DM");
}
}
var traquenard = false;
if (options.traquenard) {
if (attributeAsInt(attaquant, 'traquenard', 0) === 0) {
sendChar(attackingCharId, "ne peut pas faire de traquenard, car ce n'est pas sa première attaque du combat");
return;
}
traquenard = tokenInit(attaquant, evt);
}
if (attributeAsInt(attaquant, 'traquenard', 0) > 0) {
setTokenAttr(attaquant, 'traquenard', 0, evt);
}
if (options.feinte) explications.push("Mais c'était une feinte...");
var mainDmgType = options.type || 'normal';
if (options.sortilege) options.ignoreObstacles = true;
var critSug; //Suggestion en cas d'écher critique
//Calcul des cibles touchées
//(et on ajuste le jet pour la triche)
var ciblesTouchees = [];
var count = cibles.length;
cibles.forEach(function(target) {
target.additionalDmg = [];
target.messages = [];
//Les bonus d'attaque qui dépendent de la cible
var bad = bonusAttaqueD(attaquant, target, portee, pageId, evt, target.messages, options);
var attBonus = attBonusCommun + bad;
if (traquenard) {
var initTarg = tokenInit(target, evt);
if (traquenard >= initTarg) {
attBonus += 2;
target.additionalDmg.push({
type: mainDmgType,
value: '2d6'
});
target.messages.push(attackerTokName + " fait un traquenard à " + target.tokName);
} else {
target.messages.push(attackerTokName + " n'est pas assez rapide pour faire un traquenard à " + target.tokName);
}
}
var defautCuirasse =
tokenAttribute(target, 'defautDansLaCuirasse_' + attackerTokName);
target.crit = crit;
if (defautCuirasse.length > 0) {
target.defautCuirasse = true;
if (target.crit > 2) target.crit -= 1;
}
//Defense de la cible
var defense = defenseOfToken(attaquant, target, pageId, evt, options);
var interchange;
if (options.aoe === undefined) {
interchange = interchangeable(attackingToken, target, pageId);
if (interchange.result) defense += 5;
}
//Absorption au bouclier
var absorber;
if (target.absorber) {
explications = explications.concat(target.absorberExpl);
if (target.absorber > defense) {
defense = target.absorber;
absorber = target.absorberDisplay;
}
}
var touche = 1; //false: pas touché, 1 touché, 2 critique
if (options.dmgCoef) touche = options.dmgCoef;
// Calcule si touché, et les messages de dégats et attaque
if (!options.auto) {
if (options.triche) {
switch (options.triche) {
case "rate":
if (d20roll >= target.crit) {
if (target.crit < 2) d20roll = 1;
else d20roll = randomInteger(target.crit - 1);
}
if ((d20roll + attSkill + attBonus) >= defense) {
var maxd20roll = defense - attSkill - attBonus - 1;
if (maxd20roll >= target.crit) maxd20roll = target.crit - 1;
if (maxd20roll < 2) d20roll = 1;
else d20roll = randomInteger(maxd20roll);
}
break;
case "touche":
if (d20roll == 1) d20roll = randomInteger(dice - 1) + 1;
if ((d20roll + attSkill + attBonus) < defense) {
var mind20roll = defense - attSkill - attBonus - 1;
if (mind20roll < 1) mind20roll = 1;
if (mind20roll >= dice) d20roll = dice;
else d20roll = randomInteger(dice - mind20roll) + mind20roll;
}
break;
case "critique":
if (d20roll < target.crit) {
if (target.crit <= dice) d20roll = randomInteger(dice - target.crit + 1) + target.crit - 1;
else d20roll = dice;
}
break;
case "echecCritique":
if (d20roll > 1) d20roll = 1;
break;
default:
error("Option inconnue", options.triche);
}
// now adjust the roll
var attackInlineRoll = rollsAttack.inlinerolls[attRollNumber];
attackInlineRoll.results.total = d20roll;
attackInlineRoll.results.rolls.forEach(function(roll) {
switch (roll.type) {
case "R":
if (roll.results.length == 1) {
roll.results[0].v = d20roll;
}
break;
default:
return;
}
});
}
var attackRoll = d20roll + attSkill + attBonus;
var attackResult; // string
var paralyse = false;
if (getState(target, 'paralyse')) {
paralyse = true;
target.messages.push("Cible paralysée => réussite critique automatique");
}
if (charAttributeAsBool(attaquant, 'champion') && d20roll >= 15)
options.champion = true;
if (d20roll == 1 && options.chance === undefined) {
attackResult = " : échec critique";
if (options.demiAuto) {
target.partialSaveAuto = true;
evt.succes = false;
} else touche = false;
var confirmCrit = randomInteger(20);
critSug = "/w GM Jet de confirmation pour l'échec critique : " +
confirmCrit + "/20. Suggestion d'effet : ";
switch (confirmCrit) {
case 1:
critSug += "l'attaquant se blesse ou est paralysé un tour";
break;
case 2:
critSug += "l'attaquant blesse un allié";
break;
case 3:
critSug += "l'arme casse, ou une pièce d'armure se détache, ou -5 DEF un tour (comme surpris)";
break;
case 4:
critSug += "l'attaquant lache son arme ou glisse et tombe";
break;
default:
critSug += "simple échec";
}
} else if (paralyse || d20roll >= target.crit) {
attackResult = " : réussite critique";
touche++;
} else if (options.champion) {
attackResult = " : succès";
} else if (attackRoll < defense) {
attackResult = " : échec";
evt.succes = false;
if (options.demiAuto) {
target.partialSaveAuto = true;
} else touche = false;
} else { // Touché normal
attackResult = " : succès";
}
var attRollValue = buildinline(rollsAttack.inlinerolls[attRollNumber]);
if (attSkill > 0) attRollValue += "+" + attSkill;
else if (attSkill < 0) attRollValue += attSkill;
if (attBonus > 0) attRollValue += "+" + attBonus;
else if (attBonus < 0) attRollValue += attBonus;
var line = "Attaque ";
if (options.aoe || cibles.length > 1) {
line += "contre " + target.tokName + " ";
}
line += ":
";
line += attRollValue + " vs ";
if (absorber) line += absorber;
else line += defense;
line += " " + attackResult;
target.attackMessage = line;
if (touche) {
if (options.asDeLaGachette && attackRoll > 24) {
target.messages.push("As de la gachette : + 1d6 aux DM");
target.additionalDmg.push({
type: mainDmgType,
value: '1d6'
});
}
} else { //Effet si on ne touche pas
// Draw failed effect
if (_.has(options, "fx") && options.distance) {
var p1 = {
x: attackingToken.get('left'),
y: attackingToken.get('top')
};
var p2 = {
x: target.token.get('left'),
y: target.token.get('top')
};
// Compute some gaussian deviation in [0, 1]
var dev =
(Math.random() + Math.random() + Math.random() + Math.random() +
Math.random() + 1) / 6;
// take into account by how far we miss
dev = dev * (d20roll == 1) ? 2 : ((attackRoll - defense) / 20);
if (Math.random() > 0.5) dev = -dev;
p2.x += dev * (p2.y - p1.y);
p2.y += dev * (p2.x - p1.x);
spawnFxBetweenPoints(p1, p2, options.fx, pageId);
}
evt.succes = false;
diminueMalediction(attaquant, evt);
}
}
target.touche = touche;
if (options.aoe === undefined && interchange.targets.length > 1) { //any target can be affected
var n = randomInteger(interchange.targets.length);
target.token = interchange.targets[n - 1];
}
if (target.touche &&
attributeAsBool(target, 'image_decalee')) {
var id = randomInteger(6);
if (id > 4) {
target.touche = false;
target.messages.push("L'attaque passe à travers l'image de " + target.tokName);
} else {
target.messages.push("Malgré l'image légèrement décalée de " + target.tokName + " l'attaque touche");
}
}
if (target.touche) {
ciblesTouchees.push(target);
if (attributeAsBool(target, 'esquiveFatale')) {
var ennemisAuContact = target.ennemisAuContact;
if (ennemisAuContact === undefined) {
error("Les ennemis au contact n'ont pas été déterminé");
} else {
var iOther = ennemisAuContact.find(function(tok) {
return (tok.id != attaquant.token.id);
});
if (iOther !== undefined)
target.messages.push(bouton("!cof-esquive-fatale " + evt.id + " @{target|token_id}", "Esquive fatale ?", target));
}
}
}
if (options.test || options.feinte || !target.touche) {
//On a fini avec cette cible, on imprime ce qui la concerne
addLineToFramedDisplay(display, target.attackMessage);
target.messages.forEach(function(expl) {
addLineToFramedDisplay(display, expl, 80);
});
}
if (options.feinte) {
setTokenAttr(target, 'feinte_' + attaquant.tokName, 0, evt, undefined, target.touche);
}
count--;
if (count === 0)
attackDealDmg(attaquant, ciblesTouchees, critSug, attackLabel, weaponStats, d20roll, display, options, evt, explications, pageId);
}); //fin de détermination de toucher des cibles
}); // fin du jet d'attaque asynchrone
}
function attackDealDmg(attaquant, cibles, critSug, attackLabel, weaponStats, d20roll, display, options, evt, explications, pageId) {
if (cibles.length === 0 || options.test || options.feinte) {
finaliseDisplay(display, explications, evt);
if (critSug) sendChat('COF', critSug);
return;
}
var attackingCharId = attaquant.charId;
var attackingToken = attaquant.token;
var attackerTokName = attaquant.tokName;
options.attaquant = attaquant;
//Les dégâts
//Dégâts insrits sur la ligne de l'arme
var mainDmgType = options.type || 'normal';
var attDice = weaponStats.attDice;
var attNbDices = weaponStats.attNbDices;
var attCarBonus = weaponStats.attCarBonus;
var attDMBonusCommun = weaponStats.attDMBonusCommun;
if (isNaN(attDice) || attDice < 0 || isNaN(attNbDices) || attNbDices < 0) {
error("Dés de l'attaque incorrect", attDice);
return;
}
if (options.puissant) {
attDice += 2;
}
if (options.reroll1) attDice += "r1";
if (options.reroll2) attDice += "r2";
if (isNaN(attCarBonus)) {
if (attCarBonus.startsWith('@{')) {
var carac = caracOfMod(attCarBonus.substr(2, 3));
if (carac) {
var simplerAttCarBonus = modCarac(attaquant, carac);
if (!isNaN(simplerAttCarBonus)) {
attCarBonus = simplerAttCarBonus;
}
}
}
}
if (attCarBonus === "0" || attCarBonus === 0) attCarBonus = "";
else attCarBonus = " + " + attCarBonus;
if (isNaN(attDMBonusCommun) || attDMBonusCommun === 0) attDMBonusCommun = '';
else if (attDMBonusCommun > 0) attDMBonusCommun = '+' + attDMBonusCommun;
// Les autres modifications aux dégâts qui ne dépendent pas de la cible
if (options.rayonAffaiblissant) {
attDMBonusCommun += " -2";
}
if (attributeAsBool(attaquant, 'masqueDuPredateur')) {
var bonusMasque = modCarac(attaquant, 'SAGESSE');
if (bonusMasque > 0) attDMBonusCommun += " +" + bonusMasque;
}
if (attributeAsBool(attaquant, 'rageDuBerserk')) {
options.additionalDmg.push({
type: mainDmgType,
value: '1d6'
});
}
var attrPosture = tokenAttribute(attaquant, 'postureDeCombat');
if (attrPosture.length > 0) {
attrPosture = attrPosture[0];
var posture = attrPosture.get('max');
var postureVal;
if (posture.startsWith('DM')) {
postureVal = parseInt(attrPosture.get('current'));
attDMBonusCommun += " -" + postureVal;
explications.push("Posture de combat => -" + postureVal + " DM");
} else if (posture.endsWith('DM')) {
postureVal = parseInt(attrPosture.get('current'));
attDMBonusCommun += " +" + postureVal;
explications.push("Posture de combat => +" + postureVal + " DM");
}
}
if (aUnCapitaine(attaquant, evt, pageId)) attDMBonusCommun += " +2";
// Les autres sources de dégâts
if (options.distance) {
if (options.semonce) {
options.additionalDmg.push({
type: mainDmgType,
value: '1d6'
});
explications.push("Tir de semonce => +5 en Attaque et +1d6 aux DM");
}
} else { //bonus aux attaques de contact
if (attributeAsBool(attaquant, 'agrandissement')) {
attDMBonusCommun += "+2";
explications.push("Agrandissement => +2 aux DM");
}
if (attributeAsBool(attaquant, 'forceDeGeant')) {
attDMBonusCommun += "+2";
explications.push("Force de géant => +2 en Attaque");
}
if (options.frappeDuVide) {
options.additionalDmg.push({
type: mainDmgType,
value: '1d6'
});
}
}
if (attributeAsBool(attaquant, 'forgeron_' + attackLabel)) {
var feuForgeron = charAttributeAsInt(attackingCharId, 'voieDuMetal', 0);
if (feuForgeron < 1 || feuForgeron > 5) {
error("Rang dans la voie du métal de " + attackerTokName + " inconnu ou incorrect", feuForgeron);
} else {
options.additionalDmg.push({
type: 'feu',
value: feuForgeron
});
}
}
var poisonAttr = tokenAttribute(attaquant, 'poisonRapide_' + attackLabel);
if (poisonAttr.length > 0) {
poisonAttr = poisonAttr[0];
options.additionalDmg.push({
type: 'poison',
value: poisonAttr.get('current'),
partialSave: {
carac: 'CON',
seuil: poisonAttr.get('max')
}
});
explications.push("L'arme est empoisonnée");
evt.deletedAttributes = evt.deletedAttributes || [];
evt.deletedAttributes.push(poisonAttr);
poisonAttr.remove();
}
if (charAttributeAsBool(attackingCharId, 'dmgArme1d6_' + attackLabel)) {
options.additionalDmg.push({
type: mainDmgType,
value: '1d6'
});
explications.push("Arme enduite => +1d6 aux DM");
}
if (options.champion) {
options.additionalDmg.push({
type: mainDmgType,
value: '1d6'
});
explications.push(attackerTokName + " est un champion, son attaque porte !");
}
/////////////////////////////////////////////////////////////////
//Tout ce qui dépend de la cible
var ciblesCount = cibles.length; //Pour l'asynchronie
var finCibles = function() {
ciblesCount--;
if (ciblesCount === 0) {
cibles.forEach(function(target) {
if (target.attackMessage) {
addLineToFramedDisplay(display, target.attackMessage);
} else if (options.aoe) { //par exemple si attaque automatique
addLineToFramedDisplay(display, "" + target.tokName + " :");
}
if (target.dmgMessage) addLineToFramedDisplay(display, target.dmgMessage, 100, false);
target.messages.forEach(function(expl) {
addLineToFramedDisplay(display, expl, 80);
});
});
finaliseDisplay(display, explications, evt);
}
};
cibles.forEach(function(target) {
var attDMBonus = attDMBonusCommun;
//Les modificateurs de dégâts qui dépendent de la cible
if (options.pressionMortelle) {
var pMortelle = tokenAttribute(target, 'pressionMortelle');
if (pMortelle.length === 0) {
sendChar(attackingCharId, "Essaie une pression mortelle, mais aucun point vital de " + target.tokName + " n'a encore été affecté");
ciblesCount--;
return;
}
attNbDices = pMortelle[0].get('max');
attDice = 4; //TODO : have an option for that
attDMBonus = "+ " + pMortelle[0].get('current');
}
if (_.has(options, "tempDmg")) {
var forceTarg = modCarac(target, "FORCE");
if (forceTarg < 0) {
attDMBonus = " +" + (-forceTarg);
} else {
attDMBonus = " -" + forceTarg;
}
}
if (options.distance) {
var tirPrecis = charAttributeAsInt(attackingCharId, 'tirPrecis', 0);
if (tirPrecis > 0) {
var modDex = modCarac(attaquant, 'DEXTERITE');
if (target.distance <= 5 * modDex) {
attDMBonus += " + " + tirPrecis;
target.messages.push("Tir précis : +" + tirPrecis + " DM");
}
}
}
if (options.sournoise) {
if (charAttributeAsBool(target, 'immuniteAuxSournoises')) {
target.messages.push('Immunité aux attaques sournoises');
} else {
target.additionalDmg.push({
type: mainDmgType,
value: options.sournoise + 'd6'
});
target.messages.push("Attaque sournoise => +" + options.sournoise + "+d6 DM");
}
}
if (target.chasseurEmerite) {
attDMBonus += "+2";
}
if (target.ennemiJure) {
target.additionalDmg.push({
type: mainDmgType,
value: '1d6'
});
}
if (target.tueurDeGeants) {
target.additionalDmg.push({
type: mainDmgType,
value: '2d6'
});
}
if (target.argent) {
target.additionalDmg.push({
type: mainDmgType,
value: '1d6'
});
}
var attrFeinte = tokenAttribute(target, 'feinte_' + attaquant.tokName);
if (attrFeinte.length > 0 && attrFeinte[0].get('current') &&
attrFeinte[0].get('max')) {
target.additionalDmg.push({
type: mainDmgType,
value: '2d6'
});
}
var loupParmiLesLoups = charAttributeAsInt(attaquant, 'loupParmiLesLoups', 0);
if (loupParmiLesLoups > 0 && estHumanoide(target)) {
attDMBonus += "+" + loupParmiLesLoups;
target.messages.push("Loup parmi les loups : +" + loupParmiLesLoups + " DM");
}
if (attributeAsBool(attaquant, 'ombreMortelle') ||
attributeAsBool(attaquant, 'dedoublement')) {
if (options.divise) options.divise *= 2;
else options.divise = 2;
}
var attNbDicesCible = attNbDices;
var attDiceCible = attDice;
var attCarBonusCible = attCarBonus;
if (!options.sortilege &&
charAttributeAsBool(target.charId, 'immuniteAuxArmes')) {
if (options.magique) {
attNbDicesCible = options.magique;
attDiceCible = "6";
attCarBonusCible = modCarac(target, 'SAGESSE');
if (attCarBonusCible < 1) attCarBonusCible = "";
else attCarBonusCible = " +" + attCarBonusCible;
} else {
target.messages.push(target.tokName + " semble immunisé aux armes ordinaires");
attNbDicesCible = 0;
attCarBonusCible = "";
attDMBonus = "";
}
}
var mainDmgRollExpr =
addOrigin(attaquant.name, attNbDicesCible + "d" + attDiceCible + attCarBonusCible + attDMBonus);
//Additional damage
var additionalDmg = options.additionalDmg.concat(target.additionalDmg);
if (!options.sortilege && !options.magique &&
charAttributeAsBool(target.charId, 'immuniteAuxArmes')) {
additionalDmg = additionalDmg.filter(function(dmSpec) {
switch (dmSpec.type) {
case undefined:
case 'normal':
case 'poison':
case 'maladie':
return false;
default:
return true;
}
});
}
if (options.tirDouble || options.tirDeBarrage) {
mainDmgRollExpr += " +" + mainDmgRollExpr;
additionalDmg.forEach(function(dmSpec) {
dmSpec.value += " +" + dmSpec.Value;
});
}
var ExtraDmgRollExpr = "";
additionalDmg = additionalDmg.filter(function(dmSpec) {
dmSpec.type = dmSpec.type || 'normal';
if (dmSpec.type != mainDmgType || isNaN(dmSpec.value)) {
ExtraDmgRollExpr += " [[" + dmSpec.value + "]]";
return true;
}
// We have the same type and a constant -> should be multiplied by crit
mainDmgRollExpr += " + " + dmSpec.value;
return false;
});
var mainDmgRoll = {
type: mainDmgType,
value: mainDmgRollExpr
};
// toEvaluateDmg inlines
// 0 : roll de dégâts principaux
// 1+ : les rolls de dégâts supplémentaires
var toEvaluateDmg = "[[" + mainDmgRollExpr + "]]" + ExtraDmgRollExpr;
sendChat(attaquant.name, toEvaluateDmg, function(resDmg) {
var rollsDmg = target.rollsDmg || resDmg[0];
var afterEvaluateDmg = rollsDmg.content.split(' ');
var mainDmgRollNumber = rollNumber(afterEvaluateDmg[0]);
mainDmgRoll.total = rollsDmg.inlinerolls[mainDmgRollNumber].results.total;
mainDmgRoll.display = buildinline(rollsDmg.inlinerolls[mainDmgRollNumber], mainDmgType, options.magique);
additionalDmg.forEach(function(dmSpec, i) {
var rRoll = rollsDmg.inlinerolls[rollNumber(afterEvaluateDmg[i + 1])];
dmSpec.total = dmSpec.total || rRoll.results.total;
var addDmType = dmSpec.type;
dmSpec.display = dmSpec.display || buildinline(rRoll, addDmType, options.magique);
});
if (target.touche) { //Devrait être inutile ?
if (options.tirDeBarrage) target.messages.push("Tir de barrage : undo si la cible décide de ne pas bouger");
if (options.pointsVitaux) target.messages.push(attackerTokName + " vise des points vitaux mais ne semble pas faire de dégâts");
if (options.pressionMortelle) {
removeTokenAttr(target, 'pressionMortelle', evt);
target.messages.push(attackerTokName + " libère la pression des points vitaux, l'effet est dévastateur !");
spawnFx(target.token.get('left'), target.token.get('top'), 'bomb-death', pageId);
}
// change l'état de la cible, si spécifié
if (options.enflamme) {
var enflammePuissance = 1;
if (options.puissant) enflammePuissance = 2;
setTokenAttr(target, 'enflamme', enflammePuissance, evt);
target.messages.push(target.tokName + " prend feu !");
}
if (options.malediction) {
setTokenAttr(target, 'malediction', 3, evt);
target.messages.push(target.tokName + " est maudit...");
}
// Draw effect, if any
if (options.fx) {
var p1e = {
x: attackingToken.get('left'),
y: attackingToken.get('top'),
};
var p2e = {
x: target.token.get('left'),
y: target.token.get('top'),
};
spawnFxBetweenPoints(p1e, p2e, options.fx, pageId);
}
if (options.targetFx && !options.aoe) {
spawnFx(target.token.get('left'), target.token.get('top'), options.targetFx, pageId);
}
target.rollsDmg = rollsDmg;
// Compte le nombre de saves pour la synchronisation
// (On ne compte pas les psave, gérés dans dealDamage)
var saves = 0;
//ajoute les états sans save à la cible
if (options.etats) {
options.etats.forEach(function(ce) {
if (ce.save) {
saves++;
return; //on le fera plus tard
}
if (testCondition(ce.condition, attaquant, [{
charId: target.charId
}], d20roll)) {
setState(target, ce.etat, true, evt);
target.messages.push(target.tokName + " est " + ce.etat + eForFemale(target.charId) + " par l'attaque");
} else {
if (ce.condition.type == "moins") {
target.messages.push(
"Grâce à sa " + ce.condition.text + ", " + target.tokName +
" n'est pas " + ce.etat + eForFemale(target.charId));
}
}
});
}
var savesEffets = 0;
// Ajoute les effets sans save à la cible
if (options.effets) {
options.effets.forEach(function(ef) {
if (ef.save) {
saves++;
savesEffets++;
return; //on le fera plus tard
}
if (ef.effet == 'dedoublement') {
if (attributeAsBool(target, 'dedouble') ||
attributeAsBool(target, 'dedoublement')) {
target.messages.push(target.tokName + " a déjà été dédoublé pendant ce combat");
return;
}
target.messages.push("Un double translucide de " +
target.tokName + " apparaît. Il est aux ordres de " +
attackerTokName);
setTokenAttr(target, 'dedouble', true, evt);
copieToken(target, undefined, IMAGE_DOUBLE,
"Double de " + target.tokName, 'dedoublement', ef.duree,
pageId, evt);
return;
}
target.messages.push(target.tokName + " " + messageEffetTemp[ef.effet].activation);
setTokenAttr(target, ef.effet, ef.duree, evt, undefined,
getInit());
if (ef.effet == 'aveugleTemp') {
setState(target, 'aveugle', true, evt);
} else if (ef.effet == 'ralentiTemp') {
setState(target, 'ralenti', true, evt);
}
if (ef.saveParTour) {
setTokenAttr(target, ef.effet + "SaveParTour",
ef.saveParTour.carac, evt, undefined, ef.saveParTour.seuil);
}
});
}
// Tout ce qui se passe après les saves (autres que saves de diminution des dmg
var afterSaves = function() {
if (saves > 0) return; //On n'a pas encore fait tous les saves
if (additionalDmg.length === 0 && mainDmgRoll.total === 0 &&
attNbDices === 0) {
// Pas de dégâts, donc pas d'appel à dealDamage
finCibles();
} else {
dealDamage(target, mainDmgRoll, additionalDmg, evt, target.touche,
options, target.messages,
function(dmgDisplay, dmg) {
if (options.strigeSuce) {
var suce = attributeAsInt(attaquant, 'strigeSuce', 0);
if (suce === 0) {
setTokenAttr(attaquant, 'bufDEF', -3, evt);
target.messages.push(
attackerTokName + " s'agrippe à " + target.tokName +
" et commence à lui sucer le sang");
}
if (suce + dmg >= 6) {
target.messaged.push(
"Repus, " + attackerTokName + " se détache et s'envole");
target.messaged.push(target.tokName + " se sent un peu faible...");
setState(target, 'affaibli', true, evt);
var defbuf = attributeAsInt(attaquant, 'bufDEF', 0);
if (defbuf === -3) {
removeTokenAttr(attaquant, 'bufDEF', evt);
} else if (defbuf !== 0) {
setTokenAttr(attaquant, 'bufDEF', defbuf + 3, evt);
}
} else {
setTokenAttr(attaquant, 'strigeSuce', suce + dmg, evt);
if (suce > 0)
target.messages.push(
attackerTokName + " continue à sucer le sang de " + target.tokName);
}
}
if (options.vampirise) {
soigneToken(attaquant, dmg, evt, function(soins) {
target.messages.push(
"L'attaque soigne " + attackerTokName + " de " + soins +
" PV");
});
}
target.dmgMessage = "DM : " + dmgDisplay;
if (attributeAsBool(target, 'sous_tension') && options.contact) {
ciblesCount++;
sendChat("", "[[1d6]]", function(res) {
var rolls = res[0];
var explRoll = rolls.inlinerolls[0];
var r = {
total: explRoll.results.total,
type: 'electrique',
display: buildinline(explRoll, 'electrique', true)
};
dealDamage(attaquant, r, [], evt, 1, options,
target.messages,
function(dmgDisplay, dmg) {
var dmgMsg =
"Décharge électrique sur " + attackerTokName + " : " +
dmgDisplay;
target.messages.push(dmgMsg);
finCibles();
});
});
}
if (attributeAsBool(target, 'sangMordant') && options.contact) {
ciblesCount++;
sendChat("", "[[1d6]]", function(res) {
var rolls = res[0];
var explRoll = rolls.inlinerolls[0];
var r = {
total: explRoll.results.total,
type: 'acide',
display: buildinline(explRoll, 'acide', true)
};
dealDamage(attaquant, r, [], evt, 1, options,
target.messages,
function(dmgDisplay, dmg) {
var dmgMsg =
"Le sang acide gicle sur " + attackerTokName + " : " +
dmgDisplay + " DM";
target.messages.push(dmgMsg);
finCibles();
});
});
}
finCibles();
});
}
};
var expliquer = function(msg) {
target.messages.push(msg);
};
//Ajoute les états avec save à la cibles
var etatsAvecSave = function() {
if (savesEffets > 0) return; //On n'a pas encore fini avec les effets
if (options.etats && saves > 0) {
options.etats.forEach(function(ce) {
if (ce.save) {
if (testCondition(ce.condition, attaquant, [{
charId: target.charId
}], d20roll)) {
var msgPour = " pour résister à un effet";
var msgRate = ", " + target.tokName + " est " + ce.etat + eForFemale(target.charId) + " par l'attaque";
var saveOpts = {
msgPour: msgPour,
msgRate: msgRate,
attaquant: attaquant
};
save(ce.save, target, expliquer, saveOpts, evt,
function(reussite, rolltext) {
if (!reussite) {
setState(target, ce.etat, true, evt);
}
saves--;
afterSaves();
});
} else {
if (ce.condition.type == "moins") {
target.messages.push(
"Grâce à sa " + ce.condition.text + ", " + target.tokName +
" n'est pas " + ce.etat + eForFemale(target.charId));
}
saves--;
afterSaves();
}
}
});
} else afterSaves();
};
// Ajoute les effets avec save à la cible
var effetsAvecSave = function() {
if (options.effets && savesEffets > 0) {
options.effets.forEach(function(ef) {
if (ef.save) {
var msgPour = " pour résister à un effet";
var msgRate = ", " + target.tokName + " " + messageEffetTemp[ef.effet].activation;
var saveOpts = {
msgPour: msgPour,
msgRate: msgRate,
attaquant: attaquant
};
save(ef.save, target, expliquer, saveOpts, evt,
function(reussite, rollText) {
if (!reussite) {
setTokenAttr(target, ef.effet, ef.duree, evt,
undefined, getInit());
if (ef.effet == 'aveugleTemp') {
setState(target, 'aveugle', true, evt);
} else if (ef.effet == 'ralentiTemp') {
setState(target, 'ralenti', true, evt);
}
if (ef.saveParTour) {
setTokenAttr(target,
ef.effet + "SaveParTour", ef.saveParTour.carac,
evt, undefined, ef.saveParTour.seuil);
}
}
saves--;
savesEffets--;
etatsAvecSave();
});
}
});
} else etatsAvecSave();
};
var effetPietinement = function() {
if (options.pietine && estAussiGrandQue(attaquant, target)) {
testOppose(target, 'FOR', attaquant, 'FOR', target.messages, evt,
function(resultat) {
if (resultat == 2) {
target.messages.push(target.tokName + " est piétiné par " + attackerTokName);
setState(target, 'renverse', true, evt);
target.touche++;
} else {
if (resultat === 0) diminueMalediction(attaquant, evt);
target.messages.push(target.tokName + " n'est pas piétiné.");
}
effetsAvecSave();
});
} else effetsAvecSave();
};
// Peut faire peur à la cible
if (options.peur) {
peurOneToken(target, pageId, options.peur.seuil,
options.peur.duree, {
resisteAvecForce: true
}, display, evt, effetPietinement);
} else effetPietinement();
} else {
evt.succes = false;
finCibles();
}
});
}); //Fin de la boucle pour toutes cibles
}
function finaliseDisplay(display, explications, evt) {
explications.forEach(function(expl) {
addLineToFramedDisplay(display, expl, 80);
});
if (evt.succes === false && evt.action) {
evt.personnage = evt.action.attaquant;
var pc = attributeAsInt(evt.action.attaquant, 'PC', 0);
if (pc > 0) {
addLineToFramedDisplay(display, bouton("!cof-bouton-chance " + evt.id, "Chance", evt.personnage) + " (reste " + pc + " PC)");
}
if (charAttributeAsBool(evt.action.attaquant, 'runeDEnergie')) {
addLineToFramedDisplay(display, bouton("!cof-bouton-rune-energie " + evt.id, "Rune d'énergie", evt.personnage));
}
}
sendChat("", endFramedDisplay(display));
}
// RD spécifique au type
function typeRD(charId, dmgType) {
if (dmgType === undefined || dmgType == 'normal') return 0;
return charAttributeAsInt(charId, 'RD_' + dmgType, 0);
}
function probaSucces(de, seuil, nbreDe) {
if (nbreDe == 2) {
var proba1 = probaSucces(de, seuil, 1);
return 1 - (1 - proba1) * (1 - proba1);
}
if (seuil < 2) seuil = 2; // 1 est toujours un échec
else if (seuil > 20) seuil = 20;
return ((de - seuil) + 1) / de;
}
// Meilleure carac parmis 2 pour un save.
function meilleureCarac(carac1, carac2, personnage, seuil) {
var charId = personnage.charId;
var bonus1 = bonusTestCarac(carac1, personnage);
if (carac1 == 'DEX') bonus1 += charAttributeAsInt(charId, 'reflexesFelins', 0);
var bonus2 = bonusTestCarac(carac2, personnage);
if (carac2 == 'DEX') bonus2 += charAttributeAsInt(charId, 'reflexesFelins', 0);
var nbrDe1 = nbreDeTestCarac(carac1, charId);
var nbrDe2 = nbreDeTestCarac(carac2, charId);
var de = deTest(personnage);
var proba1 = probaSucces(de, seuil - bonus1, nbrDe1);
var proba2 = probaSucces(de, seuil - bonus2, nbrDe2);
if (proba2 > proba1) return carac2;
return carac1;
}
//s représente le save, avec une carac, une carac2 optionnelle et un seuil
//expliquer est une fonction qui prend en argument un string et le publie
// options peut contenir les champs :
// - msgPour : message d'explication à afficher avant le jet
// - msgReussite : message à afficher en cas de réussite
// - msgRate : message à afficher si l'action rate
// - attaquant : le {charId, token} de l'attaquant contre lequel le save se fait (si il y en a un)
function save(s, target, expliquer, options, evt, afterSave) {
var bonus = 0;
if (options.attaquant &&
charAttributeAsBool(target, 'protectionContreLeMal') &&
estMauvais(options.attaquant)) {
bonus += 2;
expliquer("Protection contre le mal => +2 au jet de sauvegarde");
}
var bonusAttrs = [];
var carac = s.carac;
//Cas où le save peut se faire au choix parmis 2 caracs
if (s.carac2) {
carac = meilleureCarac(carac, s.carac2, target, s.seuil);
}
if (carac == 'DEX') {
bonusAttrs.push('reflexesFelins');
}
testCaracteristique(target, carac, s.seuil, {
bonusAttrs: bonusAttrs,
bonus: bonus
}, evt,
function(tr) {
var smsg = " Jet de " + carac + " " + s.seuil;
if (options.msgPour) smsg += options.msgPour;
expliquer(smsg);
smsg = target.token.get('name') + " fait " + tr.texte;
if (tr.reussite) {
smsg += " => réussite";
if (options.msgReussite) smsg += options.msgReussite;
} else {
smsg += " => échec";
if (options.msgRate) smsg += options.msgRate;
}
expliquer(smsg);
afterSave(tr.reussite, tr.texte);
});
}
function partialSave(ps, target, showTotal, dmgDisplay, total, expliquer, evt, afterSave) {
if (ps.partialSave !== undefined) {
if ((ps.partialSave.carac == 'CON' || ps.partialSave.carac2 == 'CON') && estNonVivant(target)) {
expliquer("Les créatures non-vivantes sont immnunisées aux attaques qui demandent un test de constitution");
afterSave({
succes: true,
dmgDisplay: '0',
total: 0,
showTotal: false
});
return;
}
if (target.partialSaveAuto) {
if (showTotal) dmgDisplay = '(' + dmgDisplay + ')';
afterSave({
succes: true,
dmgDisplay: dmgDisplay + '/2',
total: Math.ceil(total / 2),
showTotal: true
});
return;
}
save(ps.partialSave, target, expliquer, {
msgPour: " pour réduire les dégâts",
msgReussite: ", dégâts divisés par 2",
attaquant: ps.attaquant
}, evt,
function(succes, rollText) {
if (succes) {
if (showTotal) dmgDisplay = "(" + dmgDisplay + ")";
dmgDisplay = dmgDisplay + " / 2";
showTotal = true;
total = Math.ceil(total / 2);
} else {}
afterSave({
succes: succes,
dmgDisplay: dmgDisplay,
total: total,
showTotal: showTotal
});
});
} else afterSave();
}
// Fonction asynchrone
// displayRes est optionnel, et peut avoir 2 arguments
// - un texte affichant le jet de dégâts
// - la valeur finale des dégâts infligés
function dealDamage(target, dmg, otherDmg, evt, crit, options, explications, displayRes) {
if (options === undefined) options = {};
var expliquer = function(msg) {
if (explications) explications.push(msg);
else sendChar(target.charId, msg);
};
if (attributeAsBool(target, 'intangible') ||
attributeAsBool(target, 'ombreMortelle') ||
(options.aoe === undefined &&
attributeAsBool(target, 'formeGazeuse'))) {
expliquer("L'attaque passe à travers de " + target.token.get('name'));
if (displayRes) displayRes('0', 0);
return 0;
}
if (options.asphyxie && estNonVivant(target)) {
expliquer("L'asphyxie est sans effet sur une créature non-vivante");
if (displayRes) displayRes('0', 0);
return 0;
}
crit = crit || 1;
otherDmg = otherDmg || [];
var dmgDisplay = dmg.display;
var dmgTotal = dmg.total;
var showTotal = false;
if (crit > 1) {
dmgDisplay += " X " + crit;
dmgTotal = dmgTotal * crit;
if (options.affute) {
var bonusCrit = randomInteger(6);
dmgDisplay = "(" + dmgDisplay + ")+" + bonusCrit;
dmgTotal += bonusCrit;
} else {
showTotal = true;
}
}
//On trie les DM supplémentaires selon leur type
var dmgParType = {};
otherDmg.forEach(function(d) {
if (_.has(dmgParType, d.type)) dmgParType[d.type].push(d);
else dmgParType[d.type] = [d];
});
// Dommages de même type que le principal, mais à part, donc non affectés par les critiques
var mainDmgType = dmg.type;
var dmgExtra = dmgParType[mainDmgType];
if (dmgExtra && dmgExtra.length > 0) {
if (crit > 1) dmgDisplay = "(" + dmgDisplay + ")";
showTotal = true;
var count = dmgExtra.length;
dmgExtra.forEach(function(d) {
count--;
partialSave(d, target, false, d.display, d.total, expliquer, evt,
function(res) {
if (res) {
dmgTotal += res.total;
dmgDisplay += "+" + res.dmgDisplay;
} else {
dmgTotal += d.total;
dmgDisplay += "+" + d.display;
}
if (count === 0) dealDamageAfterDmgExtra(target, mainDmgType, dmgTotal, dmgDisplay, showTotal, dmgParType, dmgExtra, crit, options, evt, expliquer, displayRes);
});
});
} else {
return dealDamageAfterDmgExtra(target, mainDmgType, dmgTotal, dmgDisplay, showTotal, dmgParType, dmgExtra, crit, options, evt, expliquer, displayRes);
}
}
function applyRDMagique(rdMagique, dmgType, total, display) {
if (total && rdMagique && rdMagique > 0) {
switch (dmgType) {
case 'normal':
case 'poison':
case 'maladie':
if (total < rdMagique) {
display += "-" + total;
rdMagique -= total;
total = 0;
} else {
display += "-" + rdMagique;
total -= rdMagique;
rdMagique = 0;
}
return {
total: total,
rdMagique: rdMagique,
display: display
};
default:
return;
}
}
return;
}
function applyRDSauf(rds, dmgType, total, display) {
if (total) {
for (var saufType in rds) {
var rd = rds[saufType];
if (rd < 1 || saufType == dmgType) break;
if (total < rd) {
display += "-" + total;
rds[saufType] -= total;
total = 0;
} else {
display += "-" + rd;
total -= rd;
rds[saufType] = 0;
}
}
}
return {
total: total,
display: display
};
}
function dealDamageAfterDmgExtra(target, mainDmgType, dmgTotal, dmgDisplay, showTotal, dmgParType, dmgExtra, crit, options, evt, expliquer, displayRes) {
var rdMain = typeRD(target.charId, mainDmgType);
if (options.vampirise) {
rdMain += attributeAsInt(target, 'RD_drain', 0);
}
if (rdMain > 0 && dmgTotal > 0) {
dmgTotal -= rdMain;
if (dmgTotal < 0) {
rdMain += dmgTotal;
dmgTotal = 0;
}
dmgDisplay += " - " + rdMain;
showTotal = true;
}
var rdMagique;
if (options.magique) rdMagique = 0;
else rdMagique = typeRD(target.charId, 'sauf_magique');
if (rdMagique) showTotal = true;
var resMagique = applyRDMagique(rdMagique, mainDmgType, dmgTotal, dmgDisplay);
if (resMagique) {
rdMagique = resMagique.rdMagique;
dmgTotal = resMagique.total;
dmgDisplay = resMagique.display;
}
var rdElems = 0;
if (attributeAsBool(target, 'protectionContreLesElements')) {
rdElems = charAttributeAsInt(target, 'voieDeLaMagieElementaire', 0) * 2;
}
if (rdElems > 0 && dmgTotal > 0 && estElementaire(mainDmgType)) {
if (dmgTotal > rdElems) {
dmgDisplay += ' - ' + rdElems;
dmgTotal -= rdElems;
rdElems = 0;
} else {
dmgDisplay += ' - ' + dmgTotal;
rdElems -= dmgTotal;
dmgTotal = 0;
}
}
var rdSauf = [];
if (target.attrs === undefined) {
target.attrs = findObjs({
_type: 'attribute',
_characterid: target.charId
});
}
target.attrs.forEach(function(attr) {
var attrName = attr.get('name');
if (attrName.startsWith('RD_sauf_')) {
if (attrName == 'RD_sauf_magique') return;
var rds = parseInt(attr.get('current'));
if (isNaN(rds) || rds < 1) return;
rdSauf[attrName.substr(8)] = rds;
}
});
var resSauf = applyRDSauf(rdSauf, mainDmgType, dmgTotal, dmgDisplay);
dmgTotal = resSauf.total;
dmgDisplay = resSauf.display;
var invulnerable = charAttributeAsBool(target, 'invulnerable');
var mitigate = function(dmgType, divide, zero) {
if (estElementaire(dmgType)) {
if (invulnerable) {
divide();
}
if (dmgType == 'froid' && attributeAsBool(target, 'masqueMortuaire')) {
divide();
}
} else {
if ((dmgType == 'poison' || dmgType == 'maladie') && (invulnerable || estNonVivant(target))) {
zero();
} else if (attributeAsBool(target, 'armureMagique')) {
divide();
}
}
};
// Damage mitigaters for main damage
mitigate(mainDmgType,
function() {
dmgTotal = Math.ceil(dmgTotal / 2);
if (dmgExtra) dmgDisplay = "(" + dmgDisplay + ")";
dmgDisplay += " / 2";
showTotal = true;
},
function() {
dmgTotal = 0;
});
// Other sources of damage
// First count all other sources of damage, for synchronization
var count = 0;
for (var dt in dmgParType) {
count += dmgParType[dt].length;
}
var dealOneType = function(dmgType) {
if (dmgType == mainDmgType) {
count -= dmgParType[dmgType].length;
if (count === 0) dealDamageAfterOthers(target, crit, options, evt, expliquer, displayRes, dmgTotal, dmgDisplay, showTotal);
return; //type principal déjà géré
}
showTotal = true;
var dm = 0;
var typeDisplay = "";
var typeCount = dmgParType[dmgType].length;
dmgParType[dmgType].forEach(function(d) {
partialSave(d, target, false, d.display, d.total, expliquer, evt,
function(res) {
if (res) {
dm += res.total;
if (typeDisplay === '') typeDisplay = res.dmgDisplay;
else typeDisplay += "+" + res.dmgDisplay;
} else {
dm += d.total;
if (typeDisplay === '') typeDisplay = d.display;
else typeDisplay += "+" + d.display;
}
typeCount--;
if (typeCount === 0) {
var rdl = typeRD(target.charId, dmgType);
if (rdl > 0 && dm > 0) {
dm -= rdl;
if (dm < 0) {
rdl += dm;
dm = 0;
}
typeDisplay += "-" + rdl;
}
var resMagique = applyRDMagique(rdMagique, dmgType, dm, typeDisplay);
if (resMagique) {
rdMagique = resMagique.rdMagique;
dm = resMagique.total;
typeDisplay = resMagique.display;
}
if (rdElems > 0 && dm > 0 && estElementaire(dmgType)) {
if (dm > rdElems) {
typeDisplay += ' - ' + rdElems;
dm -= rdElems;
rdElems = 0;
} else {
typeDisplay += ' - ' + dm;
rdElems -= dm;
dm = 0;
}
}
var resSauf = applyRDSauf(rdSauf, dmgType, dm, typeDisplay);
dm = resSauf.total;
typeDisplay = resSauf.display;
mitigate(dmgType,
function() {
dm = Math.ceil(dm / 2);
if (dmgParType[dmgType].length > 1) typeDisplay = "(" + typeDisplay + ")";
typeDisplay += " / 2";
},
function() {
dm = 0;
});
dmgTotal += dm;
dmgDisplay += "+" + typeDisplay;
}
count--;
if (count === 0) dealDamageAfterOthers(target, crit, options, evt, expliquer, displayRes, dmgTotal, dmgDisplay, showTotal);
});
});
};
if (count > 0) {
for (var dmgType in dmgParType) {
dealOneType(dmgType);
}
} else {
return dealDamageAfterOthers(target, crit, options, evt, expliquer, displayRes, dmgTotal, dmgDisplay, showTotal);
}
}
//Appelé quand on met à 0 PV
function mort(personnage, evt) {
if (charAttributeAsBool(personnage, 'exsangue') && !attributeAsBool(personnage, 'etatExsangue')) {
setTokenAttr(personnage, 'etatExsangue', true, evt, "continue à agir malgré son état");
return;
}
setState(personnage, 'mort', true, evt);
var targetPos = {
x: personnage.token.get('left'),
y: personnage.token.get('top')
};
spawnFxBetweenPoints(targetPos, {
x: 400,
y: 400
}, "splatter-blood");
}
function dmgNaturel(options) {
if (options.nature) return true;
if (options.artificiel) return false;
var attaquant = options.attaquant;
if (attaquant === undefined) return false;
if (charAttributeAsBool(attaquant, 'animal')) return true;
if (charAttributeAsBool(attaquant, 'insecte')) return true;
var attr = findObjs({
_type: 'attribute',
_characterid: attaquant.charId,
});
var attrProfile = attr.filter(function(a) {
return a.get('name') == 'PROFIL';
});
if (attrProfile.length > 0) {
switch (attrProfile[0].get('current').trim().toLowerCase()) {
case 'animal':
case 'insecte':
return true;
}
}
var attrRace = attr.filter(function(a) {
return a.get('name') == 'RACE';
});
if (attrRace.length === 0) return false;
var charRace = attrRace[0].get('current').trim().toLowerCase();
switch (charRace) {
case 'insecte':
case 'ankheg':
case 'araignée':
case 'araignee':
case 'guêpe':
case 'libellule':
case 'scarabée':
case 'scorpion':
case 'strige':
case 'animal':
case 'aigle':
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 'mammouth':
case 'gorille':
case 'griffon':
case 'hipogriffe':
case 'hydre':
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 dealDamageAfterOthers(target, crit, options, evt, expliquer, displayRes, dmgTotal, dmgDisplay, showTotal) {
var charId = target.charId;
var token = target.token;
// Now do some dmg mitigation rolls, if necessary
if ((options.distance || options.aoe) &&
attributeAsBool(target, 'a_couvert')) {
if (showTotal) dmgDisplay = "(" + dmgDisplay + ")";
dmgDisplay += " / 2";
dmgTotal = Math.ceil(dmgTotal / 2);
showTotal = true;
}
partialSave(options, target, showTotal, dmgDisplay, dmgTotal,
expliquer, evt,
function(saveResult) {
if (saveResult) {
dmgTotal = saveResult.total;
dmgDisplay = saveResult.dmgDisplay;
showTotal = saveResult.showTotal;
}
var rd = charAttributeAsInt(charId, 'RDS', 0);
if (crit > 1) rd += charAttributeAsInt(charId, 'RD_critique', 0);
if (options.tranchant) rd += charAttributeAsInt(charId, 'RD_tranchant', 0);
if (options.percant) rd += charAttributeAsInt(charId, 'RD_percant', 0);
if (options.contondant) rd += charAttributeAsInt(charId, 'RD_contondant', 0);
if (options.distance) {
var piqures = charAttributeAsInt(charId, 'puquresDInsecte', 0);
if (piqures > 0 && charAttributeAsBool(target, 'DEFARMUREON') && charAttributeAsInt(charId, 'DEFARMURE', 0) > 5) rd += piqures;
}
if (attributeAsBool(target, 'masqueMortuaire')) rd += 2;
var rdNature = charAttributeAsInt(target, 'RD_nature', 0);
if (rdNature > 0 && dmgNaturel(options)) rd += rdNature;
if (target.defautCuirasse) rd = 0;
if (options.intercepter) rd += options.intercepter;
if (target.extraRD) {
rd += target.extraRD;
expliquer(target.tokName + " dévie le coup sur son armure");
}
if (rd > 0) {
if (showTotal) dmgDisplay = "(" + dmgDisplay + ") - " + rd;
else {
dmgDisplay += " - " + rd;
showTotal = true;
}
}
dmgTotal -= rd;
if (dmgTotal < 0) dmgTotal = 0;
if (options.divise) {
dmgTotal = dmgTotal / options.divise;
dmgDisplay = "(" + dmgDisplay + ")/" + options.divise;
showTotal = true;
}
// compute effect on target
if (options.pointsVitaux && dmgTotal > 0) { //dégâts retardés pour une pression mortelle
var pMortelle = tokenAttribute(target, 'pressionMortelle');
var dmgPMort = dmgTotal;
var numberPMort = 1;
if (pMortelle.length > 0) {
dmgPMort += pMortelle[0].get('current');
numberPMort += pMortelle[0].get('max');
}
setTokenAttr(target, 'pressionMortelle', dmgPMort, evt, undefined,
numberPMort);
} else {
var bar1 = parseInt(token.get('bar1_value'));
var pvmax = parseInt(token.get('bar1_max'));
if (isNaN(bar1)) {
error("Pas de points de vie chez la cible", token);
bar1 = 0;
pvmax = 0;
} else if (isNaN(pvmax)) {
pvmax = bar1;
token.set("bar1_max", bar1);
}
var manaAttr = findObjs({
_type: 'attribute',
_characterid: charId,
name: 'PM'
});
var hasMana = false;
if (manaAttr.length > 0) {
var manaMax = parseInt(manaAttr[0].get('max'));
hasMana = !isNaN(manaMax) && manaMax > 0;
}
var tempDmg = 0;
if (hasMana) {
tempDmg = attributeAsInt(target, 'DMTEMP', 0);
} else {
tempDmg = parseInt(token.get("bar2_value"));
if (isNaN(tempDmg)) {
if (options.tempDmg) { //then try to set bar2 correctly
var link = token.get("bar1_link");
if (link === "") {
token.set("bar2_max", pvmax);
} else {
var tmpHitAttr =
findObjs({
_type: "attribute",
_characterid: charId,
name: "DMTEMP"
});
var dmTemp;
if (tmpHitAttr.length === 0) {
dmTemp =
createObj("attribute", {
characterid: charId,
name: "DMTEMP",
current: 0,
max: pvmax
});
} else {
dmTemp = tmpHitAttr[0];
}
token.set("bar2_max", pvmax);
token.set("bar2_link", dmTemp.id);
}
}
tempDmg = 0;
}
}
if (options.tempDmg) {
var oldTempDmg = tempDmg;
tempDmg += dmgTotal;
if (tempDmg > pvmax) tempDmg = pvmax;
if (hasMana) {
setTokenAttr(target, 'DMTEMP', tempDmg, evt);
} else {
affectToken(token, 'bar2_value', oldTempDmg, evt);
updateCurrentBar(token, 2, tempDmg);
}
} else {
affectToken(token, 'bar1_value', bar1, evt);
if (bar1 > 0 && bar1 <= dmgTotal && charAttributeAsBool(charId, 'instinctDeSurvieHumain')) {
dmgTotal = dmgTotal / 2;
if (dmgTotal < 1) dmgTotal = 1;
dmgDisplay += "/2";
showTotal = true;
expliquer("L'instinct de survie aide à réduire une attaque fatale");
}
bar1 = bar1 - dmgTotal;
if (bar1 <= 0) {
if (charAttributeAsBool(charId, 'sergent') &&
!attributeAsBool(target, 'sergentUtilise')) {
expliquer(token.get('name') + " évite l'attaque in-extremis");
setTokenAttr(target, 'sergentUtilise', true, evt);
} else {
updateCurrentBar(token, 1, 0);
if (charAttributeAsBool(charId, 'baroudHonneur')) {
expliquer(token.get('name') + " devrait être mort, mais il continue à se battre !");
setTokenAttr(target, 'baroudHonneurActif', true, evt);
} else {
var defierLaMort = charAttributeAsInt(charId, 'defierLaMort', 0);
if (defierLaMort > 0) {
save({
carac: 'CON',
seuil: defierLaMort
}, target, expliquer, {
msgPour: " pour défier la mort",
msgReussite: ", conserve 1 PV"
}, evt,
function(reussite, rollText) {
if (reussite) {
updateCurrentBar(token, 1, 1);
bar1 = 1;
setTokenAttr(target, 'defierLaMort', defierLaMort + 10, evt);
} else mort(target, evt);
if (bar1 > 0 && tempDmg >= bar1) { //assomé
setState(target, 'assome', true, evt);
}
if (showTotal) dmgDisplay += " = " + dmgTotal;
if (displayRes === undefined) return dmgDisplay;
displayRes(dmgDisplay, dmgTotal);
});
if (displayRes === undefined) return dmgDisplay;
return;
} else mort(target, evt);
}
}
} else { // bar1>0
updateCurrentBar(token, 1, bar1);
}
}
if (bar1 > 0 && tempDmg >= bar1) { //assomé
setState(target, 'assome', true, evt);
}
if (showTotal) dmgDisplay += " = " + dmgTotal;
}
if (displayRes === undefined) return dmgDisplay;
displayRes(dmgDisplay, dmgTotal);
});
return dmgDisplay;
}
function improve_image(image_url) {
image_url = image_url.replace('/med.png', '/thumb.png');
image_url = image_url.substring(0, image_url.indexOf('?'));
return image_url;
}
function startFramedDisplay(playerId, action, perso1, perso2, to_displayname) {
var player = getObj('player', playerId);
var display = {
output: '',
isOdd: true,
isfirst: true,
};
if (player === undefined && !to_displayname) {
error('Aucun déstinataire de précisé...');
return display;
}
var playerBGColor = '#333';
var playerTXColor = '#FFF';
var displayname = 'Inconnu';
if (player !== undefined) {
playerBGColor = player.get("color");
playerTXColor = (getBrightness(playerBGColor) < 50) ? "#FFF" : "#000";
displayname = player.get('displayname');
}
var res = '/direct ';
var whisper = false;
if (to_displayname) {
var who;
if (to_displayname !== true) who = to_displayname;
else who = displayname;
res = '/w "' + who + '" ';
whisper = true;
}
var name1, name2, avatar1, avatar2 = '';
var character2;
if (perso2) {
character2 = getObj('character', perso2.charId);
if (character2 !== undefined) {
avatar2 = '';
name2 = perso2.tokName;
if (name2 === undefined) name2 = perso2.token.get('name');
name2 = '' + name2 + '';
}
}
var character1;
if (perso1) {
character1 = getObj('character', perso1.charId);
if (character1 !== undefined) {
avatar1 = '
';
if (perso1.tokName) name1 = perso1.tokName;
else name1 = perso1.token.get('name');
name1 = '' + name1 + '';
}
}
res +=
'
Vous n'avez aucun consommable");
else addLineToFramedDisplay(display, 'Cliquez sur le consommable pour l\'utiliser ou sur r pour l\'échanger avec un autre personnage.');
sendChat('', endFramedDisplay(display));
});
}); //fin du getSelected
}
// Le premier argument est le message reçu : msg => String
// Le deuxième nous indique si on effectue un échange ou pas : echange => true/false
function utiliseConsommable(msg, echange) {
var cmd = msg.content.split(' ');
if ((!echange && cmd.length < 3) || (echange && cmd.length < 5)) {
error("Erreur interne de consommables", cmd);
return;
}
//perso1 = token avec qui utilise (ou qui va échanger) le consommable
var perso1 = tokenOfId(cmd[1]);
if (perso1 === undefined) {
log("Propriétaire perdu");
sendChat('COF', "Plus possible d'utiliser cette action. Réafficher les consommables.");
return;
}
var perso2;
if (echange) {
//perso2 = token avec lequel on va faire l'échange
perso2 = tokenOfId(cmd[2]);
if (perso2 === undefined) {
log("Destinataire perdu");
sendChat('COF', "Erreur concernant le destinataire. Veuillez réessayer.");
return;
}
}
// Vérifie les droits d'utiliser le consommable
if (msg.selected && msg.selected.length == 1) {
var utilisateur = tokenOfId(msg.selected[0]._id);
if (utilisateur === undefined) {
sendChat('COF', "Le token sélectionné n'est pas valide");
return;
}
var d = distanceCombat(perso1.token, utilisateur.token);
if (d > 0) {
sendChar(utilisateur.charId, "est trop loin de " + perso1.token.get('name') + " pour utiliser ses objets");
return;
}
perso1 = utilisateur;
} else {
//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 à utiliser/échanger de perso1
var attr1;
if (echange) attr1 = getObj('attribute', cmd[4]);
else attr1 = getObj('attribute', cmd[2]);
var attrName = attr1.get('name').trim();
var effet = attr1.get('max').trim();
if (attr1 === undefined) {
log("Attribut a changé/perdu");
log(cmd);
sendChat('COF', "Plus possible d'utiliser cette action. Veuillez réafficher les consommables.");
return;
}
//Nom du consommable (pour affichage)
var consName = attrName.substring(attrName.indexOf('_') + 1);
consName = "" + consName.replace(/_/g, ' ').trim() + "";
// quantité actuelle pour perso1
var quantite1 = parseInt(attr1.get('current'));
if (isNaN(quantite1) || quantite1 < 1) {
attr1.set('current', 0);
sendChat('COF', "/w " + perso1.token.get('name') + " Vous ne disposez plus de " + consName);
return;
}
var evt = {
type: "Utilisation de consommable",
attributes: []
};
if (echange) {
//c'est un échange
evt.type = "Échange de consommable";
if (perso1.charId != perso2.charId) {
// on baisse la valeur de 1 du consommable qu'on s'apprête à échanger
attr1.set('current', quantite1 - 1);
evt.attributes.push({
attribute: attr1,
current: quantite1,
max: effet
});
// ajout du consommable dans perso2 :
var attributes = findObjs({
_type: 'attribute',
_characterid: perso2.charId
});
var found = false;
var quantite2;
// on recherche si le consommable existe chez perso2
attributes.forEach(function(attr2) {
var attrName2 = attr2.get('name').trim();
if (!found && attrName == attrName2) {
if (attr2.get('max').trim() != effet) {
error("Échange dangereux : pas le même effet pour le consommable selon le personnage \n" + effet + "\n" + attr2.get('max'), attr2);
return;
}
found = true;
// si oui, on augmente sa quantité de 1
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;
}
});
// si le consommable n'a pas été trouvé, on le créé avec une valeur de 1.
if (!found) {
var attr2 = createObj("attribute", {
name: attrName,
current: 1,
max: effet,
characterid: perso2.charId
});
evt.attributes.push({
attribute: attr2,
current: null,
});
}
// on envoie un petit message précisant la résultante de l'action.
sendChat('COF', "Echange entre " + perso1.token.get('name') + " et " + perso2.token.get('name') + " terminée.");
sendChat('COF', "/w " + perso1.token.get('name') + " Il vous reste " + parseInt(attr1.get('current')) + " " + consName + ".");
sendChat('COF', "/w " + perso2.token.get('name') + " Vous possédez désormais " + quantite2 + " " + consName + ".");
// le MJ est notifié :
sendChat('COF', "/w GM " + perso1.token.get('name') + " vient de donner 1 " + consName + " à " + perso2.token.get('name') + ".");
} else {
sendChat('COF', "/w " + perso1.token.get('name') + " Vous ne pouvez pas échanger un consommable avec vous-même ...");
}
} else {
// on utilise le consommable
attr1.set('current', quantite1 - 1);
evt.attributes.push({
attribute: attr1,
current: quantite1,
max: effet
});
var start = msg.content.indexOf(' --message ') + 10;
sendChar(perso1.charId, msg.content.substring(start));
}
addEvent(evt);
}
//asynchrone
//callback(resultat, crit):
// resultat peut être 0, 1 ou 2 : 0 = match null, 1 le perso 1 gagne, 2 le perso 2 gagne.
// crit peut être 1 si un des deux perso a fait une réussite critique et pas l'autre, -1 si un des personnage a fait un échec critique et pas l'autre, et 0 sinon
function testOppose(perso1, carac1, perso2, carac2, explications, evt, callback) {
if (carac2 === undefined) carac2 = carac1;
var nom1 = perso1.token.get('name');
var nom2 = perso2.token.get('name');
jetCaracteristique(perso1, carac1, {}, function(rt1) {
jetCaracteristique(perso2, carac2, {}, function(rt2) {
explications.push("Jet de " + carac1 + " de " + nom1 + " :" + rt1.texte);
explications.push("Jet de " + carac2 + " de " + nom2 + " :" + rt2.texte);
var reussite;
var crit = 0;
if (rt1.total > rt2.total) reussite = 1;
else if (rt2.total > rt1.total) reussite = 2;
else reussite = 0;
if (rt1.echecCritique) {
if (!rt2.echecCritique) {
reussite = 2;
crit = -1;
}
} else if (rt2.echecCritique) {
reussite = 1;
crit = -1;
} else if (rt1.critique) {
if (!rt2.critique) {
reussite = 1;
crit = 1;
}
} else if (rt2.critique) {
reussite = 2;
crit = 1;
}
switch (reussite) {
case 1:
diminueMalediction(perso2, evt);
break;
case 2:
diminueMalediction(perso1, evt);
break;
}
callback(reussite, crit);
}); //Fin du jet du deuxième perso
}); //Fin du jet du premier perso
}
function provocation(msg) {
var args = msg.content.split(' --');
var cmd = args[0].split(' ');
if (cmd.length < 3) {
error("La commande !cof-provocation requiert 2 arguments", cmd);
return;
}
var voleur = tokenOfId(cmd[1]);
if (voleur === undefined) {
error("Le premier argument de !cof-provocation n'est pas un token valide");
return;
}
var cible = tokenOfId(cmd[2]);
if (cible === undefined) {
error("Le deuxième argument de !cof-provocation n'est pas un token valide");
return;
}
var nomVoleur = voleur.token.get('name');
var nomCible = cible.token.get('name');
var display =
startFramedDisplay(msg.playerid, 'Provocation', voleur, cible);
var evt = {
type: 'Provocation'
};
var jets = [];
testOppose(voleur, 'CHA', cible, 'INT', jets, evt, function(res, crit) {
jets.forEach(function(l) {
addLineToFramedDisplay(display, l);
});
var 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 provocation réussit...";
break;
case 0:
case 1:
reussite = "La provocation réussit tout juste.";
}
break;
case 1:
switch (crit) {
case -1:
reussite = nomCible + " marche complètement, il attaque " + nomVoleur;
break;
case 0:
reussite = "La provocation réussit.";
break;
case 1:
reussite = "La provocation est une réussite critique !";
}
break;
case 2:
switch (crit) {
case -1:
reussite = "Échec critique de la provocation !";
break;
case 0:
reussite = "La provocation échoue";
break;
case 1:
reussite = nomCible + " voit clair dans le jeu de " + nomCible + ". La provocation échoue.";
}
}
addLineToFramedDisplay(display, reussite);
addEvent(evt);
sendChat('', endFramedDisplay(display));
}); //Fin du test opposé
}
function enSelle(msg) {
var cmd = msg.content.split(' ');
if (cmd.length < 3) {
error("Il faut 2 arguments pour !cof-en-selle", cmd);
return;
}
var cavalier = tokenOfId(cmd[1]);
if (cavalier === undefined) {
error("Premier argument de !cof-en-selle incorrect", cmd);
return;
}
if (attributeAsBool(cavalier, 'monteSur')) {
sendChar(cavalier.charId, " est déjà en selle");
return;
}
var tokenC = cavalier.token;
var pageId = tokenC.get('pageid');
var monture = tokenOfId(cmd[2], cmd[2], pageId);
if (monture === undefined || !charAttributeAsBool(monture, 'monture')) {
sendChar(cavalier.charId, " ne peut pas monter là-dessus");
log(cmd);
return;
}
var tokenM = monture.token;
var nomMonture = tokenM.get('name');
if (attributeAsBool(monture, 'estMontePar')) {
sendChar(cavalier.charId, " ne peut monter sur " + nomMonture + " car elle a déjà un cavalier");
return;
}
if (distanceCombat(tokenC, tokenM, pageId) > 0) {
sendChar(cavalier.charId, " est trop loin de " + nomMonture);
return;
}
var evt = {
type: 'En selle'
};
setTokenAttr(cavalier, 'monteSur', tokenM.id, evt, " monte sur " + nomMonture, nomMonture);
setTokenAttr(monture, 'estMontePar', tokenC.id, evt, undefined, tokenC.get('name'));
setTokenAttr(monture, 'positionSurMonture', tokenC.get('left') - tokenM.get('left'), evt, undefined, tokenC.get('top') - tokenM.get('top'));
setTokenAttr(monture, 'directionSurMonture', tokenC.get('rotation') - tokenM.get('rotation'), evt);
addEvent(evt);
}
function listeElixirs(rang) {
var liste = [{
nom: 'fortifiant',
action: "!cof-fortifiant $rang"
}];
if (rang < 2) return liste;
liste.push({
nom: 'feu_grégeois',
action: "!cof-dmg $rangd6 --feu --psave DEX [[10+@{selected|INT}]] --disque @{target|token_id} 3 10 --lanceur @{selected|token_id}"
});
if (rang < 3) return liste;
liste.push({
nom: 'élixir_de_guérison',
action: "!cof-soin 3d6+$INT"
});
if (rang < 4) return liste;
liste.push({
nom: "potion_d_agrandissement",
action: "!cof-effet-temp agrandissement [[5+$INT]]"
});
liste.push({
nom: "potion_de_forme_gazeuse",
action: "!cof-effet-temp formeGazeuse [[1d4+$INT]]"
});
liste.push({
nom: "potion_de_protection_contre_les_éléments",
action: "!cof-effet-temp protectionContreLesElements [[5+$INT]]"
});
liste.push({
nom: "potion_d_armure_de_mage",
action: "!cof-effet-combat armureDuMage"
});
liste.push({
nom: "potion_de_chute_ralentie",
action: "est léger comme une plume."
});
if (rang < 5) return liste;
liste.push({
nom: "potion_d_invisibilité",
action: "devient invisible"
});
liste.push({
nom: "potion_de_vol",
action: "se met à voler"
});
liste.push({
nom: "potion_de_respiration_aquatique",
action: "peut respirer sous l'eau"
});
liste.push({
nom: "potion_de_flou",
action: "devient flou (/2 les DM)"
});
liste.push({
nom: "potion_de_hâte",
action: "a une action supplémentaire par tour"
});
return liste;
}
//!cof-creer-elixir token_id nom_token elixir
function creerElixir(msg) {
var options = parseOptions(msg);
var cmd = options.cmd;
if (cmd === undefined || cmd.length < 4) {
error("Pas assez d'arguments pour !cof-creer-elixir", msg.content);
return;
}
var forgesort = tokenOfId(cmd[1], cmd[2], options.pageId);
if (forgesort === undefined) {
if (msg.selected && msg.selected.length == 1) {
forgesort = tokenOfId(msg.selected[0]._id);
}
if (forgesort === undefined) {
error("Impossible de savoir qui crée l'élixir", cmd);
return;
}
}
var voieDesElixirs = charAttributeAsInt(forgesort, 'voieDesElixirs', 0);
if (voieDesElixirs < 1) {
sendChar(forgesort.charId, " ne connaît pas la Voie des Élixirs");
return;
}
var elixir = listeElixirs(voieDesElixirs).find(function(i) {
if (i.nom == cmd[3]) return true;
return false;
});
if (elixir === undefined) {
error(forgesort.token.get('name') + " est incapable de créer " + cmd[3], cmd);
return;
}
var evt = {
type: "Création d'élixir"
};
if (limiteRessources(forgesort, options, 'elixirsACreer', 'élixirs à créer', evt)) return;
var attrName = 'elixir_' + elixir.nom;
var message = "crée un " + elixir.nom.replace(/_/g, ' ');
var attr = tokenAttribute(forgesort, attrName);
if (attr.length === 0) {
var action = elixir.action.replace(/\$rang/g, voieDesElixirs);
action = action.replace(/\$INT/g, modCarac(forgesort, 'INTELLIGENCE'));
setTokenAttr(forgesort, attrName, 1, evt, message, action);
} else {
var nb = parseInt(attr[0].get('current'));
if (isNaN(nb) || nb < 1) nb = 0;
setTokenAttr(forgesort, attrName, nb + 1, evt, message);
}
addEvent(evt);
}
function gestionElixir(msg) {
var playerId = msg.playerid;
var player = getObj('player', playerId);
getSelected(msg, function(selected) {
iterSelected(selected, function(forgesort) {
var voieDesElixirs = charAttributeAsInt(forgesort, 'voieDesElixirs', 0);
if (voieDesElixirs < 1) {
sendChar(forgesort.charId, " ne connaît pas la Voie des Élixirs");
return;
}
var elixirsACreer = voieDesElixirs * 2;
var attrElixirs = tokenAttribute(forgesort, 'elixirsACreer');
if (attrElixirs.length === 0) {
attrElixirs = setTokenAttr(forgesort, 'elixirsACreer', elixirsACreer, {});
} else {
attrElixirs = attrElixirs[0];
elixirsACreer = parseInt(attrElixirs.get('current'));
if (isNaN(elixirsACreer)) elixirsACreer = 0;
}
var titre;
if (elixirsACreer < 1)
titre = "Impossible de créer un autre élixir aujourd'hui";
else titre = "Encore " + elixirsACreer + " élixirs à créer";
var display = startFramedDisplay(playerId, titre, forgesort, undefined, true);
listeElixirs(voieDesElixirs).forEach(function(elixir) {
var nbElixirs = 0;
var attr = tokenAttribute(forgesort, 'elixir_' + elixir.nom);
if (attr.length > 0) {
attr = attr[0];
nbElixirs = parseInt(attr.get('current'));
if (isNaN(nbElixirs) || nbElixirs < 0) nbElixirs = 0;
}
var nomElixir = elixir.nom.replace(/_/g, ' ');
var options = '';
var action;
if (elixirsACreer > 0) {
action = "!cof-creer-elixir " + forgesort.token.id + ' ' + forgesort.token.get('name') + ' ' + elixir.nom;
options += bouton(action, nbElixirs, forgesort, attrElixirs);
} else {
options = nbElixirs + ' ';
}
if (nbElixirs > 0) {
action = elixir.action;
action = action.replace(/\$rang/g, voieDesElixirs);
action = action.replace(/\$INT/g, modCarac(forgesort, 'INTELLIGENCE'));
options += bouton(action, nomElixir, forgesort, attr);
//TODO: Rajouter la possibilité de donner un élixir à un autre token
} else {
options += nomElixir;
}
addLineToFramedDisplay(display, options);
});
sendChat('', endFramedDisplay(display));
});
}); //Fin du getSelected
}
function rageDuBerserk(msg) {
getSelected(msg, function(selection) {
if (selection.length === 0) {
sendPlayer(msg, "Pas de token sélectionné pour la rage");
return;
}
iterSelected(selection, function(perso) {
var evt = {
type: "Rage"
};
if (attributeAsBool(perso, 'rageDuBerserk')) {
//Jet de sagesse difficulté 13 pour sortir de cet état
var options = {};
var display = startFramedDisplay(msg.playerid, "Essaie de calmer sa rage", perso);
testCaracteristique(perso, 'SAG', 13, options, evt,
function(tr) {
addLineToFramedDisplay(display, "Résultat du jet de SAG : " + tr.texte);
addEvent(evt);
if (tr.reussite) {
addLineToFramedDisplay(display, "C'est réussi, " + perso.token.get('name') + " se calme.");
removeTokenAttr(perso, 'rageDuBerserk', evt);
} else {
var msgRate = "C'est raté, " + perso.token.get('name') + " reste enragé";
//TODO : ajouter un bouton de chance
addLineToFramedDisplay(display, msgRate);
}
sendChat('', endFramedDisplay(display));
});
} else {
//Le barbare passe en rage
if (!state.COFantasy.combat) {
initiative(selection, evt);
}
setTokenAttr(perso, 'rageDuBerserk', true, evt, "entre dans une rage berserk !");
}
}); //fin iterSelected
}); //fin getSelected
}
//!cof-arme-secrete @{selected|token_id} @{target|token_id}
// TODO: ajouter la possibilité d'utiliser la chance
function armeSecrete(msg) {
var cmd = msg.content.split(' ');
if (cmd.length < 3) {
error("Il faut deux arguments à !cof-arme-secrete", cmd);
return;
}
var barde = tokenOfId(cmd[1]);
var cible = tokenOfId(cmd[2]);
if (barde === undefined || cible === undefined) {
error("Token non valide pour l'arme secrète", cmd);
return;
}
if (attributeAsInt(barde, 'armeSecreteBardeUtilisee')) {
sendChar(barde.charId, "a déjà utilisé son arme secrète durant ce combat");
return;
}
var evt = {
type: 'Arme secrète'
};
if (!state.COFantasy.combat) {
initiative([{
_id: barde.token.id
}, {
_id: cible.token.id
}], evt);
}
setTokenAttr(barde, 'armeSecreteBardeUtilisee', true, evt);
var intCible = charAttributeAsInt(cible, 'INTELLIGENCE', 10);
testCaracteristique(barde, 'CHA', intCible, {}, evt, function(testRes) {
var display = startFramedDisplay(msg.playerid, "Arme secrète", barde, cible);
var line = "Jet de CHA : " + testRes.texte;
if (testRes) {
line += " ≥ " + intCible;
addLineToFramedDisplay(display, line);
addLineToFramedDisplay(display, cible.token.get('name') + " est complètement déstabilisé");
setTokenAttr(cible, 'armeSecreteBarde', 1, evt, '', getInit());
} else {
line += "< " + intCible;
addLineToFramedDisplay(display, line);
addLineToFramedDisplay(display, cible.token.get('name') + " reste insensible au charme de " + barde.token.get('name'));
}
sendChat("", endFramedDisplay(display));
addEvent(evt);
}); //fin testCarac
// testRes.texte est l'affichage du jet de dé
// testRes.reussite indique si le jet est réussi
// testRes.echecCritique, testRes.critique pour le type
}
function apiCommand(msg) {
msg.content = msg.content.replace(/\s+/g, ' '); //remove duplicate whites
var command = msg.content.split(" ", 1);
// First replace inline rolls by their values
replaceInline(msg);
var evt;
switch (command[0]) {
case "!cof-jet":
jet(msg);
return;
case "!cof-resultat-jet":
resultatJet(msg);
return;
case "!cof-attack":
parseAttack(msg);
return;
case "!cof-undo":
undoEvent();
return;
case "!cof-hors-combat":
sortirDuCombat();
return;
case "!cof-nuit":
nuit(msg);
return;
case "!cof-jour":
evt = {
type: "Nouveau jour"
};
jour(evt);
addEvent(evt);
return;
case "!cof-recuperation":
recuperer(msg);
return;
case "!cof-recharger":
recharger(msg);
return;
case "!cof-chance": //deprecated
chance(msg);
return;
case "!cof-bouton-chance":
boutonChance(msg);
return;
case "!cof-bouton-rune-energie":
runeEnergie(msg);
return;
case "!cof-surprise":
surprise(msg);
return;
case "!cof-init":
if (!_.has(msg, 'selected')) {
error("Dans !cof-init : rien à faire, pas de token selectionné", msg);
return;
}
if (msg.content.indexOf('--aura') !== -1) aura_token_on_turn = true;
else aura_token_on_turn = false;
evt = {
type: "initiative"
};
initiative(msg.selected, evt);
addEvent(evt);
return;
case "!cof-attendre":
attendreInit(msg);
return;
case "!cof-statut":
statut(msg);
return;
case "!cof-armure-magique":
armureMagique(msg);
return;
case "!cof-buf-def":
bufDef(msg);
return;
case "!cof-remove-buf-def":
removeBufDef(msg);
return;
case "!cof-aoe": //deprecated
case "!cof-dmg":
dmgDirects(msg);
return;
case "!cof-set-state":
interfaceSetState(msg);
return;
case "!cof-degainer":
degainer(msg);
return;
case "!cof-echange-init":
echangeInit(msg);
return;
case "!cof-a-couvert":
aCouvert(msg);
return;
case "!cof-effet-temp":
effetTemporaire(msg);
return;
case "!cof-effet-combat":
effetCombat(msg);
return;
case "!cof-effet":
effetIndetermine(msg);
return;
case "!cof-attaque-magique":
attaqueMagique(msg);
return;
case "!cof-sommeil":
sommeil(msg);
return;
case "!cof-transe-guerison":
transeGuerison(msg);
return;
case "!cof-soin":
soigner(msg);
return;
case "!cof-aoe-soin": //Deprecated
aoeSoin(msg);
return;
case "!cof-nature-nourriciere":
natureNourriciere(msg);
return;
case "!cof-ignorer-la-douleur":
ignorerLaDouleur(msg);
return;
case "!cof-fortifiant":
fortifiant(msg);
return;
case "!cof-intercepter":
intercepter(msg);
return;
case "!cof-esquive-fatale":
esquiveFatale(msg);
return;
case "!cof-exemplaire":
exemplaire(msg);
return;
case "!cof-lancer-sort":
lancerSort(msg);
return;
case "!cof-peur":
peur(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-mur-de-force":
murDeForce(msg);
return;
case "!cof-capitaine":
devientCapitaine(msg);
return;
case "!cof-tueur-fantasmagorique":
tueurFantasmagorique(msg);
return;
case "!cof-tour-de-force":
tourDeForce(msg);
return;
case "!cof-encaisser-un-coup":
encaisserUnCoup(msg);
return;
case "!cof-absorber-au-bouclier":
absorberAuBouclier(msg);
return;
case "!cof-demarrer-statistiques":
if (state.COFantasy.statistiquesEnPause) {
state.COFantasy.statistiques = state.COFantasy.statistiquesEnPause;
state.COFantasy.statistiquesEnPause = undefined;
} else {
state.COFantasy.statistiques = {}; //remet aussi les statistiques à 0
}
return;
case "!cof-arreter-statistiques":
state.COFantasy.statistiques = undefined;
return;
case "!cof-pause-statistiques":
if (state.COFantasy.statistiques) {
state.COFantasy.statistiquesEnPause = state.COFantasy.statistiques;
state.COFantasy.statistiques = undefined;
} // sinon, ne pas écraser les statistiques déjà en pause
return;
case "!cof-statistiques":
displayStatistics(msg);
return;
case "!cof-destruction-des-morts-vivants":
destructionDesMortsVivants(msg);
return;
case "!cof-enduire-poison":
enduireDePoison(msg);
return;
case "!cof-consommables":
listeConsommables(msg);
return;
case "!cof-utilise-consommable": //Usage interne seulement
utiliseConsommable(msg, false);
return;
case "!cof-echange-consommables": //Usage interne seulement
utiliseConsommable(msg, true);
return;
case "!cof-provocation":
provocation(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-rage-du-berserk":
rageDuBerserk(msg);
return;
case "!cof-arme-secrete":
armeSecrete(msg);
return;
default:
return;
}
}
var messageEffetTemp = {
sous_tension: {
activation: "se charge d'énergie électrique",
actif: "est chargé d'énergie électrique",
fin: "n'est plus chargé d'énergie électrique"
},
a_couvert: {
activation: "reste à couvert",
actif: "est à couvert",
fin: "n'est plas à couvert"
},
image_decalee: {
activation: "décale légèrement son image",
actif: "a décalé son image",
fin: "apparaît à nouveau là où il se trouve"
},
chant_des_heros: {
activation: "écoute le chant du barde",
actif: "est inspiré par le Chant des Héros",
fin: "n'est plus inspiré par le Chant des Héros"
},
benediction: {
activation: "est touché par la bénédiction",
actif: "est béni",
fin: "l'effet de la bénédiction s'estompe"
},
peau_d_ecorce: {
activation: "donne à sa peau la consistance de l'écorce",
actif: "a la peau dure comme l'écorce",
fin: "retrouve une peau normale"
},
rayon_affaiblissant: {
activation: "est touché par un rayon affaiblissant",
actif: "est sous l'effet d'un rayon affaiblissant",
fin: "n'est plus affaibli"
},
peur: {
activation: "prend peur",
actif: "est dominé par sa peur",
fin: "retrouve du courage"
},
peurEtourdi: {
activation: "prend peur: il peut fuir ou rester recroquevillé",
actif: "est paralysé par la peur",
fin: "retrouve du courage et peut à nouveau agir"
},
aveugleTemp: {
activation: "n'y voit plus rien !",
actif: "", //Déjà affiché avec l'état aveugle
fin: "retrouve la vue"
},
ralentiTemp: {
activation: "est ralenti : une seule action, pas d'action limitée",
actif: "", //Déjà affiché avec l'état ralenti
fin: "n'est plus ralenti"
},
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"
},
putrefaction: {
activation: "vient de contracter une sorte de lèpre fulgurante",
actif: "est en pleine putréfaction",
fin: "La putréfaction s'arrête."
},
forgeron: {
activation: "enflamme son arme",
actif: "a une arme en feu",
fin: "L'arme n'est plus enflammée."
},
dmgArme1d6: {
activation: "enduit son arme d'une huile magique",
actif: "a une arme plus puissante",
fin: "L'arme retrouve sa puissance normale"
},
agrandissement: {
activation: "se met à grandir",
actif: "est vraiment très grand",
fin: "retrouve sa taille normale"
},
formeGazeuse: {
activation: "semble perdre de la consistance",
actif: "est en forme gazeuse",
fin: "retrouve sa consistance normale"
},
intangible: {
activation: "devient translucide",
actif: "est intangible",
fin: "redevient solide"
},
strangulation: {
activation: "commence à étouffer",
actif: "est étranglé",
fin: "respire enfin"
},
ombreMortelle: {
activation: "voit son ombre s'animer et l'attaquer !",
actif: "est une ombre animée",
fin: "retrouve une ombre normale"
},
dedoublement: {
activation: "voit un double translucide sortir de lui",
actif: "est un double translucide",
fin: "le double disparaît"
},
zoneDeSilence: {
activation: "n'entend plus rien",
actif: "est totalement sourd",
fin: "peut à nouveau entendre"
},
danseIrresistible: {
activation: "se met à danser",
actif: "danse malgré lui",
fin: "s'arrête de danser"
},
confusion: {
activation: "ne sait plus très bien ce qu'il fait là",
actif: "est en pleine confusion",
fin: "retrouve ses esprits"
},
murDeForce: {
activation: "fait apparaître un mur de force",
actif: "en entouré d'un mur de force",
fin: "voit son mur de force disparaître"
},
asphyxie: {
activation: "commence à manquer d'air",
actif: "étouffe",
fin: "peut à nouveau respirer"
},
forceDeGeant: {
activation: "devient plus fort",
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"
},
encaisserUnCoup: {
activation: "se place de façon à dévier un coup sur son armure",
actif: "est placé de façon à dévier un coup",
fin: "n'est plus en position pour encaisser un coup"
},
absorberUnCoup: {
activation: "se prépare à absorber un coup avec son bouclier",
actif: "est prêt à 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",
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",
actif: "est entouré d'une nuée d'insectes",
fin: "est enfin débarassé des insectes"
},
prisonVegetale: {
activation: "voit des plantes pousser et s'enrouler autour de ses jambes",
actif: "est bloqué par des plantes",
fin: "se libère des plantes"
},
protectionContreLesElements: {
activation: "lance un sort de protection contre les éléments",
actif: "est protégé contre les éléments",
fin: "n'est plus protégé contre les éléments"
},
masqueMortuaire: {
activation: "prend l'apparence de la mort",
actif: "semble mort et animé",
fin: "retrouve une apparence de vivant"
},
armeBrulante: {
activation: "sent son arme lui chauffer la main",
actif: "se brûle la main sur son arme",
fin: "sent son arme refroidir"
},
armureBrulante: {
activation: "sent son armure chauffer",
actif: "brûle dans son armure",
fin: "sent son armure refroidir"
},
masqueDuPredateur: {
activation: "prend les traits d'un prédateur",
actif: "a les traits d'un prédateur",
fin: "redevient normal"
},
aspectDeLaSuccube: {
activation: "acquiert une beauté fascinante",
actif: "est d'une beauté fascinante",
fin: "retrouve sa beauté habituelle"
},
sangMordant: {
activation: "transforme son sang",
actif: "a du sang acide",
fin: "retrouve un sang normal"
},
armeSecreteBarde: {
activation: "est déstabilisé",
actif: "est déstabilisé par une action de charme",
fin: "retrouve ses esprits"
}
};
var patternEffetsTemp =
new RegExp(_.reduce(messageEffetTemp, function(reg, msg, effet) {
var res = reg;
if (res !== "(") res += "|";
res += "^" + effet + "($|_)";
return res;
}, "(") + ")");
function estEffetTemp(name) {
return (patternEffetsTemp.test(name));
}
function effetTempOfAttribute(attr) {
var ef = attr.get('name');
if (ef === undefined || messageEffetTemp[ef]) return ef;
for (var effet in messageEffetTemp) {
if (ef.startsWith(effet + "_")) return effet;
}
error("Impossible de déterminer l'effet correspondant à " + ef, attr);
return undefined;
}
var messageEffetCombat = {
armureMagique: {
activation: "est entouré d'un halo magique",
actif: "est protégé par une armure magique",
fin: "n'est plus entouré d'un halo magique"
},
criDeGuerre: {
activation: "pousse son cri de guerre",
actif: "a effrayé ses adversaires",
fin: "n'effraie plus ses adversaires"
},
armureDuMage: {
activation: "fait apparaître un nuage magique argenté qui le protège",
actif: "est entouré d'une armure du mage",
fin: "n'a plus son armure du mage"
},
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"
},
protectionContreLeMal: {
activation: "reçoit une bénédiction de protection contre le mal",
actif: "est protégé contre le mal",
fin: "n'est plus protégé contre le mal"
},
rageDuBerserk: {
activation: "entre dans une rage berserk",
actif: "est dans une rage berserk",
fin: "retrouve son calme"
}
};
var patternEffetsCombat =
new RegExp(_.reduce(messageEffetCombat, function(reg, msg, effet) {
var res = reg;
if (res !== "(") res += "|";
res += "^" + effet + "($|_)";
return res;
}, "(") + ")");
function estEffetCombat(name) {
return (patternEffetsCombat.test(name));
}
function effetCombatOfAttribute(attr) {
var ef = attr.get('name');
if (ef === undefined || messageEffetCombat[ef]) return ef;
for (var effet in messageEffetCombat) {
if (ef.startsWith(effet + "_")) return effet;
}
error("Impossible de déterminer l'effet correspondant à " + ef, attr);
return undefined;
}
var messageEffetIndetermine = {
aCheval: { //deprecated, mieux vaut utiliser la commande !cof-en-selle
activation: "monte sur sa monture",
actif: "est sur sa monture",
fin: "descend de sa monture"
},
marcheSylvestre: {
activation: "se deplace maintenant en terrain difficile",
actif: "profite du terrain difficile",
fin: "est maintenant en terrain normal"
}
};
var patternEffetsIndetermine =
new RegExp(_.reduce(messageEffetIndetermine, function(reg, msg, effet) {
var res = reg;
if (res !== "(") res += "|";
res += "^" + effet + "($|_)";
return res;
}, "(") + ")");
function estEffetIndetermine(name) {
return (patternEffetsIndetermine.test(name));
}
function effetIndetermineOfAttribute(attr) {
var ef = attr.get('name');
if (ef === undefined || messageEffetIndetermine[ef]) return ef;
for (var effet in messageEffetIndetermine) {
if (ef.startsWith(effet + "_")) return effet;
}
error("Impossible de déterminer l'effet correspondant à " + ef, attr);
return undefined;
}
// Fait foo sur tous les tokens représentant charId, ayant l'effet donné, et correspondant au nom d'attribut. Pour le cas où le token doit être lié au personnage, on ne prend qu'un seul token, sauf si filterUnique est défini, auquel cas on fait l'appel sur tous les tokens qui passent filterUnique
function iterTokensOfEffet(charId, effet, attrName, foo, filterUnique) {
var total = 1; //Nombre de tokens affectés, pour gérer l'asynchronie si besoin
if (attrName == effet) { //token lié au character
var tokens =
findObjs({
_type: 'graphic',
_subtype: 'token',
represents: charId
});
tokens = tokens.filter(function(tok) {
return (tok.get('bar1_link') !== '');
});
if (tokens.length === 0) {
log("Pas de token pour un personnage");
log(charId);
log(attrName);
return;
}
if (filterUnique) {
total = tokens.length;
tokens.forEach(function(tok) {
if (filterUnique(tok)) foo(tok, total);
});
} else foo(tokens[0], 1);
} else { //token non lié au character
var tokenName = attrName.substring(attrName.indexOf('_') + 1);
var tNames =
findObjs({
_type: 'graphic',
_subtype: 'token',
represents: charId,
name: tokenName,
bar1_link: ''
});
total = tNames.length;
if (total > 1) {
var character = getObj('character', charId);
var charName = "d'id " + charId;
if (character) charName = character.get('name');
error("Attention, il y a plusieurs tokens nommés " + tokenName + ", instances du personnage " + charName, total);
}
tNames.forEach(function(tok) {
foo(tok, total);
});
}
}
function finDEffet(attr, effet, attrName, charId, evt, attrSave) { //L'effet arrive en fin de vie, doit être supprimé
//Si on a un attrSave, alors on a déjà imprimé le message de fin d'effet
if (attrSave) { //on a un attribut associé à supprimer)
evt.deletedAttributes.push(attrSave);
attrSave.remove();
} else { //On cherche si il y en a un
if (!getState({
charId: charId
}, 'mort')) {
sendChar(charId, messageEffetTemp[effet].fin);
}
var nameWithSave = effet + "SaveParTour" + attrName.substr(effet.length);
findObjs({
_type: 'attribute',
_characterid: charId,
name: nameWithSave
}).
forEach(function(attrS) {
evt.deletedAttributes.push(attrS);
attrS.remove();
});
}
switch (effet) {
case 'agrandissement': //redonner sa taille normale
getObj('character', charId).get('defaulttoken', function(normalToken) {
normalToken = JSON.parse(normalToken);
var largeWidth = normalToken.width + normalToken.width / 2;
var largeHeight = normalToken.height + normalToken.height / 2;
iterTokensOfEffet(charId, effet, attrName, function(token) {
var width = token.get('width');
var height = token.get('height');
affectToken(token, 'width', width, evt);
token.set('width', normalToken.width);
affectToken(token, 'height', height, evt);
token.set('height', normalToken.height);
},
function(token) {
if (token.get('width') == largeWidth) return true;
if (token.get('height') == largeHeight) return true;
return false;
}
);
});
break;
case 'aveugleTemp':
iterTokensOfEffet(charId, effet, attrName, function(token) {
setState({
token: token,
charId: charId
}, 'aveugle', false, evt);
}, function(token) {
return true;
});
break;
case 'ralentiTemp':
iterTokensOfEffet(charId, effet, attrName, function(token) {
setState({
token: token,
charId: charId
}, 'ralenti', false, evt);
}, function(token) {
return true;
});
break;
case 'peur':
case 'peurEtourdi':
iterTokensOfEffet(charId, effet, attrName, function(token) {
setState({
token: token,
charId: charId
}, 'peur', false, evt);
}, function(token) {
return true;
});
break;
case 'ombreMortelle':
case 'dedoublement':
iterTokensOfEffet(charId, effet, attrName, function(token) {
token.remove();
});
break;
case 'murDeForce':
iterTokensOfEffet(charId, effet, attrName, function(token) {
var attr = tokenAttribute({
charId: charId,
token: token
}, 'murDeForceId');
if (attr.length === 0) return;
var imageMur = getObj('graphic', attr[0].get('current'));
if (imageMur) {
imageMur.remove();
}
attr[0].remove();
});
break;
default:
}
evt.deletedAttributes.push(attr);
attr.remove();
}
//asynchrone
function degatsParTour(charId, effet, attrName, dmg, type, msg, evt, options, callback) {
options = options || {};
msg = msg || '';
var count = -1;
iterTokensOfEffet(charId, effet, attrName, function(token, total) {
if (count < 0) count = total;
sendChat('', "[[" + dmg + "]]", function(res) {
var rolls = res[0];
var dmgRoll = rolls.inlinerolls[0];
var r = {
total: dmgRoll.results.total,
type: type,
display: buildinline(dmgRoll, type)
};
var perso = {
token: token,
charId: charId
};
dealDamage(perso, r, [], evt, 1, options, undefined,
function(dmgDisplay, dmg) {
sendChar(charId, msg + ". " + onGenre(charId, 'Il', 'Elle') +
" subit " + dmgDisplay + " DM");
count--;
if (count === 0) callback();
});
}); //fin sendChat du jet de dé
}); //fin iterTokensOfEffet
}
function nextTurn(cmp) {
if (!cmp.get('initiativepage')) return;
var turnOrder = cmp.get('turnorder');
var pageId = state.COFantasy.combat_pageid;
if (pageId === undefined) pageId = cmp.get('playerpageid');
if (turnOrder === '') return; // nothing in the turn order
turnOrder = JSON.parse(turnOrder);
if (turnOrder.length < 1) return; // Juste le compteur de tour
var evt = {
type: 'Personnage suivant',
attributes: [],
deletedAttributes: []
};
var active = turnOrder[0];
var lastHead = turnOrder.pop();
turnOrder.unshift(lastHead);
evt.turnorder = JSON.stringify(turnOrder);
var attrs = findObjs({
_type: 'attribute'
});
// Si on a changé d'initiative, alors diminue les effets temporaires
var init = parseInt(active.pr);
if (active.id == "-1" && active.custom == "Tour") init = 0;
var count = 0; // pour l'aspect asynchrone des effets temporaires
if (state.COFantasy.init > init) {
var attrsTemp = attrs.filter(function(obj) {
if (!estEffetTemp(obj.get('name'))) return false;
var obji = obj.get('max');
return (init < obji && obji <= state.COFantasy.init);
});
evt.init = state.COFantasy.init;
state.COFantasy.init = init;
// Boucle sur les effets temps peut être asynchrone à cause des DM
count = attrsTemp.length;
attrsTemp.forEach(function(attr) {
var charId = attr.get('characterid');
var effet = effetTempOfAttribute(attr);
if (effet === undefined) {
//erreur, on stoppe tout
log(attr);
count--;
return;
}
var attrName = attr.get('name');
var v = attr.get('current');
if (v == 'tourFinal') { //L'effet arrive en fin de vie, doit être supprimé
finDEffet(attr, effet, attrName, charId, evt);
count--;
} else { //Effet encore actif
evt.attributes.push({
attribute: attr,
current: v
});
if (v > 1) attr.set('current', v - 1);
else attr.set('current', 'tourFinal');
switch (effet) { //rien après le switch, donc on sort par un return
case 'putrefaction': //prend 1d6 DM
degatsParTour(charId, effet, attrName, "1d6", 'maladie',
"pourrit", evt, {
magique: true
},
function() {
count--;
if (count === 0) nextTurnOfActive(active, attrs, evt, pageId);
});
return;
case 'asphyxie': //prend 1d6 DM
degatsParTour(charId, effet, attrName, "1d6", 'normal',
"ne peut plus respirer", evt, {
asphyxie: true
},
function() {
count--;
if (count === 0) nextTurnOfActive(active, attrs, evt, pageId);
});
return;
case 'saignementsSang': //prend 1d6 DM
if (charAttributeAsBool(charId, 'immuniteSaignement')) {
count--;
if (count === 0) nextTurnOfActive(active, attrs, evt, pageId);
return;
}
degatsParTour(charId, effet, attrName, "1d6", 'normal',
"saigne par tous les orifices du visage", evt, {
magique: true
},
function() {
count--;
if (count === 0) nextTurnOfActive(active, attrs, evt, pageId);
});
return;
case 'armureBrulante': //prend 1d4 DM
degatsParTour(charId, effet, attrName, "1d4", 'feu',
"brûle dans son armure", evt, {},
function() {
count--;
if (count === 0) nextTurnOfActive(active, attrs, evt, pageId);
});
return;
case 'nueeDInsectes': //prend 1 DM
degatsParTour(charId, effet, attrName, "1", 'normal',
"est piqué par les insectes", evt, {},
function() {
count--;
if (count === 0) nextTurnOfActive(active, attrs, evt, pageId);
});
return;
case 'armeBrulante': //prend 1 DM
degatsParTour(charId, effet, attrName, "1", 'feu',
"se brûle avec son arme", evt, {},
function() {
count--;
if (count === 0) nextTurnOfActive(active, attrs, evt, pageId);
});
return;
case 'strangulation':
var nameDureeStrang = 'dureeStrangulation';
if (effet != attrName) { //concerne un token non lié
nameDureeStrang += attrName.substring(attrName.indexOf('_'));
}
var dureeStrang = findObjs({
_type: 'attribute',
_characterid: charId,
name: nameDureeStrang
});
if (dureeStrang.length === 0) {
var attrDuree = createObj('attribute', {
characterid: charId,
name: nameDureeStrang,
current: 0,
max: false
});
evt.attributes.push({
attribute: attrDuree,
current: null
});
} else {
var 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, messageEffetTemp[effet].fin);
attr.set('current', v);
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();
}
}
count--;
return;
default:
count--;
return;
}
}
}); //fin de la boucle sur tous les attributs d'effets
}
if (count === 0) nextTurnOfActive(active, attrs, evt, pageId);
}
//evt a un champ attributes et un champ deletedAttributes
function nextTurnOfActive(active, attrs, evt, pageId) {
if (active.id == "-1" && active.custom == "Tour") {
var tour = parseInt(active.pr);
if (isNaN(tour)) {
error("Tour invalide", active);
return;
}
evt.tour = tour - 1;
evt.updateNextInitSet = updateNextInitSet;
active.pr = tour - 1; // préparation au calcul de l'undo
sendChat("GM", "Début du tour " + tour);
state.COFantasy.tour = tour;
state.COFantasy.init = 1000;
// Enlever les bonus d'un tour
attrs = removeAllAttributes('actionConcertee', evt, attrs);
attrs = removeAllAttributes('intercepter', evt, attrs);
attrs = removeAllAttributes('exemplaire', evt, attrs);
// Pour défaut dans la cuirasse, on diminue si la valeur est 2, et on supprime si c'est 1
var 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 {
var 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
var feinte = allAttributesNamed(attrs, 'feinte');
feinte.forEach(function(attr) {
var valFeinte = parseInt(attr.get('current'));
if (isNaN(valFeinte) || valFeinte > 0) {
evt.deletedAttributes.push(attr);
attr.remove();
} else {
var prevAttr = {
attribute: attr,
current: 0
};
evt.attributes.push(prevAttr);
attr.set('current', 1);
}
});
// nouveau tour : enlever le statut surpris
// et faire les actions de début de tour
var selected = [];
updateNextInitSet.forEach(function(id) {
selected.push({
_id: id
});
});
findObjs({
_type: 'graphic',
_subtype: 'token',
_pageid: pageId
}).forEach(function(tok) {
var charId = tok.get('represents');
if (charId === '') return;
var perso = {
token: tok,
charId: charId
};
if (getState(perso, 'surpris')) { //surprise
setState(perso, 'surpris', false, {});
selected.push({
_id: tok.id
});
}
var enflammeAttr = tokenAttribute(perso, 'enflamme');
if (enflammeAttr.length > 0) {
var 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
var dernierTourEnflamme = parseInt(enflammeAttr[0].get('max'));
if ((isNaN(dernierTourEnflamme) || dernierTourEnflamme < tour) &&
!isNaN(enflamme) && enflamme > 0) {
var d6Enflamme = randomInteger(6);
var feu = d6Enflamme + enflamme - 1;
var dmg = {
type: 'feu',
total: feu,
display: feu
};
feu = dealDamage(perso, dmg, [], evt, 1);
sendChar(charId, " est en flamme ! " +
onGenre(charId, 'Il', 'Elle') + " subit " + feu + " DM");
if (d6Enflamme < 3) {
sendChar(charId, " les flammes s'éteignent");
removeTokenAttr(perso, 'enflamme', evt);
} else {
enflammeAttr[0].set('max', tour);
}
}
}
});
initiative(selected, evt); // met Tour à la fin et retrie
updateNextInitSet = new Set();
// Saves à faire à la fin de chaque tour
var attrsSave = attrs.filter(function(attr) {
var attrName = attr.get('name');
var indexSave = attrName.indexOf('SaveParTour');
if (indexSave < 0) return false;
return estEffetTemp(attrName.substring(0, indexSave));
});
//Les saves sont asynchrones
var count = attrsSave.length;
attrsSave.forEach(function(attr) {
var attrName = attr.get('name');
var carac = attr.get('current');
if (isNotCarac(carac)) {
error("Save par tour " + attrName + " mal formé", carac);
count--;
if (count === 0) addEvent(evt);
return;
}
var seuil = parseInt(attr.get('max'));
if (isNaN(seuil)) {
error("Save par tour " + attrName + " mal formé", seuil);
count--;
if (count === 0) addEvent(evt);
return;
}
var charId = attr.get('characterid');
var indexSave = attrName.indexOf('SaveParTour');
var effet = attrName.substring(0, indexSave);
attrName = effet + attrName.substr(indexSave + 11);
var token;
iterTokensOfEffet(charId, effet, attrName, function(tok) {
if (token === undefined) token = tok;
});
if (token === undefined) {
log("Pas de token pour le save " + attrName);
count--;
if (count === 0) addEvent(evt);
return;
}
var perso = {
token: token,
charId: charId
};
if (getState(perso, 'mort')) {
count--;
if (count === 0) addEvent(evt);
return;
}
var attrEffet = findObjs({
_type: 'attribute',
_characterid: charId,
name: attrName
});
if (attrEffet === undefined || attrEffet.length === 0) {
error("Save sans effet temporaire " + attrName, attr);
attr.remove();
count--;
if (count === 0) addEvent(evt);
return;
}
attrEffet = attrEffet[0];
var expliquer = function(msg) {
sendChar(charId, msg);
};
var msgPour = " pour ne plus être sous l'effet de " + effet;
var sujet = onGenre(charId, 'il', 'elle');
var msgReussite = ", " + sujet + " " + messageEffetTemp[effet].fin;
var msgRate = ", " + sujet + " " + messageEffetTemp[effet].actif;
var saveOpts = {
msgPour: msgPour,
msgReussite: msgReussite,
msgRate: msgRate
};
save({
carac: carac,
seuil: seuil
}, perso, expliquer, saveOpts, evt,
function(reussite) { //asynchrone
if (reussite) {
finDEffet(attrEffet, effet, attrName, charId, evt, attr);
}
count--;
if (count === 0) addEvent(evt);
});
}); //fin boucle attrSave
} else { // change the active token
setActiveToken(active.id, evt);
}
addEvent(evt);
}
function destroyToken(token) { //to remove unused local attributes
var charId = token.get('represeernts');
if (charId === "") return;
if (token.get('bar1_link') !== "") return;
var endName = "_" + token.get('name');
var 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 estSurMonture(perso, pageId) {
var attr = tokenAttribute(perso, 'monteSur');
if (attr.length === 0) return false;
if (pageId === undefined) pageId = perso.token.get('pageid');
var monture = tokenOfId(attr[0].get('current'), attr[0].get('max'), pageId);
attr[0].remove();
if (monture === undefined) {
sendChar(perso.charId, "descend de sa monture");
return true;
}
sendChar(perso.charId, "descend de " + monture.token.get('name'));
removeTokenAttr(monture, 'estMontePar');
removeTokenAttr(monture, 'positionSurMonture');
return true;
}
function moveToken(token) {
var charId = token.get('represents');
if (charId === '') return;
var monture = {
token: token,
charId: charId
};
var pageId = token.get('pageid');
if (estSurMonture(monture, pageId)) return;
var attr = tokenAttribute(monture, 'estMontePar');
if (attr.length === 0) return;
var cavalier = tokenOfId(attr[0].get('current'), attr[0].get('max'), pageId);
if (cavalier === undefined) {
attr[0].remove();
return;
}
var x = token.get('left');
var y = token.get('top');
var position = tokenAttribute(monture, 'positionSurMonture');
if (position.length > 0) {
var dx = parseInt(position[0].get('current'));
var 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', monture.token.get('rotation') + attributeAsInt(monture, 'directionSurMonture', 0));
}
return {
apiCommand: apiCommand,
nextTurn: nextTurn,
destroyToken: destroyToken,
moveToken: moveToken,
changeHandout: changeHandout,
};
}();
on("change:handout", function(obj, prev) {
COFantasy.changeHandout(obj, prev);
});
on("destroy:handout", function(prev) {
COFantasy.changeHandout(undefined, prev);
});
on("ready", function() {
var script_version = 0.6;
COF_loaded = true;
state.COFantasy = state.COFantasy || {
combat: false,
tour: 0,
init: 1000,
eventId: 0,
version: script_version,
};
if (state.COFantasy.version === undefined) {
state.COFantasy.eventId = 0;
state.COFantasy.version = script_version;
}
var handout = findObjs({
_type: 'handout'
});
handout.forEach(function(hand) {
COFantasy.changeHandout(hand);
});
log("COFantasy " + script_version + " loaded");
});
on("chat:message", function(msg) {
"use strict";
if (!COF_loaded || msg.type != "api") return;
COFantasy.apiCommand(msg);
});
on("change:campaign:turnorder", COFantasy.nextTurn);
on("destroy:token", COFantasy.destroyToken);
on("change:token:left", COFantasy.moveToken);
on("change:token:top", COFantasy.moveToken);
on("change:token:rotation", COFantasy.moveToken);