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 @@
- {{#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
*/