diff --git a/COFantasy/COFantasy.js b/COFantasy/COFantasy.js index 2f94f2d4d..22e70013f 100644 --- a/COFantasy/COFantasy.js +++ b/COFantasy/COFantasy.js @@ -1,4 +1,4 @@ -//Derni\xE8re modification : lun. 17 f\xE9vr. 2025, 02:42 +//Derni\xE8re modification : mer. 01 avr. 2026, 02:50 // ------------------ generateRowID code from the Aaron --------------------- const generateUUID = (function() { "use strict"; @@ -368,6 +368,11 @@ var COFantasy = COFantasy || function() { type: 'image', val: "https://s3.amazonaws.com/files.d20.io/images/52767134/KEGYUXeKnxZr5dbDwQEO4Q/thumb.png?15248300835" }, + image_squelette: { + explications: "Image utilis\xE9e pour les squelettes", + type: 'image', + val: "https://s3.amazonaws.com/files.d20.io/images/29280162/yE969j48uDK89Roil4qlRg/thumb.png?1487951226" + }, image_mur_de_force: { explication: "Image utilis\xE9e pour un mur de force sph\xE9rique", type: 'image', @@ -1720,12 +1725,14 @@ var COFantasy = COFantasy || function() { } stateCOF.roundMarkerId = roundMarker.id; if (roundMarkerSpec.layer === 'map') toFront(roundMarker); - // Ne pas amener une monture mont\xE9e en avant pour \xE9viter de cacher le cavalier - if (cavalier && monture) { - toFront(monture.token); - toFront(cavalier.token); - } else { - toFront(token); + if (!predicateAsBool(personnage, 'tokenEnDessous')) { + // Ne pas amener une monture mont\xE9e en avant pour \xE9viter de cacher le cavalier + if (cavalier && monture) { + toFront(monture.token); + toFront(cavalier.token); + } else { + toFront(token); + } } setTimeout(_.bind(activateRoundMarker, undefined, sync), 200); } else if (roundMarker) { //rotation @@ -2340,7 +2347,9 @@ var COFantasy = COFantasy || function() { getState(perso, 'assomme') || getState(perso, 'etourdi') || getState(perso, 'paralyse') || getState(perso, 'endormi') || getState(perso, 'apeure') || attributeAsBool(perso, 'statueDeBois') || - attributeAsBool(perso, 'souffleDeMort') || attributeAsBool(perso, 'petrifie'); + attributeAsBool(perso, 'souffleDeMort') || + attributeAsBool(perso, 'limiteParTour_decarnationEmpecheDAgir') || + attributeAsBool(perso, 'petrifie'); return !inactif; } @@ -4933,14 +4942,14 @@ var COFantasy = COFantasy || function() { case 'lienDeSang': iterTokensOfAttribute(charId, options.pageId, efComplet, attrName, function(token) { let perso = { - token: token, - charId: charId + token, + charId, }; - let attrsLienDeSangDe = tokenAttribute(perso, "lienDeSangDe"); + let attrsLienDeSangDe = tokenAttribute(perso, 'lienDeSangDe'); attrsLienDeSangDe.forEach(function(attr) { - let tokenLie = persoOfId(attr.get("current")); + let tokenLie = persoOfId(attr.get('current')); if (tokenLie) { - tokenAttribute(tokenLie, "lienDeSangVers").forEach(function(a) { + tokenAttribute(tokenLie, 'lienDeSangVers').forEach(function(a) { a.remove(); }); } @@ -4948,6 +4957,20 @@ var COFantasy = COFantasy || function() { }); }); break; + case 'concentrationSurDecarnationMorbide': + iterTokensOfAttribute(charId, options.pageId, efComplet, attrName, function(token) { + let perso = { + token, + charId, + }; + let valAttr = tokenAttribute(perso, efComplet + 'Valeur'); + valAttr.forEach(function(attr) { + let cible = persoOfId(attr.get('current')); + if (!cible) return; + finDEffetDeNom(cible, 'decarnationMorbide', evt); + }); + }); + break; default: } if (options.attrSave === undefined && charId) { @@ -5296,6 +5319,11 @@ var COFantasy = COFantasy || function() { //Maintenant on remet les attributs if (character.attributes) { character.attributes.forEach(function(attr) { + let name = attr.get('name'); + if (!name) { + error("Attribut sans nom dans un personnage effac\xE9", attr); + return; + } let oldId = attr.id; let newAttr = createObj('attribute', { characterid: charId, @@ -5342,6 +5370,10 @@ var COFantasy = COFantasy || function() { let oldId = attr.id; let nameDel = attr.get('name'); log("Restoring attribute " + nameDel); + if (!nameDel) { + error("L'attribut effac\xE9 n'avait pas de nom", attr); + return; + } let newAttr = createObj('attribute', { characterid: attr.newCharId || attr.get('characterid'), @@ -8256,9 +8288,9 @@ var COFantasy = COFantasy || function() { return translatePathCoordinates(v[1], v[2], p); }); return chemin; - } catch (error) { + } catch (err) { error("Erreur, chemin mal form\xE9 dans le calque d'\xE9clairage dynamique", path); - log(error.name + ": " + error.message); + log(err.name + ": " + err.message); } }); //On rajoute les portes ferm\xE9es. @@ -9297,7 +9329,8 @@ var COFantasy = COFantasy || function() { bonus: 0, id: generateUUID() }; - let exprDM = expr.trim().toLowerCase(); + if (expr && typeof expr == 'object' && expr.length) expr = expr[0]; + let exprDM = (expr + '').trim().toLowerCase(); let indexD = exprDM.indexOf('d'); if (indexD > 0) { dm.nbDe = parseInt(exprDM.substring(0, indexD)); @@ -10985,6 +11018,14 @@ var COFantasy = COFantasy || function() { }); }); } + if (weaponStats.armeNaturelle && predicateAsBool(perso, 'optionsAttaquesNaturelles')) { + let wo = ' ' + predicateAsBool(perso, 'optionsAttaquesNaturelles'); + wo.split(' --').reverse().forEach(function(o) { + o = o.trim(); + if (o === '') return; + optArgs.unshift(o); + }); + } options.sortilege = weaponStats.sortilege; options.hache = weaponStats.hache; options.armeNaturelle = weaponStats.armeNaturelle; @@ -15662,7 +15703,7 @@ var COFantasy = COFantasy || function() { } } if (options.pointsVitaux && estNonVivant(target)) { - sendPlayer(playerName, "La cible n'est pas vraiment vivante : " + attaquant.name + " ne trouve pas de points vitaux", playerId); + sendPlayer(playerName, "La cible n'est pas vraiment vivante : " + nomPerso(attaquant) + " ne trouve pas de points vitaux", playerId); return false; } if (attributeAsBool(attaquant, 'tenuADistanceManoeuvre(' + target.token.id + ')')) { @@ -16681,6 +16722,10 @@ var COFantasy = COFantasy || function() { } } } + if (options.attaquant && predicateAsBool(options.attaquant, 'crocDuDragon') && (predicateAsBool(target, 'dragonInvincible') || predicateAsBool(target, 'emissaireDuDragonInvincible'))) { + dmgCoef *= 2; + expliquer("Croc du dragon contre le dragon => x2 DMG"); + } otherDmg = otherDmg || []; let dmgDisplay = dmg.display; let dmgTotal = dmg.total; @@ -16940,9 +16985,8 @@ var COFantasy = COFantasy || function() { case 'Arme de jet': break; default: - if (oatk === undefined) oatk = fieldAsInt(att, 'armeatk', 0); - if (atkcac === undefined) atkcac = oatk; - if (oatk > atkcac) atkcac = oatk; + oatk = fieldAsInt(att, 'armeatk', 0); + if (atkcac === undefined || oatk > atkcac) atkcac = oatk; } } if (atkcac === undefined) { @@ -16964,9 +17008,8 @@ var COFantasy = COFantasy || function() { if (portee === 0) continue; let typeat = fieldAsString(att, 'armetypeattaque', 'Naturel'); if (typeat == 'Sortilege') continue; - if (oatk === undefined) oatk = fieldAsInt(att, 'armeatk', 0); - if (atktir === undefined) atktir = oatk; - if (oatk > atktir) atktir = oatk; + oatk = fieldAsInt(att, 'armeatk', 0); + if (atktir === undefined || oatk > atktir) atktir = oatk; } if (atktir === undefined) { if (atk === undefined) @@ -16985,9 +17028,8 @@ var COFantasy = COFantasy || function() { } let typeat = fieldAsString(att, 'armetypeattaque', 'Naturel'); if (typeat != 'Sortilege') continue; - if (oatk === undefined) oatk = fieldAsInt(att, 'armeatk', 0); - if (atkmag === undefined) atkmag = oatk; - if (oatk > atkmag) atkmag = oatk; + oatk = fieldAsInt(att, 'armeatk', 0); + if (atkmag === undefined || oatk > atkmag) atkmag = oatk; } if (atkmag === undefined) { if (atk === undefined) @@ -17847,6 +17889,7 @@ var COFantasy = COFantasy || function() { name: 'charge_' + attackLabel }); if (chargesArme.length > 0) { + chargesArme = chargesArme[0]; currentCharge = parseInt(chargesArme[0].get('current')); } if (isNaN(currentCharge) || currentCharge < 1) { @@ -17870,17 +17913,30 @@ var COFantasy = COFantasy || function() { chargesGrenaille[0].set('current', currentChargeGrenaille); } } - evt.attributes.push({ - attribute: chargesArme[0], - current: currentCharge - }); + if (chargesArme) { + evt.attributes.push({ + attribute: chargesArme, + current: currentCharge + }); + } currentCharge -= 1; //Si l'arme n'est plus charg\xE9e, on peut perdre le bonus d'initiative if (currentCharge === 0 && bonusPlusViteQueSonOmbre(attaquant, weaponStats)) { updateNextInit(attaquant); } - chargesArme[0].set('current', currentCharge); + if (chargesArme) { + chargesArme.set('current', currentCharge); + } else { + chargesArme = createObj('attribute', { + characterid: attackingCharId, + name: 'charge_' + attackLabel, + value: currentCharge + }); + evt.attributes.push({ + attribute: chargesArme + }); + } } } // Effets quand on rentre en combat @@ -18911,6 +18967,7 @@ var COFantasy = COFantasy || function() { function addEffetTemporaireLie(perso, attr, evt) { let etlAttr = tokenAttribute(perso, 'effetsTemporairesLies'); + evt.attributes = evt.attributes || []; if (etlAttr.length === 0) { etlAttr = createObj('attribute', { characterid: perso.charId, @@ -19087,6 +19144,32 @@ var COFantasy = COFantasy || function() { setTokenAttr(ef.attaquant, 'lienDeSangVers', target.token.id, evt, opt); setTokenAttr(target, 'lienDeSangDe', ef.attaquant.token.id, evt, opt); } + if (ef.effet === 'decarnationMorbide') { + let attaquant = ef.attaquant; + if (attaquant === undefined) { + error("Effet de d\xE9carnation morbide sans sans attaquant", ef); + return; + } + let attrConcentration = tokenAttribute(attaquant, 'concentrationSurDecarnationMorbide'); + if (attrConcentration.length === 0) { + //On commence la d\xE9carnation + setTokenAttr(target, 'decarnationMorbideValeur', attaquant.token.id, evt); + setTokenAttr(attaquant, 'concentrationSurDecarnationMorbideValeur', target.token.id, evt); + } else { + //L'attaquant poursuit une d\xE9carnation + let attrCibleConcentration = tokenAttribute(attaquant, 'concentrationSurDecarnationMorbideValeur'); + if (attrCibleConcentration.length === 0) { + setTokenAttr(attaquant, 'concentrationSurDecarnationMorbideValeur', target.token.id, evt); + } else { + if (attrCibleConcentration[0].get('current') != target.token.id) { + sendPerso(attaquant, "ne cible pas la bonne personne pour se concentrer sur la d\xE9carnation morbide"); + sendChat('COF', "/w gm annuler la derni\xE8re attaque"); + return; + } + } + } + setAttrDuree(attaquant, 'concentrationSurDecarnationMorbide', 2, evt); + } if (ef.actif !== undefined) { setTokenAttr(target, ef.effet + 'Actif', ef.actif, evt); } @@ -22604,7 +22687,7 @@ var COFantasy = COFantasy || function() { }; //si il faut noter les DMs d'un type particulier if (mainDmgType == 'drain') dmSuivis.drain = dmgTotal; predicatesNamed(target, 'vitaliteSurnaturelle').forEach(function(a) { - if (typeof a != "string") return; + if (typeof a != 'string') return; let indexType = a.indexOf('/'); if (indexType < 0 || indexType == a.length) return; a = a.substring(indexType + 1); @@ -22915,6 +22998,17 @@ var COFantasy = COFantasy || function() { soinsDuPhenix(personnage, evt, expliquer); return; } + if (attributeAsBool(personnage, 'decarnationMorbide')) { + let attrSorcier = tokenAttribute(personnage, 'decarnationMorbideValeur'); + let sorcier; + if (attrSorcier.length > 0) sorcier = persoOfId(attrSorcier[0].get('current')); + if (sorcier) { + sendPerso(personnage, "Le squelette de "+nomPerso(personnage)+" s'extrait de son corps et se met au service de "+nomPerso(sorcier)); + //TODO: cr\xE9er le squelette sur la carte + } else { + sendPerso(personnage, "son squelette sort de son corps !"); + } + } setState(personnage, 'mort', true, evt); let targetPos = { x: personnage.token.get('left'), @@ -23111,7 +23205,9 @@ var COFantasy = COFantasy || function() { attrs = attrs[0]; options = options || {}; options.pageId = options.pageId || perso.token.get('pageid'); - finDEffet(attrs, effetTempOfAttribute(attrs), attrs.get('name'), perso.charId, evt, options); + let nEffet = effet; + if (estEffetTemp(effet)) nEffet = effetTempOfAttribute(attrs); + finDEffet(attrs, nEffet, attrs.get('name'), perso.charId, evt, options); } function interfaceMettreAZeroPV(msg) { @@ -23269,7 +23365,7 @@ var COFantasy = COFantasy || function() { let rdTarget = getRDS(target); let rd = rdTarget.rdt || 0; if (!target.perteDeSubstance && options.attaquant && predicateAsBool(target, 'ancreInvincible')) { - if (predicateAsBool(options.attaquant, 'dragonInvincble')) { + if (predicateAsBool(options.attaquant, 'dragonInvincible')) { rd += 10; target.messages.push("Ancre contre le dragon => +10 RD"); } else if (predicateAsBool(options.attaquant, 'emissaireDuDragonInvincible')) { @@ -24268,7 +24364,12 @@ var COFantasy = COFantasy || function() { affectToken(prevToken, 'showplayers_aura2', prevToken.get('showplayers_aura2'), evt); removeTokenFlagAura(prevToken); let perso = persoOfToken(prevToken); - if (perso) removeTokenAttr(perso, 'energieImpie', evt); + if (perso) { + removeTokenAttr(perso, 'energieImpie', evt); + if (attributeAsInt(perso, 'concentrationSurDecarnationMorbide', 0) == 1) { + finDEffetDeNom(perso, 'concentrationSurDecarnationMorbide', evt); + } + } } else { if (pageId) { prevToken = findObjs({ @@ -25006,11 +25107,14 @@ var COFantasy = COFantasy || function() { if (msg.selected && msg.selected.length > 0) { let firstSelected = getObj('graphic', msg.selected[0]._id); if (firstSelected === undefined) { - error("Un token s\xE9lectionn\xE9 n'est pas trouv\xE9 en interne", msg.selected); - return; + firstSelected = getObj(msg.selected[0]._type, msg.selected[0]._id); + if (!firstSelected) { + error("Un token s\xE9lectionn\xE9 n'est pas trouv\xE9 en interne", msg.selected); + } } - pageId = firstSelected.get('pageid'); - } else { + if (firstSelected) pageId = firstSelected.get('pageid'); + } + if (!pageId) { playerId = getPlayerIdFromMsg(msg); pageId = getPageId(playerId); } @@ -25912,8 +26016,8 @@ var COFantasy = COFantasy || function() { type: "nouveauJour", attributes: [], action: { - persos: persos, - options: options + persos, + options } }; addEvent(evt); @@ -26647,12 +26751,12 @@ var COFantasy = COFantasy || function() { sendPerso(perso, expl); }); }); - utiliseCapacite(perso, testProuesse, evt); + utiliseCapacite(perso, testProuesse, evtProuesse); if (rollId) { options.chanceRollId = options.chanceRollId || {}; options.chanceRollId[rollId] = (options.chanceRollId[rollId] + 5) || 5; } else { - options.chance = (options.chance + 10) || 10; + options.chance = (options.chance + 5) || 5; } if (redoEvent(evt, action, perso)) return; } @@ -28101,8 +28205,7 @@ var COFantasy = COFantasy || function() { // - ligneOptions : une cha\xEEne de caract\xE8res \xE0 ajouter aux attaques // - target : l'id de la cible des attaques // - nePasAfficherArmes : quand on affiche plus tard l'arme en main - function listeAttaquesVisibles(perso, options) { - options = options || {}; + function listeAttaquesVisibles(perso, options = {}) { let ligneOptions = options.ligneOptions || ''; let target = options.target || '@{target|token_id}'; let ligne = ''; @@ -28327,6 +28430,14 @@ var COFantasy = COFantasy || function() { return ligneArme; } + function combatDeMasse(perso) { + if (!predicateAsBool(perso, 'combatDeMasse')) return false; + let turnOrder = Campaign().get('turnorder'); + if (turnOrder === '') return false; // rien dans le turn order + turnOrder = JSON.parse(turnOrder); + return (turnOrder.length > 10); + } + //Si listActions est fourni, \xE7a peut faire r\xE9f\xE9rence \xE0 une ability //dont le nom commence et termine par #, contenant une liste d'actions //\xE0 afficher @@ -28364,6 +28475,18 @@ var COFantasy = COFantasy || function() { return true; } } + //Test pour la d\xE9carnation morbide + if (attributeAsBool(perso, 'decarnationMorbide') && !attributeAsBool(perso, 'limiteParTour_testDecarnationMorbide')) { + let attrsAttId = tokenAttribute(perso, 'decarnationMorbideValeur'); + if (attrsAttId.length > 0) { + let org = persoOfId(attrsAttId[0].get('current')); + if (org) { + let command = '!cof-agir-malgre-decarnation ' + perso.token.id + ' ' + org.token.id; + sendPerso(perso, boutonSimple(command, "Essayer d'agir") + " malgr\xE9 la d\xE9carnation morbide
"); + return true; + } + } + } formeDarbre = attributeAsBool(perso, 'formeDArbre'); if (formeDarbre) { actionsDuTour = findListeActions(perso, "Forme d'arbre", abilities); @@ -28386,6 +28509,9 @@ var COFantasy = COFantasy || function() { } else if (attributeAsBool(perso, 'rapideCommeLeVent')) { ligne += "Rapide comme le vent : une action d'attaque ou de mouvement en plus
"; } + if (combatDeMasse(perso)) { + ligne += "Combat de masse : une action d'attaque ou de mouvement en plus
"; + } if (attributeAsBool(perso, 'reactionViolente')) { ligne += "Crise de folie : doit attaquer la personne qui l'a provoqu\xE9 et ceux qui l'en emp\xEAchent.
"; ligne += boutonSimple('!cof-fin-reaction-violente ' + perso.token.id, "Prendre sur soi") + "
"; @@ -28889,6 +29015,13 @@ var COFantasy = COFantasy || function() { } getSelected(msg, function(selected, playerId) { iterSelected(selected, function(perso) { + if (liste) { + const evt = { + type: "liste d'actions" + }; + addEvent(evt); + if (limiteRessources(perso, options, liste, liste, evt)) return; + } let actions = turnAction(perso, playerId, liste); if (!actions) { let l = liste || ''; @@ -29478,7 +29611,7 @@ var COFantasy = COFantasy || function() { } } if (rangSoin > 3) { - let soinsGuerison = attributeAsInt(perso, 'limiteParJour_gu\xE9rison', rangSoin); + let soinsGuerison = attributeAsInt(perso, 'limiteParJour_gu\xE9rison', 1); if (soinsGuerison > 0) { addLineToFramedDisplay(display, "peut encore faire " + soinsGuerison + " gu\xE9rison" + (soinsGuerison > 1 ? 's' : '') + " aujourd'hui"); } else { @@ -30624,8 +30757,6 @@ var COFantasy = COFantasy || function() { } let effet = cmd[1]; let lanceur = options.lanceur; - let charId; - if (lanceur) charId = lanceur.charId; if (cof_states[effet]) { //remplacer par sa version effet temporaire effet += 'Temp'; } @@ -30637,7 +30768,7 @@ var COFantasy = COFantasy || function() { } let armeActuelle = armesEnMain(lanceur); if (!armeActuelle) { - whisperChar(charId, "Pas d'arme en main, impossible de savoir \xE0 quoi appliquer " + effet); + whisperChar(lanceur.charId, "Pas d'arme en main, impossible de savoir \xE0 quoi appliquer " + effet); return; } effet = effet + '(' + armeActuelle.label + ')'; @@ -30685,7 +30816,6 @@ var COFantasy = COFantasy || function() { lanceur = persoOfId(selected[0]._id); if (lanceur) { options.lanceur = lanceur; - charId = lanceur.charId; } } } @@ -36891,8 +37021,12 @@ var COFantasy = COFantasy || function() { if (cavalierBis === undefined) emp.remove(); }); if (cavalierBis) { - sendPerso(cavalier, "ne peut monter sur " + nomMonture + " car " + onGenre(monture, 'il', 'elle') + " a d\xE9j\xE0 un cavalier, " + nomPerso(cavalierBis)); - return; + if (attributeAsBool(cavalierBis, 'monteSur') && distanceCombat(cavalierBis.token, tokenM, pageId) === 0) { + sendPerso(cavalier, "ne peut monter sur " + nomMonture + " car " + onGenre(monture, 'il', 'elle') + " a d\xE9j\xE0 un cavalier, " + nomPerso(cavalierBis)); + return; + } + removeTokenAttr(cavalierBis, 'monteSur'); + removeTokenAttr(monture, 'estMontePar'); } } if (distanceCombat(tokenC, tokenM, pageId) > 0) { @@ -39262,7 +39396,7 @@ var COFantasy = COFantasy || function() { }], actions: [{ titre: 'Charge', - code: '!cof-attack @{selected|token_id} @{target|token_id} 1 --m2d20 --pietine}' + code: '!cof-attack @{selected|token_id} @{target|token_id} 1 --m2d20 --pietine' }, ] }, tigreDentsDeSabre: { @@ -39355,7 +39489,7 @@ var COFantasy = COFantasy || function() { }], actions: [{ titre: 'Charge', - code: '!cof-attack @{selected|token_id} @{target|token_id} 1 --m2d20 --pietine}' + code: '!cof-attack @{selected|token_id} @{target|token_id} 1 --m2d20 --pietine' }, ] } }; @@ -41322,6 +41456,53 @@ var COFantasy = COFantasy || function() { }); } + //!cof-agir-malgre-decarnation tid tid_du_sorcier + function agirMalgreDecarnation(msg) { + const options = msg.options || parseOptions(msg); + if (options === undefined) return; + const cmd = options.cmd; + if (cmd === undefined || cmd.length < 3) { + error("cof-agir-malgre-decarnation attend 2 arguments", msg.content); + return; + } + let playerId = getPlayerIdFromMsg(msg); + let perso = persoOfId(cmd[1]); + if (!perso) { + sendPlayer(msg, "Personnage victime de la d\xE9carnation non trouv\xE9e", playerId); + return; + } + let sorcier = persoOfId(cmd[2]); + if (!sorcier) { + sendPlayer(msg, "lanceur de la d\xE9carnation non trouv\xE9", playerId); + return; + } + const evt = { + type: "Test action decarnation" + }; + addEvent(evt); + setTokenAttr(perso, 'limiteParTour_testDecarnationMorbide', true, evt); + const rollId = "actionDecarnation_" + perso.token.id; + let explications = []; + let titre = "Jet " + deCarac('FOR') + " pour pouvoir agir malgr\xE9 la d\xE9carnation"; + let display = startFramedDisplay(playerId, titre, perso, { + perso2: sorcier + }); + testOppose(rollId, perso, 'FOR', options, sorcier, 'INT', {}, explications, evt, + function(resultat, crit, rt1, rt2) { + if (resultat == 2) { + setTokenAttr(perso, 'limiteParTour_decarnationEmpecheDAgir', true, evt); + explications.push(nomPerso(perso) + " ne peut pas agir ce tour, " + nomPerso(sorcier) + " tire trop fort sur son squelette"); + } else { + explications.push(nomPerso(perso) + " r\xE9ussit \xE0 agir ce tour"); + } + explications.forEach(function(e) { + addLineToFramedDisplay(display, e); + }); + sendFramedDisplay(display); + if (resultat != 2) turnAction(perso, playerId); + }); + } + //!cof-animer-cadavre lanceur cible function animerCadavre(msg) { const options = msg.options || parseOptions(msg); @@ -47114,6 +47295,9 @@ var COFantasy = COFantasy || function() { case '!cof-affaiblir-carac': parseAffaiblirCarac(msg); return; + case '!cof-agir-malgre-decarnation': + agirMalgreDecarnation(msg); + return; case '!cof-agrandir-page': agrandirPage(msg); return; @@ -48312,6 +48496,12 @@ var COFantasy = COFantasy || function() { dm: true, visible: true }, + concentrationSurDecarnationMorbide: { + activation: "se concentre sur une d\xE9carnation morbide", + actif: "est concentr\xE9 sur une d\xE9carnation morbide", + actifF: "est concentr\xE9e sur une d\xE9carnation morbide", + fin: "relache sa concentration", + }, ombreMortelle: { activation: "voit son ombre s'animer et l'attaquer !", actif: "est une ombre anim\xE9e", @@ -48933,6 +49123,16 @@ var COFantasy = COFantasy || function() { actif: "danse la danse des lames", fin: "termine sa danse des lames" }, + decarnationMorbide: { + activation: "sent son squelette chercher \xE0 sortir de son corps !", + actif: "lutte pour garder ses os dans son corps", + fin: "retrouve le contr\xF4le de son corps", + msgSave: "pouvoir garder ses os dans son corps", + prejudiciable: true, + seulementVivant: true, + dm: true, + visible: true + }, detournerLeRegard: { activation: "d\xE9tourne le regard", actif: "d\xE9tourne le regard", @@ -50424,8 +50624,7 @@ var COFantasy = COFantasy || function() { } let attrName = attr.get('name'); let effetC = effetComplet(effet, attrName); - let v = parseInt(attr.get('current')); - if (isNaN(v)) v = 1; + let v = toInt(attr.get('current'), 1); if (v <= 1) { //L'effet arrive en fin de vie, doit \xEAtre supprim\xE9 //Sauf si on a accumul\xE9 plusieurs fois l'effet let accumuleAttr = attributeExtending(charId, attrName, effetC, 'DureeAccumulee'); @@ -53607,7 +53806,7 @@ var COFantasy = COFantasy || function() { }(); on('ready', function() { - const scriptVersion = '3.15'; + const scriptVersion = '3.16'; on('add:token', COFantasy.addToken); on("change:campaign:playerpageid", COFantasy.changePlayerPage); state.COFantasy = state.COFantasy || { diff --git a/COFantasy/ChangeLog.md b/COFantasy/ChangeLog.md index 713767b3a..3c3f49406 100644 --- a/COFantasy/ChangeLog.md +++ b/COFantasy/ChangeLog.md @@ -1,4 +1,8 @@ # COFantasy: Historique des changements (depuis la version 1.0) +## 3.16 +* Support minimal pour le combat de masse. +* Support pour la dague Croc du Dragon + ## 3.15 ### Capacités * Implémentaion d'un prédicat pour Autorité naturelle. diff --git a/COFantasy/doc.html b/COFantasy/doc.html index 35d7861ab..05b72d8e6 100644 --- a/COFantasy/doc.html +++ b/COFantasy/doc.html @@ -133,7 +133,7 @@

