From 187f178a39cd148cae03a7ca7cf93f0d50d0ffaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Devouard?= Date: Tue, 2 Sep 2025 11:36:28 +0200 Subject: [PATCH] COF2E Version 1.8.0 --- ChroniquesOublieesFantasy2/cof2e.html | 796 +++++++++++++++++++------- 1 file changed, 595 insertions(+), 201 deletions(-) diff --git a/ChroniquesOublieesFantasy2/cof2e.html b/ChroniquesOublieesFantasy2/cof2e.html index 2d0af34b22..184270edc9 100644 --- a/ChroniquesOublieesFantasy2/cof2e.html +++ b/ChroniquesOublieesFantasy2/cof2e.html @@ -1,9 +1,9 @@
- + - + @@ -67,6 +67,7 @@ + @@ -105,7 +106,7 @@
@@ -121,6 +122,9 @@ + @@ -265,8 +269,7 @@ - - +
@@ -993,7 +996,7 @@
DEF
-
+
+
 +
@@ -1226,7 +1229,7 @@
- +
@@ -4551,8 +4554,10 @@ Bourse
PC: - PA: - PO: + +  PA: + +  PO: PP:
@@ -4585,6 +4590,7 @@
+    A une attaque @@ -4892,7 +4898,6 @@ Attaques différenciées -
  • Usure des armes @@ -4926,22 +4931,22 @@
    • - + PV:
    • - + DR:
    • - + PC:
    • - + PM:
    • @@ -4965,6 +4970,9 @@
        + @@ -5331,6 +5339,13 @@  
  • +
    +
    Teinte 1/2 PV
    +
    + +   +
    +
    Marker PC
    @@ -5343,7 +5358,10 @@
    -
    +
    +
    + + ChatSetAttr
    @@ -5370,7 +5388,24 @@
  • - Version 1.7.0 (2025-07-jj) + Version 1.8.0 (2025-08-31) + +
      +
    • Prise en compte des capacités épiques dans le calcul des rangs dans la voie et des PV (propriété epic:).
    • +
    • Prise en compte des capacités épiques Défense héroïque et Mana épique dans le calcul de la DEF et des PM.
    • +
    • Possibilité de compter le trésor en PA ou en PO.
    • +
    • Ajout d'une option dans la configuration Token Mod pour appliquer une teinte au token quand le nombre de PV tombe au-dessous de sa valeur maximum.
    • +
    • Ajout d'un bouton pour copier la configuration par défaut depuis une fiche nommée PJBase.
    • +
    • Option jet:N déplacée du champ Spécial d'une attaque au champ Modificateurs d'attaque
    • +
    • Ajout d'une option dans les coups spéciaux des PNJ pour n'affecter que la prochaine attaque
    • +
    +
    +
  • + +
  • +
    + + Version 1.7.0 (2025-07-26)
    • Sous-onglet Maîtrises dans l'onglet Capacités pour les langues et les maîtrises spéciales d'armes / armures
    • @@ -5709,7 +5744,7 @@
  • - +
    @@ -5724,7 +5759,7 @@ RD
    - +
    @@ -5869,7 +5904,7 @@
    - +
    @@ -6004,6 +6039,7 @@    /    +  
    @@ -6011,10 +6047,11 @@   -   -   +   +    /    +   @@ -6022,10 +6059,11 @@   -   -   +   +    /    +   @@ -6132,8 +6170,10 @@ Bourse
    PC: - PA: - PO: + +  PA: + +  PO: PP:
    @@ -6162,6 +6202,7 @@
    +
    @@ -6561,11 +6602,22 @@ Token Mod
    -
    Marker 0 PV
    +
    Teinte 1/2 PV
    -  :  + +  
    +
    +
    Marker 0 PV
    +
    + +   +
    +
    +
    + + ChatSetAttr
    @@ -7061,11 +7113,20 @@ {{#title}}
    {{title}}
    {{/title}} {{#subtitle}}
    {{subtitle}}
    {{/subtitle}}
    + {{#text}} +
    +
    {{text}}
    +
    + {{/text}}
    - {{#allprops() title subtitle desc color token}} + {{#text}} +
    +
    + {{/text}} + {{#allprops() title subtitle desc text color token}}
    {{key}}
    {{value}}
    - {{/allprops() title subtitle desc color token}} + {{/allprops() title subtitle desc text color token}} {{#desc}}
    {{desc}}
    {{/desc}}
    @@ -7076,7 +7137,7 @@ /** * COF2e Sheet Worker - * v1.7.0 + * v1.8.0 */ const SWData = { @@ -7109,19 +7170,19 @@ const SWData = { template: "cof2" }, COLORS: { - default: [ "burlywood", "#000" ], - blue: [ "#0d6efd", "#fff" ], - indigo: [ "#6610f2", "#fff" ], - purple: [ "#6f42c1", "#fff" ], - pink: [ "#d63384", "#fff" ], - red: [ "#dc3545", "#fff" ], - orange: [ "#fd7e14", "#000" ], - yellow: [ "#ffc107", "#000" ], - green: [ "#198754", "#fff" ], - teal: [ "#20c997", "#fff" ], - cyan: [ "#0dcaf0", "#fff" ], - gray: [ "#6c757d", "#fff" ], - dark: [ "#343a40", "#fff" ], + default: [ "burlywood", "#000" ], + blue: [ "#0d6efd", "#fff" ], + indigo: [ "#6610f2", "#fff" ], + purple: [ "#6f42c1", "#fff" ], + pink: [ "#d63384", "#fff" ], + red: [ "#dc3545", "#fff" ], + orange: [ "#fd7e14", "#000" ], + yellow: [ "#ffc107", "#000" ], + green: [ "#198754", "#fff" ], + teal: [ "#20c997", "#fff" ], + cyan: [ "#0dcaf0", "#fff" ], + gray: [ "#6c757d", "#fff" ], + dark: [ "#343a40", "#fff" ], }, PC: { PROFILES: { @@ -7782,6 +7843,7 @@ const SWData = { "bonus-def", "armure", "armure-malus", + "agi-max", "bouclier", "bouclier-malus", "casque", @@ -8510,6 +8572,20 @@ function strval(value, onError = "") { return value || onError; } +/** + * Get object from serialized JSON + * @param {string} value - stringified JSON value + * @param {object} onError - default value on parse error + * @returns {object} + */ +function objval(value, onError = {}) { + try { + return JSON.parse(value); + } catch (e) { + return onError; + } +} + /** * Capitalize string * @param {string} s - string to capitalize @@ -8530,6 +8606,11 @@ function shorten(stat) { return stat.substring(0, 3); } +/** + * Return stat upper short name + * @param {string} stat - stat name + * @returns {string} + */ function upperShort(stat) { return shorten(stat).toUpperCase(); } @@ -8649,10 +8730,11 @@ function cleanText(t, clearTags = true) { t = t.replace(/æ/g, "ae"); t = t.replace(/’/g, "'"); if (clearTags) { - t = t.replace(/\(G\)/, ""); - t = t.replace(/\(M\)/, ""); - t = t.replace(/\(A\)/, ""); - t = t.replace(/\(L\)/, ""); + t = t.replace(/\(g\)/, ""); + t = t.replace(/\(m\)/, ""); + t = t.replace(/\(a\)/, ""); + t = t.replace(/\(l\)/, ""); + t = t.replace(/\(e\)/, ""); t = t.replace(/\*/, ""); } return t.trim(); @@ -8722,7 +8804,7 @@ function hasAtkMod(mods, modName) { /** * Get an attack modifier value - * @param {string[]} data - comma-delimited list of attack modifiers + * @param {string[]} mods - comma-delimited list of attack modifiers * @param {string} modName - name of modifier to check * @returns {string} */ @@ -8730,7 +8812,7 @@ function getAtkMod(mods, modName) { const ix = findAtkMod(mods, modName); if (ix === -1) return ""; - return strval(mods[ix]).replace(modName, "").trim(); + return strval(mods[ix]).replace(modName, "").replace(/[:=]/g, "").trim(); } /** @@ -8778,7 +8860,7 @@ function allPathRanks(attr) { * Return a list of event as array or as string * @param {string} eventName - Name of the event * @param {string[]} attributeList - List of attributes - * @param {string} joinChar - Character to use for joining the items + * @param {string} [joinChar=""] - Character to use for items concatenation * @returns {string[]|string} */ function eventList(eventName, attributeList, joinChar = "") { @@ -8786,6 +8868,18 @@ function eventList(eventName, attributeList, joinChar = "") { return joinChar !== "" ? result.join(joinChar) : result; } +/** + * Return a list of change: events as string + * @param {string[]} attributeList - List of attributes + * @param {string} [section=""] - Repeating section name + * @returns {string} + */ +function changeEvent(attributeList, section = "") { + if (section !== "") + attributeList = attributeList.map(attr => `repeating_${section}:${attr}`); + return eventList("change", attributeList, " "); +} + /** * Return repeating attribute name * @param {string} section - section name @@ -9066,7 +9160,7 @@ function sendChatMsg(msg, options) { * @param {object} [options={}] - optional properties to add to roll * @returns {string[]} */ -function cof2RollTemplate(props, options = {}) { +function rollTemplate(props, options = {}) { if (options.tags) { if (Array.isArray(options.tags)) { if (options.tags.length > 0) @@ -9121,6 +9215,19 @@ function ask(askParams, callback) { }); }; +/** + * Extract string data from CRP roll object + * @param {object} rollInfo - startRoll roll object + * @param {string} name - name of roll to parse data from + * @returns {string} + */ +function getRollData(rollInfo, name) { + const expr = strval(rollInfo.results[name].expression); + if (expr === "") + return + return (expr.match(/^0\[(.*)\]\s*$/) || [])[1]; +} + /** * Add numeric attributes values * @param {object} values - getAttrs object with numeric values @@ -9177,6 +9284,25 @@ function tokenMod(charName, props = {}) { return tokenmod + (tokenProps !== "" ? " --set " : "") + tokenProps; } +/** + * Build ChatSetAttr command + * @param { { select: string, from: string, header: string, content: string } } options - !setattr options + * @param {Object} attrs - attributes name/value pairs + * @returns {String} + */ +function chatSetAttr(options, attrs = {}) { + options = { select: "--sel", from: "COF2e", header: "", ...options }; + if (!options.select) + return ""; + const setattr = Object.keys(options).reduce((result, optk) => { + if ([ "from", "header", "content" ].includes(optk) && options[optk]) + result += ` --fb-${optk} ${options[optk]}`; + return result; + }, `!setattr ${options.select}`); + const attribs = " " + Object.entries(attrs).map(([ name, value ]) => `--${name}|${value}`).join(" "); + return setattr + attribs; +} + /** * Show a div area via Roll20 jQuery * @param {string} div - Name of div area to show @@ -9313,6 +9439,17 @@ function getPathRanks(rangs_voies) { return strval(rangs_voies).split(",").map(r => intval(r)); } +/** + * Return list of rank names + * @param {string} pathName - Name of path + * @param {object} values - getAttrs() Roll20 object + * @returns {string[]} + */ +function getRankNames(pathName, values) { + const rankNames = objval(strval(values.noms_rangs, "[]")); + return rankNames.filter(rn => rn.path === pathName).map(rn => rn.rank); +} + /** * Return list of owned abilities * @param {object} values - getAttrs() object @@ -9324,11 +9461,12 @@ function ownedAbilities(values) { getPathNames(strval(values.noms_voies)).forEach((pathName, ix) => { if (ranks[ix] === 0) return; - pathName = cleanText(pathName) + pathName = cleanText(pathName); const path = SWData.PVPATH.find(path => path.name === pathName); if (!path) return; - const owned = path.abilities.slice(0, ranks[ix]); + //const owned = path.abilities.slice(0, ranks[ix]); + const owned = getRankNames(pathName, values).slice(0, ranks[ix]); abilities = [ ...abilities, ...owned ]; }); return abilities; @@ -9349,7 +9487,8 @@ function ownedAbilityTree(values) { const path = SWData.PVPATH.find(path => path.name === pathName); if (!path) return; - const owned = path.abilities.slice(0, ranks[ix]); + //const owned = path.abilities.slice(0, ranks[ix]); + const owned = getRankNames(pathName, values).slice(0, ranks[ix]); abilities.push({ name: pathName, profile: path.profile, abilities: owned }); }); return abilities; @@ -9378,6 +9517,22 @@ function hasAbility(search, values) { return owned.includes(cleanText(name)); } +/** + * Search ability name + * @param {string} search - ability name to search + * @param {object} values - getAttrs() object (should contain noms_rangs) + * @returns {boolean} + * @description Useful for epic abilities + */ +function hasAbilityName(search, values) { + try { + const rankNames = objval(strval(values.noms_rangs, "[]")).map(rn => rn.rank); + return rankNames.includes(search); + } catch (error) { + return false; + } +} + /** * Count the number of paths with attained rank * @param {object} search - search option @@ -9803,10 +9958,14 @@ function getDEvol(level) { /** * On level change */ -on("change:niveau", function(eventInfo) { +on(changeEvent([ + "niveau", "noms_rangs", + "cfg_use_epic", "cfg_use_capped6", "cfg_atk_diff" +]), function(eventInfo) { getAttrs([ - "sheet_type", "niveau", "profil", "pc_max", - "cfg_use_epic", "cfg_use_capped6", "cfg_atk_diff" + "sheet_type", "niveau", "profil", "pc", "pc_max", + "cfg_use_epic", "cfg_use_capped6", "cfg_atk_diff", + "noms_rangs" ], function(values) { if (strval(values.sheet_type) === SWData.SHEET_TYPE.NPC) return; @@ -9819,8 +9978,11 @@ on("change:niveau", function(eventInfo) { let updateAttrs = new Map(); + // reset PC if (intval(eventInfo.newValue) === intval(eventInfo.previousValue) + 1) { - updateAttrs.set("pc", intval(values.pc_max)); + const pcMax = intval(values.pc_max); + if (intval(values.pc) < pcMax) + updateAttrs.set("pc", pcMax); } let atks = { cac: 1, tir: 1, mag: 1 }; @@ -9837,10 +9999,11 @@ on("change:niveau", function(eventInfo) { if (level > levelCap && useEpic) { const oddLevel = (level %2 === 1) ? level : level -1; const epicBase = levelCap + Math.ceil((oddLevel - levelCap) / 2); + const heroicDef = hasAbilityName("defense heroique", values) ? 2 : 0; updateAttrs.set("atkcac_base", epicBase); updateAttrs.set("atktir_base", epicBase); updateAttrs.set("atkmag_base", epicBase); - updateAttrs.set("def_base", epicBase); + updateAttrs.set("def_base", epicBase + heroicDef); } logAndSet(Object.fromEntries(updateAttrs), logTitle(eventInfo)); }); @@ -9858,7 +10021,7 @@ on("clicked:alias-toggle", function() { if (alias !== "") { const cofantasy = JSON.stringify({ action: "alias", param: name }); setAttrs({ character_name: alias, alias: "", notes, cofantasy }); - sendChatMsg(cof2RollTemplate({ + sendChatMsg(rollTemplate({ text })); } @@ -9875,7 +10038,7 @@ SWData.PC.STATS.forEach(stat => { `${short}_buff`, `t${short}_buff`, ]; - on(eventList("change", attrs, " "), function (eventInfo) { + on(changeEvent(attrs), function (eventInfo) { getAttrs([ "sheet_type", ...attrs], function(values) { if (strval(values.sheet_type) === SWData.SHEET_TYPE.NPC) return; @@ -9911,7 +10074,7 @@ function updateInit(eventInfo) { /** * On init components change */ -on(eventList("change", SWData.PC.COMBAT.init, " "), updateInit); +on(changeEvent(SWData.PC.COMBAT.init), updateInit); /** * On attacks components change @@ -9924,7 +10087,7 @@ SWData.PC.COMBAT.attacks.forEach(attk => { `${attack}_cond`, `${attack}_buff` ]; - on(eventList("change", changed, " "), function(eventInfo) { + on(changeEvent(changed), function(eventInfo) { getAttrs(changed, function(values) { const score = addValues(values); logAndSet({ [attack]: score }, logTitle(eventInfo)); @@ -10003,7 +10166,7 @@ function updatePVMax(eventInfo, changed = "") { return; const props = strval(values[`${ability}_props`]).toLowerCase(); - if (hasProp(props, "doublon")) + if (hasProp(props, "doublon") || hasProp(props, "epic")) return; const { path, rank } = abilityInfo(ability); @@ -10042,7 +10205,7 @@ function updatePVMax(eventInfo, changed = "") { }); if (unknowns.length > 0) { - sendChatMsg(cof2RollTemplate({ + sendChatMsg(rollTemplate({ lsub: "Erreur", rsub: "Calcul PV max", alert: `**Calcul impossible : Pas de PV indiqué pour la ou les voies ${ unknowns.join(", ") }**`, @@ -10102,10 +10265,10 @@ function updatePMMax(eventInfo) { getAttrs([ "cfg_use_mana", "pmmax_force", "vol", "pm_buff", - "cha", "rangs_voies", "noms_voies", + "cha", "rangs_voies", "noms_voies", "noms_rangs", ...spells, ...owned, - ...bRanks + ...bRanks ], function(values) { if (boolval(values.pmmax_force)) return; @@ -10127,7 +10290,8 @@ function updatePMMax(eventInfo) { base = cha; } - const pm_max = spellPM > 0 ? base + spellPM + intval(values.pm_buff) : 0; + const epicMana = hasAbilityName("mana epique", values) ? 5 : 0; + const pm_max = spellPM > 0 ? base + spellPM + intval(values.pm_buff) + epicMana : 0; logAndSet({ pm_max }, logTitle(eventInfo)); }); } @@ -10136,14 +10300,14 @@ function updatePMMax(eventInfo) { * On VOL & PM buff change * On CHA & ranks change */ -on("change:vol change:pm_buff change:cha change:rangs_voies", updatePMMax); +on(changeEvent([ "vol","pm_buff","cha","rangs_voies","noms_voies","noms_rangs" ]), updatePMMax); /** * On change of an ability spell */ seq(9).forEach(p => { seq(5).forEach(r => { - on(`change:v${p}r${r} change:v${p}r${r}_spell`, updatePMMax) + on(`change:v${p}r${r} change:v${p}r${r}_spell`, updatePMMax); }); }); @@ -10167,7 +10331,7 @@ function updatePCMax(eventInfo) { /** * On PC components change */ -on("change:pc_base change:cha change:pc_buff change:cfg_pc_var", updatePCMax); +on(changeEvent([ "pc_base","cha","pc_buff","cfg_pc_var" ]), updatePCMax); /** * Return list of attributes for luck roll button @@ -10276,7 +10440,7 @@ SWData.PC.STATS.forEach(stat => { const defAction = intval(values.def_action); const text = defAction !== 0 ? `+ [[${defAction}]] Défense ${ defAction === 5 ? "totale" : "partielle" }` : ""; const carac = `[[@{${roll}}[Dé] + @{${short}${ isNPC ? "" : "_test" }}[Bonus ${short.toUpperCase()}]${shadow.malus}${fear}${armor_malus}${shield_malus}${helmet_malus} ]] `; - const chatMsg = cof2RollTemplate({ + const chatMsg = rollTemplate({ lsub: "Test", rsub: capitalize(stat) + statMod, roll: carac, @@ -10294,7 +10458,14 @@ SWData.PC.STATS.forEach(stat => { * Used by PC & NPC sheet */ on("clicked:init-btn", function () { - getAttrs([ "sheet_type", "cfg_init_mod", "per", "agi", "casque_eqp", "casque", "pnj_tactique" ], function(values) { + getAttrs([ + "sheet_type", + "cfg_init_mod", + "per", "agi", + "casque_eqp", + "casque", + "pnj_tactique" + ], function(values) { let initMod = ""; if (boolval(values.cfg_init_mod)) initMod = ` + [[(@{per}+@{agi})/10]][Distinct]`; @@ -10303,8 +10474,9 @@ on("clicked:init-btn", function () { if (!isNPC) helmet = helmetMalus(values); const tactique = isNPC ? strval(values.pnj_tactique) : ""; - const roll = `[[@{init}[Initiative]${helmet} + @{cfg_init_variable}[Dé(s)]${initMod} &{tracker} ]]`; - const chatMsg = cof2RollTemplate({ + const initBonus = isNPC ? " + ?{Bonus ?}[Bonus]" : ""; + const roll = `[[@{init}[Initiative]${helmet}${initBonus} + @{cfg_init_variable}[Dé(s)]${initMod} &{tracker} ]]`; + const chatMsg = rollTemplate({ lsub: "Jet", rsub: "Initiative", roll @@ -10312,7 +10484,7 @@ on("clicked:init-btn", function () { sendChatMsg(chatMsg); if (tactique.replaceAll("0", "") !== "") { - const chatMsg = cof2RollTemplate({ + const chatMsg = rollTemplate({ lsub: "Combat", rsub: "Tactique", text: tactique @@ -10354,7 +10526,7 @@ SWData.PC.COMBAT.attacks.forEach(attk => { }; if (roll === "1d20" && !useOneRoll) atkProps.broll = atkRoll; - const chatMsg = cof2RollTemplate(atkProps, { tags }); + const chatMsg = rollTemplate(atkProps, { tags }); sendChatMsg(chatMsg); }); }); @@ -10456,7 +10628,7 @@ function updatePVDR(vp, lossOrGain, buttonClicked = true) { }*/ if (notify > 0) { - const chatMsg = cof2RollTemplate({ + const chatMsg = rollTemplate({ lsub: "PV", rsub: `${ lossOrGain === -1 ? "Blessure" : "Soins" } `, text : `@{character_name} ${ lossOrGain === -1 ? "perd" : "regagne" } ${vp} PV${effects}${hasRD} ` @@ -10491,29 +10663,42 @@ function changeVP(button) { * On PV change action buttons */ on("clicked:vigloss-btn clicked:vigheal-btn", function (eventInfo) { - const [ button ] = eventInfo.triggerName.split(":").slice(1); + const [ button ] = strval(eventInfo.triggerName).split(":").slice(1); changeVP(button); }); /** * Change PV background color + * + Token tint color if TokenMod installed */ -on("change:pv", function () { - getAttrs([ "sheet_type", "character_name", "tokenmod", "tkmarker_dead_on", "tkmarker_dead", "pv", "pv_max" ], function (values) { +on("change:pv change:pvig", function (eventInfo) { + const PV = strval(eventInfo.triggerName) === "pvig" ? "PVIG" : "pv"; + getAttrs([ + "sheet_type", "character_name", + "tokenmod", "token_tint_on", "token_tint", "tkmarker_dead_on", "tkmarker_dead", + "pv", "pv_max", "PVIG" + ], function (values) { const isNPC = strval(values.sheet_type) === SWData.SHEET_TYPE.NPC; + const tokenTint = boolval(values.token_tint_on); const tokenmod = boolval(values.tokenmod); const markerDead = boolval(values.tkmarker_dead_on) ? strval(values.tkmarker_dead).trim() : ""; const wounds = wounded(values); - - let tokenmodCmd; + + const tokenProps = {}; + if (tokenTint) { + const tokenTintColor = strval(values.token_tint).split("").every(ch => ch.match(/[0-9A-Fa-f]/)) + ? strval(values.token_tint) + : "ED2939"; + tokenProps.tint_color = numval(values[PV]) <= Math.floor(numval(values.pv_max) / 2) + ? tokenTintColor + : "transparent"; + } if (tokenmod && markerDead !== "") { let deadOrAlive = wounds.condition_inconscient === 1 ? "+" : "-"; - tokenmodCmd = tokenMod(strval(values.character_name), { - statusmarkers: `${deadOrAlive}${markerDead}` - }); + tokenProps.statusmarkers = `${deadOrAlive}${markerDead}`; } - if (tokenmodCmd) - sendChatCmd(tokenmodCmd); + if (Object.keys(tokenProps).length > 0) + sendChatCmd(tokenMod(strval(values.character_name), tokenProps)); if (isNPC) return; @@ -10548,7 +10733,7 @@ on("clicked:drecup-btn", function() { let dr = intval(values.dr); let drMax = intval(values.dr_max); if (dr === 0 && drMax !== 0) { - const chatMsg = cof2RollTemplate({ + const chatMsg = rollTemplate({ lsub, rsub, text: "Plus de Dé de Récupération !", @@ -10561,7 +10746,7 @@ on("clicked:drecup-btn", function() { const pvMax = intval(values.pv_max); let pv = intval(values.pv); if (pv === pvMax && !highFan) { - const chatMsg = cof2RollTemplate({ + const chatMsg = rollTemplate({ lsub, rsub, text: "@{character_name} est déjà à son maximum de PV !", @@ -10602,7 +10787,7 @@ on("clicked:drecup-btn", function() { }; if (typeRecup === 3 && rolls.length > 1) chatMsg = { ...chatMsg, broll: `[[${rolls[1]} + 1d10 + @{vol}[VOL] ]]` }; - const chatCmd = buildChatMsg(cof2RollTemplate(chatMsg)); + const chatCmd = buildChatMsg(rollTemplate(chatMsg)); startRoll(chatCmd, roll => { finishRoll(roll.rollId); let updates = new Map([ [ "dr", dr ] ]); @@ -10702,7 +10887,7 @@ function updateEquiped(attribute) { /** * On DEF components change */ -on(eventList("change", SWData.PC.COMBAT.def, " "), function(eventInfo) { +on(changeEvent(SWData.PC.COMBAT.def), function(eventInfo) { updateDef(eventInfo); const attribute = strval(eventInfo.triggerName, ""); if (attribute.endsWith("_eqp")) @@ -10725,7 +10910,7 @@ on("clicked:luck_apply-btn", function() { const rsub = pcLabel; let pc = intval(values.pc); if (pc === pcMin) { - const chatMsg = cof2RollTemplate({ + const chatMsg = rollTemplate({ lsub, rsub, text: `Plus de ${pcLabel} !`, @@ -10741,7 +10926,7 @@ on("clicked:luck_apply-btn", function() { let roll = "[[" + luck + (last_roll !== "" ? ` + ${last_roll} ` : "" ) + "]]"; if (last_broll) roll += " | [[" + luck + ` + ${last_broll} ` + "]]"; - const chatMsg = cof2RollTemplate({ + const chatMsg = rollTemplate({ lsub, rsub, roll, @@ -10759,7 +10944,7 @@ on("clicked:luck_apply-btn", function() { on("clicked:luck_roll-btn", function() { getAttrs([ "cfg_shadow" ], function(values) { const pcLabel = boolval(values.cfg_shadow) ? "Choc" : "Chance"; - const chatMsg = cof2RollTemplate({ + const chatMsg = rollTemplate({ lsub: "Jet", rsub: pcLabel, roll: `[[ 1d6![Dé] + @{pc}[Points de ${pcLabel} restants] ]]`, @@ -10774,7 +10959,7 @@ on("clicked:luck_roll-btn", function() { */ on("clicked:devol-btn", function() { askValue("Nombre de dés ?", function(dice) { - const chatMsg = cof2RollTemplate({ + const chatMsg = rollTemplate({ lsub: "Jet", rsub: "Dé évolutif", roll: `[[${dice}@{devol}]]` @@ -10851,7 +11036,7 @@ function displayCondition(label, effects, description, notify) { malus.push(`${effects[attribute]} en ${name}`); }); text = [ "@{character_name} subit les effets suivants :", ...malus, ...description ].join("\n"); - const chatMsg = cof2RollTemplate({ + const chatMsg = rollTemplate({ lsub: "Condition", rsub: label, text @@ -10993,7 +11178,7 @@ function insertRolls(text, options = {}) { // Search for "... #d# +/- X..." matchRegExp(text, /([0-9]+[dD][0-9]+)([ ]*[\+\-]*[ ]*[0-9]*)?/gm).forEach(result => { const [ matched, dice, mod ] = result; - const replace = `[[${dice}${ mod || "" }]]`; + const replace = `[[${dice}${ mod.trim() || "" }]] `; text = text.replace(matched, replace); }); @@ -11249,7 +11434,7 @@ on("clicked:repeating_armes:attack-btn", function(eventInfo) { // 2 mains & bouclier const twoHanded = boolval(values[section + "2m"]); if (atk !== "atkmag" && twoHanded && shieldUsed) { - sendChatMsg(cof2RollTemplate({ + sendChatMsg(rollTemplate({ lsub: "Attaque", rsub: name, text: `@{character_name} ne peut pas manier ${name} avec son bouclier équipé !`, @@ -11417,9 +11602,13 @@ on("clicked:repeating_armes:attack-btn", function(eventInfo) { let dmOpt = ""; if (modifiers.includes("explodemax") && !dm.includes("!")) dmOpt += "!"; - if (modifiers.includes("reroll1")) - dmOpt += "r"; - + if (modifiers.includes("reroll1")) { + if (numval(getAtkMod("reroll1")) === 1) + dmOpt += "ro"; + else + dmOpt += "r"; + } + // mounted extra DM if (mounted && atk === "atkcac") { if (hasAbility({ name: "monture fantastique", inPath: "cavalier" }, values)) { @@ -11434,7 +11623,7 @@ on("clicked:repeating_armes:attack-btn", function(eventInfo) { let powderBonus = 0; if (hasAbility({ name: "poudre puissante", inPath: "explosifs" }, values)) { powderBonus = 1 + getPathsWithRank({ profile: "arquebusier", minRank: 5 }, values); - range = ` *(\`\`${ rangeWithUnits(portee, 10) }\`\`)*`; + range = " *(" + "``" + rangeWithUnits(portee, 10) + "``" + ")*"; } if (powderBonus > 0) dmAbilities +=` + ${powderBonus}[Poudre puissante]`; @@ -11506,10 +11695,10 @@ on("clicked:repeating_armes:attack-btn", function(eventInfo) { break; case "perforants": if (dice && faces) - dmcrit = `[[${dice * 2}d${faces}]] DM + hémorragie : [[1@{devol}]] DM/round pendant 4 rounds`; + dmcrit = `[[${dice * 2}d${faces}]] DM + hémorragie : [[1@{devol}]] DM pendant 4 rounds`; break; case "tranchants": - dmcrit = `[[${dmMax}]] DM et hémorragie : [[1@{devol}]] DM/round pendant 4 rounds`; + dmcrit = `[[${dmMax}]] DM et hémorragie : [[1@{devol}]] DM pendant 4 rounds`; break; } } @@ -11596,7 +11785,7 @@ on("clicked:repeating_armes:attack-btn", function(eventInfo) { atkProps.fumble = fumbleEffect; } - const chatMsg = cof2RollTemplate(atkProps, { tags: allTags }); + const chatMsg = rollTemplate(atkProps, { tags: allTags }); sendChatMsg(chatMsg, rollOptions); if (useAmmo && resourceId !== "") { @@ -11621,12 +11810,12 @@ on("change:usure_check", function() { const updates = new Map(); try { - let { attack, name, usure, roll } = JSON.parse(check); + let { attack, name, usure, roll } = objval(check); if (attack !== "") { usure = Math.max(0, usure - 1); updates.set(`repeating_armes_${attack}_arme-usure`, usure); if (roll > usure) { - sendChatMsg(cof2RollTemplate({ + sendChatMsg(rollTemplate({ lsub: "Usure", rsub: name, text: `L'arme se brise (usure : ${usure}) !`, @@ -11903,7 +12092,7 @@ function actionAbility(path, rank) { rollProps.alert = alert; rollProps.alertclass = alertCSS; } - const chatMsg = cof2RollTemplate(rollProps, { tags: allTags }); + const chatMsg = rollTemplate(rollProps, { tags: allTags }); sendChatMsg(chatMsg); // Update usages @@ -12047,7 +12236,7 @@ function checkAbilityPoints(changed = "") { let text = usedPoints > maxPoints ? `${usedPoints} points utilisés mais ${maxPoints} au maximum` : ""; if (alerts.length > 0) text += ( text !== "" ? "\n" : "") + alerts.join("\n"); - const chatMsg = cof2RollTemplate({ + const chatMsg = rollTemplate({ lsub: "Contrôle", rsub: "Points de Capacités", text @@ -12104,11 +12293,7 @@ function updatePathInfos(path) { /** * On check/un-check rank 2 for mages */ -on(eventList( - "change", - SWData.settings.level1Paths.map(p => `v${p}r2`), - " " -), function(eventInfo) { +on(changeEvent(SWData.settings.level1Paths.map(p => `v${p}r2`)), function(eventInfo) { const rank = eventInfo.triggerName; const rankProps = abilityInfo(rank).rankProps; const rank2Props = SWData.settings.level1Paths @@ -12153,7 +12338,7 @@ on(eventList( seq(9).forEach(path => { seq(5).forEach(rank => { - const ability = `v${path}r${rank}`; + const ability = abilityInfo({ path, rank }).rankOwned; // `v${path}r${rank}`; // Handle ability buttons click on(`clicked:${ability}-btn`, function() { @@ -12252,7 +12437,7 @@ on("clicked:reset_uses-btn", function() { // Nothing to process if (freqs.length === 1) { - const chatMsg = cof2RollTemplate({ + const chatMsg = rollTemplate({ lsub: "Capacités", rsub: "Utilisations", text: "Aucune capacité avec nombre d'utilisations à recharger" @@ -12275,7 +12460,7 @@ on("clicked:reset_uses-btn", function() { const usage = intval(values[k]); text += title + " : " + usage + ` utilisation${ usage > 1 ? "s" : ""}\n`; }); - const chatMsg = cof2RollTemplate({ + const chatMsg = rollTemplate({ lsub: "Capacités", rsub: `Par ${freq}`, text @@ -12471,7 +12656,7 @@ on("clicked:repeating_rolls:roll-btn", function() { text = abilityDesc; // Send to chat - const chatMsg = cof2RollTemplate({ + const chatMsg = rollTemplate({ lsub, rsub: rollName + rollBonus, roll: rollCmd, @@ -12539,7 +12724,7 @@ SWData.PC.SKILLS.forEach(skill => { rollCmd = "[[" + rollCmd + " ]]"; // Send to chat - const chatMsg = cof2RollTemplate({ + const chatMsg = rollTemplate({ lsub: "Compétence", rsub: skillName + skillTotal, roll: rollCmd, @@ -12827,7 +13012,7 @@ function encumbrance() { const text = "@{character_name} est encombré-e et subit les effets suivants :\n" + (effects.length > 1 ? "- " : "") + effects.join("\n- "); - const chatMsg = cof2RollTemplate({ + const chatMsg = rollTemplate({ lsub: "Encombrement", rsub: "Limite dépassée", text @@ -12864,13 +13049,14 @@ on("clicked:helpgear-btn", function() { selected === 2 ? "bonus-def:=Bonus DEF de l'armure ou du bouclier" : "", selected === 2 ? "armure:=Bonus DEF de l'armure" : "", selected === 2 ? "armure-malus:=Malus d'armure portée" : "", + selected === 2 ? "agi-max:=Bonus d'AGI maximum" : "", selected === 2 ? "bouclier:=Bonus DEF du bouclier" : "", selected === 2 ? "bouclier-malus:=Malus de bouclier porté" : "", selected === 2 ? "casque:=RD du casque contre les DM critiques" : "", selected === 2 ? "casque-malus:=Malus de PER et Init du casque porté" : "", - selected === 3 ? "bonus-magique:=Bonus pour toucher et DM" : "", - selected === 3 ? "magique-att:=Bonus magique pour toucher" : "", - selected === 3 ? "magique-dm:=Bonus magique aux DM" : "", + selected === 3 ? "magie:=Bonus magique pour toucher et DM" : "", + selected === 3 ? "magieAtt:=Bonus magique pour toucher" : "", + selected === 3 ? "magieDmg:=Bonus magique aux DM" : "", selected === 3 ? "affutee:=Arme affûtée" : "", selected === 3 ? "element:=Nom de l'élément (+' intense')" : "", selected === 3 ? "fleau:=Nom du type de créature" : "", @@ -13090,21 +13276,24 @@ function processAllGear(attributes, callback) { * Process coins & treasure */ SWData.COINS.forEach(coin => { - on(`change:tresor_${coin}`, function() { + on(`change:tresor_${coin} change:tresor_monnaie`, function() { const coins = SWData.COINS.map(c => `tresor_${c}`); - getAttrs(coins, function(values) { + getAttrs([ "tresor_monnaie", ...coins ], function(values) { + const monnaie = intval(values.tresor_monnaie) === 2 ? "po" : "pa"; + delete values.tresor_monnaie; const tresor_pieces = SWData.COINS .filter(c => intval(values[`tresor_${c}`]) !== 0) .map(c=> `${ c.toUpperCase() } : ${ intval(values[`tresor_${c}`]) }`) .join(", "); - values.tresor_pp = intval(values.tresor_pp) * 100; - values.tresor_po = intval(values.tresor_po) * 10; + const multiplier = monnaie === "pa" ? 1 : 0.1; + values.tresor_pp = intval(values.tresor_pp) * multiplier * 100; + values.tresor_po = intval(values.tresor_po) * multiplier * 10; const pc = intval(values.tresor_pc); - values.tresor_pa = intval(values.tresor_pa) + Math.floor(pc / 10); + values.tresor_pa = (intval(values.tresor_pa) + Math.floor(pc / 10)) * multiplier; values.tresor_pc = 0; const total = addValues(values); - const tresor = `${total} PA`; - setAttrs({ tresor, tresor_pieces }); + const tresor = `${total} ${ monnaie.toUpperCase() }`; + setAttrs({ tresor, tresor_pieces }, { silent: true }); }); }); }) @@ -13121,27 +13310,50 @@ on("change:repeating_equipement remove:repeating_equipement", function() { */ on("clicked:repeating_equipement:equipshow-btn", function () { const section = "equipement"; - const gearAttrs = [ "nom", "detail", "props" ].map(a => rptAttr(section, `equip-${a}`)); - getAttrs([ ...gearAttrs, "character_name" ], function(values) { - const nom = strval(values[rptAttr(section, "equip-nom")]); + const gearAttrs = [ "nom", "qte", "detail", "props" ].map(a => rptAttr(section, `equip-${a}`)); + getAttrs([ ...gearAttrs, "character_name", "chatsetattr" ], function(values) { + const name = strval(values[rptAttr(section, "equip-nom")]); + const qty = numval(values[rptAttr(section, "equip-qte")]); const detail = strval(values[rptAttr(section, "equip-detail")]); const props = strval(values[rptAttr(section, "equip-props")]); if (detail === "" && props === "") return const chatMsg = [ "title=Equipement", - `subtitle=${nom}`, + `subtitle=${ qty > 1 ? `${qty} x ` : "" }${name}`, `desc=${ strval(values.character_name) }` ]; if (detail !== "") - chatMsg.push(`Description=${detail}`); + chatMsg.push(`text=${detail}`); if (props !== "") { props.split("\n").filter(prop => strval(prop) !== "").forEach(prop => { const [ code, value ] = strval(prop).split(":"); - chatMsg.push(`Prop: ${code}=${value} `); + chatMsg.push(`Prop ${code}: =${value} `); }); } - sendChatMsg(chatMsg, { template: "custom" }); + if (boolval(values.chatsetattr)) { + const attrs = rptAttrs( + "equipement", + [ "nom", "detail", "modprops" ].map(a => `equip-${a}`), + "-CREATE" + ); + const chatSetAttrs = { [attrs[0]]: name }; + if (detail !== "") + chatSetAttrs[attrs[1]] = detail; + if (props !== "") + chatSetAttrs[attrs[2]] = props.replaceAll(":","=").replaceAll("\n", "~"); + const setattrCmd = chatSetAttr({ + header: "Objet ajouté", + content: "*" + name + "*" + }, chatSetAttrs); + const button = menuButton("default", { size: "1.1em" }, { + class: "chat-button", + padding: "0px 5px;", + "border-radius": "5px;" + }); + chatMsg.push(`Objet=[Equiper](${setattrCmd}${button})`); + } + sendChatMsg(chatMsg, { template: "custom", whisper: "" }); }); }); @@ -13150,19 +13362,38 @@ on("clicked:repeating_equipement:equipshow-btn", function () { */ on("change:repeating_equipement:equip-nom", function() { const section = "equipement"; - const gearAttrs = [ "nom", "props" ].map(a => rptAttr(section, `equip-${a}`)); + const gearAttrs = rptAttrs(section, [ "equip-nom", "equip-props" ]); getAttrs(gearAttrs, function(values) { const name = strval(values[rptAttr(section, "equip-nom")]); const hasProps = strval(values[rptAttr(section, "equip-props")]); if (name !== "" && hasProps === "") { const props = getGearProps(name); if (props.length > 0) { - setAttrs({ [rptAttr(section, "equip-props")]: props.join("\n") }); + setAttrs({ [rptAttr(section, "equip-props")]: props.join("\n") }, { silent: true }); } } }) }); +/** + * Re-format props to LF-delimited list of 'name: value' pairs + */ +on("change:repeating_equipement:equip-modprops", function() { + const props = rptAttr("equipement", "equip-modprops"); + getAttrs([ props ], function(values) { + let updated = strval(values[props]); + if (updated.includes("~")) + updated = updated.split("~").join("\n"); + updated = updated.split("\n").map(prop => { + if (!prop.includes("=")) + return prop; + const [ name, value ] = prop.split("="); + return name + ":" + value; + }).join("\n"); + setAttrs({ [props.replace("-mod", "-")]: updated }); + }); +}); + /** * Equip / Unequip armor & shield */ @@ -13269,7 +13500,7 @@ on("change:repeating_equipement:equip-on", function(eventInfo) { setAttrs(Object.fromEntries(updates)); if (unEquip2Hands) { - sendChatMsg(cof2RollTemplate({ + sendChatMsg(rollTemplate({ lsub: "En main", rsub: "Bouclier", text: "L'arme en main est rengainée pour lever le bouclier", @@ -13304,11 +13535,11 @@ on("remove:repeating_equipement", function (eventInfo) { /** * Handle attacks */ -on(eventList("change", [ +on(changeEvent([ "props", "atk", "predicats" -].map(a => `repeating_equipement:equip-${a}`), " "), function (eventInfo) { +].map(a => `equip-${a}`), "equipement"), function (eventInfo) { const { rowId: gearId } = rptInfo(eventInfo.sourceAttribute); const gearAttrs = [ "nom", @@ -13538,23 +13769,23 @@ on("change:repeating_ressources:res-props", function(eventInfo) { */ on("change:repeating_ressources:res-val", function(eventInfo) { const section = "repeating_ressources"; - const attrs = [ "desc", "val", "props" ].map(a => `${section}_res-${a}`) + const attrs = rptAttrs(section, [ "res-desc", "res-val", "res-props" ]); getAttrs([ "cfg_use_ammo", "cfg_ammo_alert", ...attrs ], function (values) { const useAmmo = boolval(values.cfg_use_ammo); if (!useAmmo) return; const ammoAlert = intval(values.cfg_ammo_alert); - const desc = strval(values[`${section}_res-desc`]); - const props = strval(values[`${section}_res-props`]); + const desc = strval(values[rptAttr(section, "res-desc")]); + const props = strval(values[rptAttr(section, "res-props")]); if (getProp(props, "type").toLowerCase() !== "munitions") return; - const value = intval(values[`${section}_res-val`]); + const value = intval(values[rptAttr(section, "res-val")]); if (value <= 0 || value <= ammoAlert) { const text = value <= 0 ? `Plus de ${desc}` : `Plus que ${value} ${desc}`; const textclass = value <= 0 ? "fumble" : "secondary"; - const chatMsg = cof2RollTemplate({ + const chatMsg = rollTemplate({ lsub: "Attaque", rsub: "Munitions", text, @@ -13647,7 +13878,7 @@ on("clicked:appearance-btn clicked:description-btn", function() { const text = strval(values.sheet_type) === SWData.SHEET_TYPE.PC ? strval(values.apparence) : strval(values.pnj_desc); - sendChatMsg(cof2RollTemplate({ text }), { whisper: "" } ); + sendChatMsg(rollTemplate({ text }), { whisper: "" } ); let avatar = strval(values.character_avatar); if (!avatar) @@ -13657,6 +13888,75 @@ on("clicked:appearance-btn clicked:description-btn", function() { }) }); +/** + * On clicking the default configuration button + */ +on("clicked:def_config-btn", function() { + const allConfigs = [ + "use_skills", + "cfg_init_mod", + "cfg_init_agi", + "cfg_use_encmb", + "cfg_pc_var", + "cfg_bouclier_malus", + "cfg_use_ammo", + "cfg_ammo_alert", + "cfg_crit_diff", + "cfg_tbl_crit", + "cfg_tbl_fumble", + "cfg_use_epic", + "cfg_use_drniv", + "cfg_use_mana", + "cfg_use_capped6", + "cfg_use_dronly", + "cfg_atk_diff", + "cfg_usure", + "cfg_use_fear", + "cfg_shadow", + "pvmax_force", + "drmax_force", + "pcmax_force", + "pmmax_force", + "cfg_1attack_roll", + "cfg_luck_btn", + "$tresor_monnaie", + ]; + getAttrs([ "character_name", "sheet_type", ...allConfigs ], function(values) { + const sheetBase = strval(values.sheet_type) === SWData.SHEET_TYPE.NPC ? "PNJBase" : "PJBase"; + + // PJBase : make sure all attributes are created + if (strval(values.character_name).trim() === sheetBase) { + const updates = new Map(); + allConfigs.filter(attr => !attr.startsWith("$")).forEach(attr => updates.set(attr, intval(values[attr]))); + setAttrs(Object.fromEntries(updates)); + return; + } + + // Other PCs : copy config + const askValues = allConfigs.map(attr => { + if (attr.startsWith("$")) + return `{{${attr.slice(1)}=[[0[@{${sheetBase}|${attr.slice(1)}}] ]]}}`; + else + return `{{${attr}=[[@{${sheetBase}|${attr}}]]}}`; + }).join(" "); + startRoll("! " + askValues, values => { + const updates = new Map(); + allConfigs.forEach(configAttr => { + let value; + if (configAttr.startsWith("$")) + value = getRollData(values, configAttr.slice(1)); + else + value = intval(values.results[configAttr].result); + if (value) + updates.set(configAttr.replace("$",""), value); + }); + setAttrs(Object.fromEntries(updates)); + + finishRoll(values.rollId); + }); + }); +}); + /** * On clicking the Reset button */ @@ -13671,6 +13971,7 @@ on("clicked:reset_all-btn", function() { abilityUsages(e); encumbrance(e); rebuildRanks(e); + updateAbilityList(e) rebuildBuffLists(e); rebuildBuffValues(e); rebuildCustom(e); @@ -13704,7 +14005,7 @@ on("change:cfg_highfan change:cfg_lowfan", function (eventInfo) { /** * On changing one of high fantasy options */ -on(eventList("change", SWData.settings.highFan, " "), function () { +on(changeEvent(SWData.settings.highFan), function () { getAttrs(SWData.settings.highFan, function (values) { if (Object.values(values).some(v => !boolval(v))) setAttrs({ cfg_highfan: 0 }, { silent: true }); @@ -13714,7 +14015,7 @@ on(eventList("change", SWData.settings.highFan, " "), function () { /** * On changing one of low fantasy options */ -on(eventList("change", SWData.settings.lowFan, " "), function () { +on(changeEvent(SWData.settings.lowFan), function () { getAttrs(SWData.settings.lowFan, function (values) { if (Object.values(values).some(v => !boolval(v))) setAttrs({ cfg_lowfan: 0 }, { silent: true }); @@ -13870,7 +14171,7 @@ on("clicked:import_json-btn", function () { let attributes = new Map(); - const profileData = JSON.parse(jsonData); + const profileData = objval(jsonData); if (profileData.character) attributes = importProfile(profileData.character, attributes); @@ -13893,8 +14194,8 @@ on("clicked:import_json-btn", function () { * On change of debug mode state */ on("change:debug_mode", function() { - getAttrs([ "debug_mode" ], function(value) { - setDebugMode(value.debug_mode); + getAttrs([ "debug_mode" ], function(values) { + setDebugMode(values.debug_mode); }); }); @@ -14406,7 +14707,7 @@ SWData.PC.STATS.forEach(stat => { if (skills.length > 0) skill = `+ ?{Compétence ?|Aucune,0|${skills.join("|")}}`; const roll = `[[${ dieRoll(values[dType]) }[Dé] + (@{${short}})[Bonus ${short.toUpperCase()}]${skill} ]] `; - const chatMsg = cof2RollTemplate({ + const chatMsg = rollTemplate({ lsub: "Test", rsub: capitalize(stat), roll @@ -14526,7 +14827,8 @@ on("clicked:repeating_npcarmes:npcatk-btn", function() { const specials = [ "pnj_spec", ...seq(3).map(t => `cspec${t}_nom`), - ...seq(3).map(t => `cspec${t}_roll`), + ...seq(3).map(t => `cspec${t}_next`), + ...seq(3).map(t => `cspec${t}_` + "roll"), ...seq(3).map(t => `cspec${t}_bonus`), ...seq(3).map(t => `cspec${t}_dm`), ]; @@ -14536,7 +14838,7 @@ on("clicked:repeating_npcarmes:npcatk-btn", function() { "character_name", "niveau", "compagnon", "atkgroup", "nbgroup", "pnj_tact", ...specials, - ...attackAttrs.map(attribute => `${section}${attribute}`) + ...attackAttrs.map(attribute => rptAttr(section.split("_")[1], `arme-${attribute}`)) ], function (values) { const useOneRoll = boolval(values.cfg_1attack_roll); const useCritDiff = intval(values.cfg_crit_diff); @@ -14552,6 +14854,9 @@ on("clicked:repeating_npcarmes:npcatk-btn", function() { const portee = strval(values[section + "portee"]); const range = (portee !== "") ? ` *(${ rangeWithUnits(portee) })*` : ""; const crit = intval(values[section + "crit"], 20); + const atkMods = strval(values[section + "atkmods"]).split(",").map(mod => mod.trim().toLowerCase()); + + const resetValues = new Map(); const grouped = "Attaque groupée"; const bonus5 = { nb: 2, bonus: ` +5[${grouped}]` }; @@ -14619,14 +14924,23 @@ on("clicked:repeating_npcarmes:npcatk-btn", function() { } let specRoll = 0; + let specNext = false; let specBonus = ""; let specDM = ""; let specName = ""; const specMove = intval(values.pnj_spec); if (specMove > 0) { - specRoll = intval(values[`cspec${specMove}_roll`]); + specRoll = intval(values[`cspec${specMove}_` + "roll"]); + specNext = boolval(values[`cspec${specMove}_next`]); specName = strval(values[`cspec${specMove}_nom`]); + const rollMatching = specName.match(/[ ]*jet:([0-9]*)[ ]*/); + if (rollMatching) { + const rollMatched = rollMatching[0]; + specName = specName.replace(rollMatched, "").trim(); + if (!atkMods.includes(rollMatched.trim())) + atkMods.push(rollMatched.trim()); + } const bonus = intval(values[`cspec${specMove}_bonus`]); if (bonus > 0) specBonus = ` + [[${bonus}]][${specName}]`; @@ -14646,7 +14960,10 @@ on("clicked:repeating_npcarmes:npcatk-btn", function() { if (specRoll === 1) roll = "2d20kh1"; if (specRoll === 2) - roll = "2d20kl1"; + roll = "2d20kl1"; + + if (specNext) + resetValues.set("pnj_spec", 0); } @@ -14677,9 +14994,8 @@ on("clicked:repeating_npcarmes:npcatk-btn", function() { } } - const modifiers = strval(values[section + "atkmods"]).split(",").map(mod => mod.trim()); if (boolval(values.cfg_lowfan)) - modifiers.push("explodemax"); + atkMods.push("explodemax"); let [ dm, ...dmextra ] = strval(values[section + "dm"]).split(" "); const dmtype = strval(values[section + "dmtype"]); @@ -14687,10 +15003,14 @@ on("clicked:repeating_npcarmes:npcatk-btn", function() { const dmdiv = strval(values[section + "dmdiv"]); let dmOpt = ""; - if (modifiers.includes("explodemax") && !dm.includes("!")) + if (atkMods.includes("explodemax") && !dm.includes("!")) dmOpt += "!"; - if (modifiers.includes("reroll1")) - dmOpt += "r"; + if (atkMods.includes("reroll1")) { + if (numval(getAtkMod("reroll1")) === 1) + dmOpt += "ro"; + else + dmOpt += "r"; + } let dmroll = ""; if (dm !== "") { @@ -14703,17 +15023,21 @@ on("clicked:repeating_npcarmes:npcatk-btn", function() { dmroll = "[[" + dmroll + " ]]"; } - const specialCmd = []; let special = strval(values[section + "special"]); special = insertRolls(special, { isNPC: true, companionOf }); - while (true) { - const matched = special.match(/jet:([1-9]+)/i); + + const specRolls = []; + + atkMods.forEach(atkMod => { + const matched = atkMod.match(/jet:([0-9]+)/i); if (!matched) - break; - const [ matching, index ] = matched; - special = special.replace(matching, "Spécial : @{" + rptAttr("npcrolls", "npcroll-name", "$" + (intval(index) - 1) ) + "}"); - specialCmd.push("%{" + charName + "|" + rptAttr("npcrolls", "npcroll", "$" + (intval(index) - 1) ) + "}"); - } + return; + const index = matched.slice(1)[0]; + if (!index) + return; + specRolls.push("%{" + charName + "|" + rptAttr("npcrolls", "npcroll", "$" + (intval(index) - 1) ) + "}"); + special = "Spécial : @{" + rptAttr("npcrolls", "npcroll-name", "$" + (intval(index) - 1) ) + "}\n" + special; + }); let dmMax = 0; const result = dm.match(/([0-9]*)[dD]([0-9]*)/); @@ -14830,15 +15154,19 @@ on("clicked:repeating_npcarmes:npcatk-btn", function() { for (let atk = 1; atk <= multiAttacks; atk++) { if (useMulti === 1) chatAtk = { ...chatAtk, lsub: `${atk}${ atk > 1 ? "ème" : "ère" } attaque` }; - const chatMsg = cof2RollTemplate(chatAtk); + const chatMsg = rollTemplate(chatAtk); sendChatMsg(chatMsg, rollOptions); } }); - // Commands in special - if (specialCmd.length > 0) - sendChatCmd(specialCmd.join(" ")); - }) + // Special rolls + if (specRolls.length > 0) + sendChatCmd(specRolls.join(" ")); + + // Reset attributes + if (resetValues.size > 0) + setAttrs(Object.fromEntries(resetValues)); + }); }); /** @@ -14849,14 +15177,14 @@ on("clicked:repeating_npcrolls:npcroll-btn", function() { getAttrs([ "compagnon", `${section}name`, `${section}desc` ], function (values) { const companionOf = strval(values.compagnon); let name = strval(values[section + "name"], "Capacité"); - const action = name.match(/ \((G|M|A|L)\)$/); + const action = name.match(/ \((G|M|A|L)\)[ ]*$/); let actionType = ""; if (action) { name = name.replace(action[0], ""); actionType = " - " + action[1]; } const text = insertRolls(strval(values[section + "desc"]), { isNPC: true, companionOf }); - const chatMsg = cof2RollTemplate({ + const chatMsg = rollTemplate({ lsub: "Capacité" + actionType, rsub: capitalize(name), text @@ -14895,7 +15223,7 @@ function setCompanion(values) { // retrieve values & update attributes if (askValues) { - startRoll("!" + askValues, values => { + startRoll("! " + askValues, values => { const updates = new Map(); const mounted = boolval(values.results.mounted.result); const rider = intval(values.results.rider.result); @@ -14912,6 +15240,8 @@ function setCompanion(values) { updates.set("def", defOwner); if (updates.size > 0) setAttrs(Object.fromEntries(updates)); + + finishRoll(values.rollId); }); } } @@ -15053,10 +15383,11 @@ function statBlockFromPDF(statblock) { ability += sbLine + " "; } } else { + sbLine = sbLine.toUpperCase(); let match; // name & nc if (lineNum === 0) { - match = sbLine.match(/(.*) \| NC[ ]+([0-9]*)/); + match = sbLine.match(/(.+)\|[ ]*NC[ ]+([0-9.,]+)/); if (match) { const [ , character_name, nc ] = match; npcAttrs.set("character_name", character_name); @@ -15066,7 +15397,7 @@ function statBlockFromPDF(statblock) { } // creature type - match = sbLine.match(/CRÉATURE[ ]*(NON VIVANTE|HUMANOÏDE|VÉGÉTATIVE|VIVANTE)/); + match = sbLine.match(/CRÉATURE[ ]+(NON[ ]+VIVANTE|HUMANOÏDE|VÉGÉTATIVE|VIVANTE)/); if (match) { const type = strval(match[1]); if (type !== "") @@ -15074,7 +15405,7 @@ function statBlockFromPDF(statblock) { } // size - match = sbLine.match(/TAILLE[ ]*(MINUSCULE|TRÈS PETITE|PETITE|MOYENNE|GRANDE|ÉNORME|COLOSSALE)/); + match = sbLine.match(/TAILLE[ ]+(MINUSCULE|TRÈS[ ]+PETITE|PETITE|MOYENNE|GRANDE|ÉNORME|COLOSSALE)/); if (match) { const taille = strval(match[1]); if (taille !== "") @@ -15082,24 +15413,30 @@ function statBlockFromPDF(statblock) { } // défense - match = sbLine.match(/\(S\) Défense ([0-9]*) \(([0-9]*)\)/); + match = sbLine.match(/(\(S\))?[ ]*DÉFENSE[ ]+([0-9]+)([ ]+\(([0-9]+)\))?/); if (!match) - match = sbLine.match(/\(S\)DEF ([0-9]*) \(([0-9]*)\)/); + match = sbLine.match(/(\(S\))?[ ]*DEF[ ]+([0-9]+)([ ]\(([0-9]+)\))?/); if (match) { - const [ def, defSup ] = match.slice(1); + match = match.slice(1); + if (match[0] === "(S)") + match = match.slice(1); + const [ def, defSup, defSupVal ] = match; npcAttrs.set("def", def); npcAttrs.set("def_val", def); - if (strval(defSup) !== "") { - npcAttrs.set("def_supval", defSup); + if (defSup) { + npcAttrs.set("def_supval", defSupVal); } } // pv - match = sbLine.match(/\(V\) Points de vigueur ([0-9]*)([ ]\(RD ([0-9]*)\))?/); + match = sbLine.match(/(\(V\))?[ ]*POINTS[ ]+DE[ ]+VIGUEUR[ ]+([0-9]+)([ ]*\(RD[ ]*([0-9]+)\))?/); if (!match) - match = sbLine.match(/\(V\)PV ([0-9]*)([ ]\(RD ([0-9]*)\))?/); + match = sbLine.match(/(\(V\))?[ ]*PV[ ]+([0-9]+)([ ]*\(RD ([0-9]*)\))?/); if (match) { - const [ , pv, rd, rdValue ] = match; + match = match.slice(1); + if (match[0] === "(V)") + match = match.slice(1); + const [ pv, rd, rdValue ] = match; npcAttrs.set("pv", pv); npcAttrs.set("pv_max", pv); if (rd) @@ -15107,11 +15444,14 @@ function statBlockFromPDF(statblock) { } // initiative - match = sbLine.match(/\(I\) Initiative ([0-9]*)/); + match = sbLine.match(/(\(I\))?[ ]*INITIATIVE[ ]+([0-9]+)/); if (!match) - match = sbLine.match(/\(I\)Init. ([0-9]*)/); + match = sbLine.match(/(\(I\))?[ ]*INIT[\.]?[ ]*([0-9]+)/); if (match) { - const [ , init ] = match; + match = match.slice(1); + if (match[0] === "(I)") + match = match.slice(1); + const [ init ] = match; npcAttrs.set("init", init); } @@ -15139,7 +15479,7 @@ function statBlockFromPDF(statblock) { } /** - * + * On click of import NPC statblock button */ on("clicked:importsb-btn", function() { @@ -15198,8 +15538,9 @@ on("clicked:npc_menus-btn", function () { /** * Set/Unset Debug Mode * @param {string} debugMode - value of debug_mode attribute + * @param {function():void} [callback=null] - debug callback */ -function setDebugMode(debugMode) { +function setDebugMode(debugMode, callback = null) { SWData.settings.debugMode = boolval(debugMode); clog( ` Running sheetworker... ${ SWData.settings.debugMode ? "DEBUG" : "PROD" } `, @@ -15209,6 +15550,8 @@ function setDebugMode(debugMode) { backColor: "brown" } ); + if (SWData.settings.debugMode && callback) + callback(); } function migrateCOF1Value(attr, oldVal) { @@ -15691,6 +16034,37 @@ on("sheet:opened change:pc", function() { }); }); +/** + * Update the noms_rangs attribute + */ +function updateAbilityList() { + const allRanksOwned = allPathRanks("rankOwned"); + const allRanks = [ + ...allRanksOwned, + ...allPathRanks("rankName"), + ...allPathRanks("rankProps"), + ...nameSeq(9, abilityInfo().pathName) + ]; + getAttrs(allRanks, function(values) { + const abilityList = allRanksOwned + .filter(owned => { + const { rankOwned, rankProps } = abilityInfo(owned); + return boolval(values[rankOwned]) || hasProp(strval(values[rankProps]), "epic"); + }) + .map(owned => { + const { pathName, rankName } = abilityInfo(owned); + return { id: owned, rank: cleanText(values[rankName]), path: cleanText(values[pathName]) }; + }); + setAttrs({ noms_rangs: JSON.stringify(abilityList) }); + }); +} + +on("sheet:opened " + + changeEvent(allPathRanks("rankOwned")) + + changeEvent(allPathRanks("rankName")) + + changeEvent(allPathRanks("rankProps")) + + changeEvent(nameSeq(9, abilityInfo().pathName)), updateAbilityList); + /** * Sync pv <=> PVIG for token bars */ @@ -15710,6 +16084,9 @@ on("change:pv", function(eventInfo) { }); }); +/** + * Sync PVIG <=> pv for token bars + */ on("change:pvig", function(eventInfo) { if (strval(eventInfo.sourceType) !== "player") return; @@ -15729,6 +16106,9 @@ on("change:pvig", function(eventInfo) { }); }); +/** + * Sync pv_max <=> PVIG_max for token bars + */ on("change:pv_max", function(eventInfo) { if (eventInfo.sourceType !== "player") return; @@ -15738,6 +16118,9 @@ on("change:pv_max", function(eventInfo) { }); }); +/** + * Sync PVIG_max <=> pv_max for token bars + */ on("change:pvig_max", function(eventInfo) { if (eventInfo.sourceType !== "player") return; @@ -15747,14 +16130,21 @@ on("change:pvig_max", function(eventInfo) { }); }); -on("clicked:token_min-btn clicked:token_max-btn clicked:token_def-btn", function(eventInfo) { +/** + * On clicking token adjust buttons (TokenMod) + */ +on("clicked:token_min-btn clicked:token_max-btn clicked:token_siz-btn clicked:token_def-btn", function(eventInfo) { getAttrs([ "sheet_type", "character_name", "pm_max" ], function(values) { - const [ bt ] = strval(eventInfo.triggerName).split("_"); - const resize = bt === "min-btn" ? 0.9 : bt === "max-btn" ? 1.1 : 0; + const bt = strval(eventInfo.triggerName.split("_")[1]); + const resize = strval(({ + "min-btn": "0.9", + "max-btn": "1.1", + "siz-btn": "?{*Taille ?}", + })[bt]); const isNPC = strval(values.sheet_type) === SWData.SHEET_TYPE.NPC; let props = {}; - if (resize > 0) { - props = { width: resize, height: resize }; + if (resize !== "") { + props = { scale: "*" + resize }; } else { props = { bar2_link: "def" }; if (isNPC) { @@ -15770,6 +16160,10 @@ on("clicked:token_min-btn clicked:token_max-btn clicked:token_def-btn", function }); +// ====================== +// CHARACTERMANCER EVENTS +// ====================== + /** * Run charactermancer */