From 1cf4a54b7581f9c27900cf696151b51225c7651c Mon Sep 17 00:00:00 2001 From: Ulty Date: Fri, 17 Feb 2023 09:58:53 +0100 Subject: [PATCH] Chroniques Oubliees: support parsing of JSON statblocks from co-drs.org --- ChroniquesOubliees/ReadMe.md | 1 + ChroniquesOubliees/co.html | 489 +++++++++++++++++++++++++++++++---- 2 files changed, 436 insertions(+), 54 deletions(-) diff --git a/ChroniquesOubliees/ReadMe.md b/ChroniquesOubliees/ReadMe.md index 93080e05ab..318c1ac9b7 100644 --- a/ChroniquesOubliees/ReadMe.md +++ b/ChroniquesOubliees/ReadMe.md @@ -15,6 +15,7 @@ Le jeu complet, des suppléments et des téléchargements grat # Notes de version ## v5.02 (2023-01-26) +* Support des statblocks JSON de CO-DRS * Ajout des munitions * Champs prédicat pour les armes portées diff --git a/ChroniquesOubliees/co.html b/ChroniquesOubliees/co.html index 2ccf07c588..6abe430952 100644 --- a/ChroniquesOubliees/co.html +++ b/ChroniquesOubliees/co.html @@ -4248,9 +4248,9 @@ }; function buildRegexp(obj) { - var res = "\\b("; - var premier = true; - for (var field in obj) { + let res = "\\b("; + let premier = true; + for (const field in obj) { if (premier) { res += field; premier = false; @@ -4342,7 +4342,7 @@ }]; function keyListToMap(l) { - var res = {}; + let res = {}; l.forEach(function(s) { s.keys.forEach(function(k) { res[k] = s; @@ -4396,25 +4396,405 @@ } } + //parse les expressions du type 3d6+4 + //renvoie un structure avec les champs nbDe, dice et bonus + function parseDice(expr, msg) { + let dm = { + nbDe: 0, + dice: 4, + bonus: 0 + }; + let exprDM = expr.trim().toLowerCase(); + let indexD = exprDM.indexOf('d'); + if (indexD > 0) { + dm.nbDe = parseInt(exprDM.substring(0, indexD)); + if (isNaN(dm.nbDe) || dm.nbDe < 0) { + if (msg) + console.log("Expression de " + msg + ' ' + exprDM + " mal formée"); + console.log(expr); + return; + } + exprDM = exprDM.substring(indexD + 1); + indexD = exprDM.search(/[+\-]/); + if (indexD <= 0) { + dm.dice = parseInt(exprDM); + if (isNaN(dm.dice) || dm.dice < 1) { + if (msg) + console.log("Nombre de faces incorrect dans l'expression de " + msg); + console.log(expr); + return; + } + return dm; + } + exprDM = exprDM.replace('+-', '-'); + dm.dice = parseInt(exprDM.substring(0, indexD)); + if (isNaN(dm.dice) || dm.dice < 1) { + if (msg) + console.log("Nombre de faces incorrect dans l'expression de " + msg); + console.log(expr); + return; + } + exprDM = exprDM.substring(indexD).trim(); + } + dm.bonus = parseInt(exprDM); + if (isNaN(dm.bonus)) { + if (msg) + console.log("Expression de " + msg + " incorrecte"); + console.log(expr); + return; + } + return dm; + } + + const voiesDeCreatures = { + 82: { + nom: 'Voie du prédateur', + 1: { + actions: [{ + titre: 'Embuscade des joueurs', + code: "!cof-surprise [[15+@{selected|DEX}]] --equipe joueurs" + }, { + titre: 'Attaque cible surprise', + code: "#Attaque -1 --sournoise 1 --if moins FOR --etat renverse --endif" + }, ], + }, + 2: { + predicats: 'devorer ', + }, + 3: { + predicats: 'gober ', + }, + }, + 84: { + nom: 'Voie de la meute', + 1: { + predicats: 'interchangeable ', + }, + 2: { + actions: [{ + titre: 'Attaque en traître', + code: '#Attaque -1 --sournoise 2 --bonusAttaque 5' + }] + }, + }, + 87: { + nom: 'Voie de la magie de combat', + 2: { + actions:[{ + titre: 'Armure magique', + code:'!cof-effet-combat armureMagique' + }] + }, + }, + 88: { + nom: 'Voie des créatures volantes', + 1: { + actions: [{ + titre: 'Attaque en piqué', + code: '#Attaque -1 --bonusAttaque 5 --plus 1d6', + }], + 2: { + predicats: 'agripper ', + }, + }, + }, + 89: { + nom: 'Voie des créatures magiques', + }, + 91: { + nom: 'Voie du colosse', + 1: { + predicats: 'fauchage ' + }, + 2: { + actions:[{ + titre: 'Balayage', + code: '#Attaque -1 --target @{target|Cible 2|token_id}' + }] + }, + 3: { + predicats: 'projection ' + } + }, + 92: { + nom: 'Voie du cogneur', + 1: { + actions:[{ + titre:'Charge', + code:'#Attaque -1 --m2d20 --pietine' + }], + }, + 2: { + predicats: 'peutEnrager ' + }, + 3: { + actions:[{ + titre:'Charge (remplace le rang 1)', + code:'#Attaque -1 --m2d20 --percute' + }], + } + }, + 93: { + nom: 'Voie du champion', + 1: { + actions: [{ + titre: 'Attaque imparable (L)', + code: '#Attaque -1 --m2d20' + }], + predicats: 'champion ', + }, + 2: { + predicats: 'riposte ', + }, + 3: { + predicats: 'hausserLeTon ', + }, + }, + 94: { + nom: "Voie du chef d'armée", + 1: { + predicats: 'sergent ', + }, + 2: { + actions: [{ + titre:'Devenir capitaine de', + code:'!cof-capitaine @{selected|token_id} 2 --target @{target|token_id}', + }], + }, + 3: { + actions: [{ + titre:'Devenir commandant de', + code:'!cof-capitaine @{selected|token_id} 3 --target @{target|token_id}', + }], + predicats: 'commandant ', + }, + }, + }; + + //Les champs JSON qui peuvent être directement transcrits + const statsJSON = { + str_mod: 'pnj_for', + dex_mod: 'pnj_dex', + con_mod: 'pnj_con', + int_mod: 'pnj_int', + wis_mod: 'pnj_sag', + cha_mod: 'pnj_cha', + creature_family: 'race', + profile: 'profil', + level: 'niveau', + size: 'taille', + profil: 'profil', + init: 'pnj_init', + defense: 'pnj_def', + health_point: 'PV_max', + dmg_reduction: 'RDS', + name: 'character_name', + archetype: 'archetype', + boss_rank: 'boss_rank', + boss_type: 'boss_type', + description: 'notes', + special_capabilities: 'capacites_pnj', + }; + + function htmlToText(msg) { + msg = msg.replace(/