Sommaire

-

Documentation pour la version 3.15 de COFantasy

+

Documentation pour la version 3.16 de COFantasy

1. Comment utiliser Le script

1.1 Tokens et personnages

@@ -260,7 +260,7 @@

Initiative : !cof-init

Les tokens sélectionnés sont ajouté au tracker de tour. Si celui-ci n'existe pas, un nouveau est créé, avec compteur de tour à 1, et on rentre en combat. Les tokens ayant un prédicat aucuneActionCombat ne sont jamais ajoutés au turn tracker.

Une option du script (ajustable en utilisant !cof-options) permet d'utiliser la règle de l'initiative variable (ajout d'un d6 explosif à l'initiative). Il est alors possible de faire lancer les dés d'initiative par les joueurs.

Les règles de tri suivent ce qui est donné p 70 : priorité aux PJs. Pour l'instant, on distingue un PJ d'un PNJ au dé de vie : si le personnage d'a pas de dé de vie, c'est un PNJ. Ensuite, entre PJs, priorité à la plus haute sagesse. Pour les PNJ, c'est aléatoire.

-

À chaque instant, le token dont c'est le tour d'agir est signalé par une aura qui bouge lentement. L'image de cette aura peut être modifiée dans les options (!cof-options images), et il est possible de spécifier une aura pour chaque token, en ajoutant une ligne dans le champ GM Notes du token (en bas de la partie basique de l'édition du token). Cette ligne doit commencer par init_aura:, et contenir une url valide pour un token. N'oubliez pas de sauvegarder le token comme token par défaut pour un personnage si vous voulez que ce personnage ait toujours cette aura.

+

À chaque instant, le token dont c'est le tour d'agir est signalé par une aura qui bouge lentement. L'image de cette aura peut être modifiée dans les options (!cof-options images), et il est possible de spécifier une aura pour chaque token, en ajoutant une ligne dans le champ GM Notes du token (en bas de la partie basique de l'édition du token). Cette ligne doit commencer par init_aura:, et contenir une url valide pour un token. N'oubliez pas de sauvegarder le token comme token par défaut pour un personnage si vous voulez que ce personnage ait toujours cette aura. Le token dont c'est le tour est automatiquement mis au premier plan, sauf si le personnage a un prédicat tokenEnDessous (utile pour les gros tokens).

Il est possible de laisser les joueurs passer leur tour, en utilisant la commande !cof-tour-suivant. Une utilisation possible est de le mettre en macro dans les actions du tour (voir Actions du tour ci-dessous).

Astuces :
@@ -913,7 +913,7 @@

Escaliers et portails

La limite au nombre d'escaliers est de 12 étages, donc pas de lettre après L.

Il est possible d'avoir des escaliers qui mènent à d'autres cartes (ce qui fait alors changer la carte vue par le joueur). Il suffit que le nom de l'escalier commence par tmap_. Attention, à cause de limitations de Roll20, cela ne peut fonctionner que si l'image du token est dans une library personnelle d'un joueur : si elle vient du marketplace, il faut d'abord la copier dans sa library, puis utiliser l'image qui est dans la library pour le token.