||<\/strong>||<\/em>|
/g, ''); + msg = msg.replace(/<\/p>/g, '\n'); + return msg; + } + + function readJSONStats(stats, maxAttack, values) { + console.log(stats); + let se = {}; + for (const f in statsJSON) { + if (stats[f] && stats[f].length > 0) { + let val = stats[f][0].value; + if (stats[f][0].label) val = stats[f][0].label; + if (val) { + if (stats[f][0].format == 'simple_html' || + stats[f][0].format == 'basic_html') val = htmlToText(val); + se[statsJSON[f]] = val; + } + } + } + if (stats.comments && stats.comments.length > 0 && stats.comments[0].value) { + if (se.notes) se.notes += '\n'; + else se.notes = ''; + se.notes += htmlToText(stats.comments[0].value); + } + if (stats.paths && stats.paths.length > 0) { + let voies = stats.paths[0].value; + if (voies) { + if (se.capacites_pnj) se.capacites_pnj += '\n'; + else se.capacites_pnj = ''; + se.capacites_pnj += htmlToText(voies); + } + } + let predicats = ''; + if (values.predicats_script) predicats = values.predicats_script + ' '; + if (stats.category && stats.category.length > 0) { + switch (stats.category[0].value) { + case 'alive': //rien à faire + break; + case 'humanoid': + predicats += 'humanoide '; + break; + case 'vegetative': + predicats += 'vegetatif '; + break; + case 'non_living': + predicats += 'nonVivant '; + break; + default: + console.log("Catégorie " + stats.category[0].value + " non reconnue"); + } + } + if (stats.sup_abilities) { + stats.sup_abilities.forEach(function(sab) { + se['pnj_' + sab.label.toLowerCase() + '_sup'] = 'on'; + se[sab.label.toLowerCase() + '_sup'] = '@{jetsup}'; + }); + } + if (se.niveau) se.niveau = parseInt(se.niveau); + se.PV = se.PV_max; + if (se.RDS) { + se.RDS = se.RDS.replace(/Armes magiques/, 'magique'); + se.RDS = se.RDS.replace(/magie/g, 'magique'); + } + if (stats.attacks && stats.attacks.length > 0) { + let atd = stats.attacks[0].data; + if (atd) { + atd.forEach(function(att) { + let nomAttaque = ''; + if (att.name) nomAttaque = att.name; + let nbDe = 0; + let de = 4; + let bonusDM = 0; + if (att.dm) { + let d = parseDice(att.dm, "dégâts de l'attaque"); + if (d) { + nbDe = d.nbDe; + de = d.dice; + bonusDM = d.bonus; + } + } + let bonusAtt = parseInt(att.test); + if (isNaN(bonusAtt)) { + console.log("Bonus d'attaque incorrect"); + console.log(att); + bonusAtt = ''; + } + let portee = parseInt(att.reach); + if (isNaN(portee)) portee = 0; + if (portee) { + //on va essayer d'enlever les indications de portée du nom de l'attaque. + let index = nomAttaque.indexOf('(' + portee); + if (index > 0) { + let index2 = nomAttaque.indexOf(')', index); + if (index2 > 0) { + if (index2 < nomAttaque.length - 1) { + nomAttaque = nomAttaque.substring(0, index) + nomAttaque.substring(index2 + 1); + } else { + nomAttaque = nomAttaque.substring(0, index); + } + nomAttaque = nomAttaque.trim(); + } + } + } + const prefix = 'repeating_pnjatk_' + generateRowID() + '_arme'; + maxAttack++; + se[prefix + 'label'] = maxAttack; + se[prefix + 'nom'] = nomAttaque; + se[prefix + 'atk'] = bonusAtt; + se[prefix + 'dmnbde'] = nbDe; + se[prefix + 'dmde'] = de; + se[prefix + 'dm'] = bonusDM; + if (att.special) se[prefix + 'spec'] = att.special; + if (portee) se[prefix + 'portee'] = portee; + // On essaie de trouver des valeurs pour le type de l'attaque + guessAttackType(nomAttaque, prefix, se, values); + }); + } + } + let maxRangAction = parseInt(values.maxrangaction); + if (isNaN(maxRangAction)) maxRangAction = 0; + const addAction = function(a) { + maxRangAction++; + const prefix = 'repeating_actions_' + generateRowID() + '_action'; + se[prefix + 'rang'] = maxRangAction; + for (const champ in a) { + se[prefix + champ] = a[champ]; + } + }; + if (stats.structured_capabilities) { + for (const vid in stats.structured_capabilities) { + let sc = stats.structured_capabilities[vid]; + switch (sc.type) { + case 'creature': + let v = voiesDeCreatures[vid]; + if (v) { + for (const rang in sc.capabilities) { + if (rang == 'nom') continue; + let r = v[rang]; + if (r) { + if (r.actions) { + r.actions.forEach(addAction); + } + if (r.predicats) { + predicats += r.predicats; + } + } + } + } + break; + case 'character': + case 'prestige': + case 'race': //On ne sait pas encore faire + break; + default: + console.log("Voie non reconnue"); + console.log(sc); + } + } + } + if (predicats) se.predicats_script = predicats; + if (maxAttack) se.max_attack_label = maxAttack; + if (maxRangAction) se.maxrangaction = maxRangAction; + setAttrs(se); + } + //Parsing des statblocks. Peut cascader on('change:statblock', function(eventinfo) { - getAttrs(['max_attack_label', 'scriptVersion', 'scriptVersion_max', 'montrerarmeenmain'], function(values) { - var maxAttack = parseInt(values.max_attack_label); + getAttrs(['max_attack_label', 'scriptVersion', 'scriptVersion_max', 'montrerarmeenmain', 'predicats_script', 'maxrangaction'], function(values) { + let maxAttack = parseInt(values.max_attack_label); if (isNaN(maxAttack)) maxAttack = 0; - var stats = eventinfo.newValue; - if (stats === '') return; + let stats = eventinfo.newValue; + if (stats === '' || stats === undefined) return; + if (stats.startsWith('{"name":')) { + let jstats; + try { + jstats = JSON.parse(stats); + } catch (e) { + console.log("Erreur de décodage JSON du stat block"); + console.log(e); + jstats = undefined; + } + if (jstats) { + readJSONStats(jstats, maxAttack, values); + return; + } + } stats = stats.replace(/\r/g, ''); //On enlève les caractères images des dernières versions de COF stats = stats.replace(/\ue1e9/g, ''); //DEF stats = stats.replace(/♥/g, ''); //PV stats = stats.replace(/\ue0bf/g, ''); //Attaque stats = stats.replace(/\, /g, ' '); - var rows = stats.split(/\n/); - var newAttrs = {}; - var previousLine = ''; //Au cas d'attaque sur plusieurs lignes - var previousContainsDM = false; - var lastPrefix = ''; //Pour les suites d'attaque sur la ligne suivante - var firstLine = true; //si la première ligne ne correspond à rien, c'est le nom + let rows = stats.split(/\n/); + let newAttrs = {}; + let previousLine = ''; //Au cas d'attaque sur plusieurs lignes + let previousContainsDM = false; + let lastPrefix = ''; //Pour les suites d'attaque sur la ligne suivante + let firstLine = true; //si la première ligne ne correspond à rien, c'est le nom rows.forEach(function(row) { if (row === '') { previousLine = ''; @@ -4428,17 +4808,18 @@ previousLine += row + ' '; previousContainsDM = true; } else if (previousContainsDM || row.search(/\bDM\b/i) >= 0) { //Attaque - var currentRow = previousLine + row; - var nomAttaqueFin = currentRow.search(/\s(\+\d+|0\b|DM)/i); - var nomAttaque = currentRow.substring(0, nomAttaqueFin); - var portee = /\(\d+\s*(m|mètre|mètres|metre|metres)\)/i.exec(nomAttaque); + let currentRow = previousLine + row; + let nomAttaqueFin = currentRow.search(/\s(\+\d+|0\b|DM)/i); + let nomAttaque = currentRow.substring(0, nomAttaqueFin); + let portee = + /\(\d+\s*(m|mètre|mètres|metre|metres)\)/i.exec(nomAttaque); if (portee) { nomAttaque = nomAttaque.substring(0, portee.index).trim(); portee = parseInt(portee[0].substring(1)); if (isNaN(portee)) portee = 0; } currentRow = currentRow.substring(nomAttaqueFin).trim(); - var bonusAtt = 0; + let bonusAtt = 0; if (currentRow.startsWith('+')) { currentRow = currentRow.substring(1); bonusAtt = parseInt(currentRow); @@ -4451,9 +4832,9 @@ } else if (currentRow.startsWith('0')) { currentRow = currentRow.substring(1).trim(); } - var index = currentRow.search(/\bDM\b/i); - var specAtt = currentRow.substring(0, index).trim(); - var specInAttackName = /\([^\)]*\)/i.exec(nomAttaque); + let index = currentRow.search(/\bDM\b/i); + let specAtt = currentRow.substring(0, index).trim(); + let specInAttackName = /\([^\)]*\)/i.exec(nomAttaque); if (specInAttackName) { nomAttaque = nomAttaque.substring(0, specInAttackName.index).trim(); specAtt += ' ' + specInAttackName[0].substring(1, specInAttackName[0].length - 1); @@ -4472,11 +4853,11 @@ if (index < 0) { index = currentRow.length; } - var nbDe = 0; - var de = 4; - var bonusDM = 0; - var dmExpr = currentRow.substring(0, index); - var indexDe = dmExpr.search(/d/i); + let nbDe = 0; + let de = 4; + let bonusDM = 0; + let dmExpr = currentRow.substring(0, index); + let indexDe = dmExpr.search(/d/i); if (indexDe < 0) { bonusDM = parseInt(dmExpr); if (isNaN(bonusDM)) { @@ -4495,7 +4876,7 @@ previousContainsDM = true; return; } - var indexBonusDM = dmExpr.search(/(\+|-)/); + let indexBonusDM = dmExpr.search(/(\+|-)/); if (indexBonusDM >= 0) { bonusDM = parseInt(dmExpr.substring(indexBonusDM)); if (isNaN(bonusDM)) { @@ -4515,7 +4896,7 @@ if (index >= 0 && index < currentRow.length - 1) { specAtt += currentRow.substring(index).trim(); } - var prefix = 'repeating_pnjatk_' + generateRowID() + '_arme'; + const prefix = 'repeating_pnjatk_' + generateRowID() + '_arme'; maxAttack++; newAttrs[prefix + 'label'] = maxAttack; newAttrs[prefix + 'nom'] = nomAttaque; @@ -4534,9 +4915,9 @@ newAttrs[lastPrefix + 'spec'] += row; } else { //Pas attaque lastPrefix = ''; - var lexemes = []; - var lastMatch; - var match = patternStats.exec(row); + let lexemes = []; + let lastMatch; + let match = patternStats.exec(row); while (match) { if (lastMatch) { lexemes.push({ @@ -4562,19 +4943,19 @@ } } else previousLine = ''; lexemes.forEach(function(l) { - var lstat = l.match[0].toLowerCase(); - var lattr = statsReconnues[lstat]; + let lstat = l.match[0].toLowerCase(); + let lattr = statsReconnues[lstat]; if (lattr === undefined && lstat == 'créature') lattr = statsReconnues.creature; if (lattr === undefined) { console.log("Erreur ! Pattern " + lstat + " non reconne. La ligne était " + l); return; } - var valAttr = row.substring(l.match.index + lstat.length, l.end).trim(); + let valAttr = row.substring(l.match.index + lstat.length, l.end).trim(); valAttr = valAttr.replace(/\)$/, ''); if (lattr.search(/(for|dex|con|sag|int|cha)/) > 0) { //Cas ou on rajoute une autre valeur pour la caractéristique //par exemple pour le centaure - var caracParen = valAttr.search(/\([+-]\d/); + let caracParen = valAttr.search(/\([+-]\d/); if (caracParen > 0) { valAttr = valAttr.substring(0, caracParen - 1).trim(); } @@ -4908,19 +5289,19 @@ on('change:attaque_risquee_check change:attaque_dm_temp_check', function(eventinfo) { getAttrs(['attaque_risquee_check', 'attaque_dm_temp_check'], function(values) { - var attaque_risquee = parseInt(values.attaque_risquee_check); - var attaque_non_lethale = parseInt(values.attaque_dm_temp_check); - var updateArmes = function(section) { + let attaque_risquee = parseInt(values.attaque_risquee_check); + let attaque_non_lethale = parseInt(values.attaque_dm_temp_check); + const updateArmes = function(section) { getSectionIDs('repeating_' + section, function(idArray) { idArray.forEach(function(id) { - var prefix = 'repeating_' + section + '_' + id + '_'; + const prefix = 'repeating_' + section + '_' + id + '_'; getAttrs([prefix + 'armeportee', prefix + 'armemodificateurs'], function(values) { - var portee = values[prefix + 'armeportee']; - var modifs = values[prefix + 'armemodificateurs'].split(','); - var bonus = 0; + let portee = values[prefix + 'armeportee']; + let modifs = values[prefix + 'armemodificateurs'].split(','); + let bonus = 0; if (attaque_non_lethale) bonus = -2; - var attrs = {}; - var dmTemp = attaque_non_lethale; + let attrs = {}; + let dmTemp = attaque_non_lethale; modifs.forEach(function(m) { switch (m.trim()) { case 'tempDmg': @@ -5036,16 +5417,16 @@ on('change:repeating_pnjatk:armeportee change:repeating_pnjatk:armemodificateurs', function(eventinfo) { getAttrs(['attaque_risquee_check', 'attaque_dm_temp_check', 'repeating_pnjatk_armeportee', 'repeating_pnjatk_armemodificateurs'], function(values) { - var attaque_risquee = parseInt(values.attaque_risquee_check); - var attaque_non_lethale = parseInt(values.attaque_dm_temp_check); - var portee = values.repeating_pnjatk_armeportee; - var modifs = values.repeating_pnjatk_armemodificateurs.split(','); - var bonus = 0; + let attaque_risquee = parseInt(values.attaque_risquee_check); + let attaque_non_lethale = parseInt(values.attaque_dm_temp_check); + let portee = values.repeating_pnjatk_armeportee; + let modifs = values.repeating_pnjatk_armemodificateurs.split(','); + let bonus = 0; if (attaque_non_lethale) bonus = -2; - var attrs = {}; - var dmTemp = attaque_non_lethale; - var nbDes = 1; //Nombre de dés pour l'attaque - var plusFort = true; + let attrs = {}; + let dmTemp = attaque_non_lethale; + let nbDes = 1; //Nombre de dés pour l'attaque + let plusFort = true; attrs.repeating_pnjatk_armedegats = DEGATSPNJ; attrs.repeating_pnjatk_armepoudre = '{{poudre=}}'; attrs.repeating_pnjatk_armereussiteauto = '{{attaqueautomatique=}}';