Pour des escaliers plus flexibles (voire des portails), on peut décrire dans le champ des Notes du MJ du token sur la couche MJ une destination quand on monte et une destination quand on descend. Il suffit de mettre sur une ligne, descend: directement suivi du nom du token vers lequel aller pour descendre, et/ou sur une autre ligne monte: suivi du nom du token vers lequel aller quand on monte. Par exemple, pour bloquer un escalier qui descend, il suffit de mettre descend: dans le champ de notes du MJ. On peut aussi faire des "escaliers" à sens unique.

-

Il est possible d'automatiser les escaliers, de façons à ce que les tokens qui passent sur le lieux de l'escalier soient automatiquement transportés. Pour cela, sélectionner le token de l'escalier et lancer la commande !cof-tp-auto. Dans sa version sans argument, le dépacement automatique aura lieu dès qu'un token sera dans le rectangle du token de l'escalier. Sinon, on peut préciser un rayon (en mètres), et le mouvement aura lieur dès que le token sera à moins de cette distance du centre de l'escalier. Le déplacement se fera vers le haut si c'est possible, et sinon vers le bas. À noter que Roll20 peut perdre parfois les données sur les tokens entre les parties. Quand c'est le cas, le script va s'efforce de rerouver les escaliers automatiques, mais ça a plus de chances de marcher si vous donnez un nom unique au token de votre escalier. Pour mettre fin au déplacement automatique sur un escalier, sélectionner le token de l'escalier et taper la commande !cof-tp-auto off.

+

Il est possible d'automatiser les escaliers, de façons à ce que les tokens qui passent sur le lieux de l'escalier soient automatiquement transportés. Pour cela, sélectionner le token de l'escalier et lancer la commande !cof-tp-auto. Dans sa version sans argument, le déplacement automatique aura lieu dès qu'un token sera dans le rectangle du token de l'escalier. Sinon, on peut préciser un rayon (en mètres), et le mouvement aura lieur dès que le token sera à moins de cette distance du centre de l'escalier. Le déplacement se fera vers le haut si c'est possible, et sinon vers le bas. À noter que Roll20 peut perdre parfois les données sur les tokens entre les parties. Quand c'est le cas, le script va s'efforce de rerouver les escaliers automatiques, mais ça a plus de chances de marcher si vous donnez un nom unique au token de votre escalier. Pour mettre fin au déplacement automatique sur un escalier, sélectionner le token de l'escalier et taper la commande !cof-tp-auto off.

Montures

@@ -1033,7 +1033,7 @@

Voie du champ de bataille

  • Action concertée : utiliser !cof-echange-init @{selected|token_id} @{target|token_id} n --limiteParTour 1 actionConcerteen peut valoir 0, 1 ou 2 selon le rang dans la voie.
  • À couvert : !cof-a-couvert @{selected|token_id} @{target|token_id} le target est l'allié qui se déplace avec l'arquebusier. Se sélectionner soi-même si pas d'allié.
  • Combattant aguerri : se reporter à la capacité choisie
  • -
  • Combat de masse : pas de support particulier, effectuer les actions supplémentaires en fonction des situations.
  • +
  • Combat de masse : ajouter un prédicat combatDeMasse.
  • Constitution héroïque : reporter le bonus sur la fiche
  • Voie du Pistolero

    @@ -1449,7 +1449,7 @@

    Voie de la résistance

  • Robustesse : rajouter les PV sur la fiche.
  • Armure naturelle : rajouter la DEF sur la fiche.
  • Second souffle : Ajouter un prédicat secondSouffle permet de faire apparaître un bouton pour utiliser le second souffle quand le guerrier a perdu des PVs en combat, limite le gain de PVs à ceux perdus dans le combat en cours, et utilise automatiquement la capacité en fin de combat si elle n'a pas été utilisée. Si cette automatisation ne vous convient pas, vous pouvez utiliser la commande !cof-soin @{selected|token_id} secondSouffle à la place. Enfin, on peut préciser une valeur pour ce prédicat, pour remplacer le niveau + CON (par exemple pour un PNJ).
  • -
  • Dur à cuire : rajouter un prédicat durACuire sur la fiche.
  • +
  • Dur à cuire : rajouter un prédicat durACuire sur la fiche et ajouter le bonus de +5 aux tests de CON.
  • Constitution héroïque : reporter les bonus sur la fiche.
  • Voie du soldat

    @@ -2299,6 +2299,14 @@

    Voie du héro

  • Homme de la situation : pas de support.
  • Meneur d'hommes : Ajouter une action : !cof-set-attribute meneurDHommesCible @{selected|token_id} --target @{target|token_id}. Le personnage peut utiliser cette action pour désigner sa cible. Tous les alliés du personnage recevront automatiquement les bonus sur le reste du combat.
  • +

    Voie du maître des cadavres

    +
      +
    1. +
    2. +
    3. +
    4. +
    5. Décarnation morbide : faire une attaque magique de portée 30 m, de DM 2d6+INT, avec le modificateur necromancie, avec comme option --mana 3 --effet decarnationMorbide. Pour le maintien de la concentration, ajouter une seconde attaque magique de portée 30 m, DM 2d6+INT, avec les modificateurs necromancie, auto et comme option --si etat concentrationSurDecarnationMorbide --effet decarnationMorbide. Le MJ doit vérifier si la cible a bien un squelette à décarner.
    6. +

    Voie du maître des poisons

    Rappel : pour appliquer un poison, utiliser !cof-enduire-poison -1 type force save.
      @@ -2484,6 +2492,7 @@

      4.6 Capacités diverses

    1. Clignotement : !cof-effet-temp clignotement [[1d6+2]].
    2. Créature non-vivante : ajouter un prédicat nonVivant.
    3. Créature végétative : ajouter un prédicat vegetatif.
    4. +
    5. Croc du dragon (Invicible) : ajouter un prédicat crocDuDragon dans les prédicats de l'arme, et --seulementVivant --bonusContreArmure 3 dans les options d'attaque. Le dragon est reconnu par un prédicat dragonInvicible, et ses émissaires par un prédicat emissaireDuDragonInvincible.
    6. Cyclone (pour les élementaires) : !cof-effet-temp cyclone [[1d4+1]] --limiteParCombat 1. Cela va proposer une attaque Traverser dans les actions de l'élémentaire, qui ne peut fonctionner que si le personnage a au moins une attaque sur sa fiche. Par défaut, l'attaque utilisée en traversant est l'attaque de label 1, mais on peut préciser un autre label en utilisant l'option --valeur.
    7. Dégâts si touché au contact (élémentaire de feu,...) : ajouter un prédicat nommé dmSiToucheContact, de valeur les dégâts, suivis de -- et du type si besoin. Par exemple, pour un élémentaire de feu, le prédicat dmSiToucheContact:: 1d6 --feu, en fin de ligne (rien ne doit suivre sur cette ligne).
    8. Drain de force (pour la campagne Invincible) : ajouter un prédicat auraDrainDeForce pour la version de base. Pour la version avancée, remplacer par un prédicat auraDrainDeForceSup.
    9. @@ -3140,7 +3149,7 @@

      9. Index des prédicats

    10. attaqueEnMeute : booléen ou entier (2 par défaut), bonus à l'attaque quand le personnage attaque la même cible qu'un allié.
    11. attaqueEnTraitre : booléen, propose une attaque en traître quand un allié touche un adversaire au contact.
    12. attaqueSournoise : entier, nombre de d6 à ajouter pour les DM d'attaques sournoises.
    13. -
    14. attributsDeStatut : nom, spécifie un attribut qui doit être afficher avec le statt d'un personnage. On peut spécifier plusieurs attributs en ayant plusieur prédicats attributsDeStatut.
    15. +
    16. attributsDeStatut : nom, spécifie un attribut qui doit être afficher avec le statut d'un personnage. On peut spécifier plusieurs attributs en ayant plusieur prédicats attributsDeStatut.
    17. asDeLaGachette : booléen, augmente les DM des arbalètes et armes à poudre de 1d6 si le jet d'attaque atteint 25.
    18. aucuneActionCombat : booléen, le personnage ne rentre pas en combat.
    19. batarde : expression de dés, seulement pour les prédicats des armes à 1 mains : dés de DM quand l'arme est portée à 2 mains.
    20. @@ -3272,6 +3281,7 @@

      9. Index des prédicats

    21. nbDesFeinte : entier (2 par défaut), nombre de dés à ajouter aux DM des feintes réussies.
    22. necromancien : booléen, le personnage est un nécromancien ou une sorcière.
    23. nonVivant : booléen, le personnage n'est pas vivant (élémentaire, golem ou autre).
    24. +
    25. optionsAttaquesNaturelles : argments à ajouter aux attaques avec armes naturelles.
    26. ordreDuChevalierDragon : booléen, augmente de 5 les jets de persuasion et d'intimidation quand le personnage est sur une monture.
    27. pacifisme : booléen, augmente la DEF de 5 tant que le personnage n'a pas attaqué dans un combat.
    28. pacteSanglant : entier, propose un bouton pour augmenter la DEF de cette valeur, en échange d'une dépense de PV (1d4 pour 3 ou moins, 2d4 sinon).