-
+
+
+
+
+
Attaque groupée
+
+
-
-
+
+
+
+ 2 créatures
+
+
+ +5 en attaque
+
-
-
+
+
+
+ 3 créatures
+
+
+ +10 en attaque
+
+
+
+
+
+
+ 4 créatures
+
+
+ Touche automatique
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+ CAPACITÉ
+
+
+
DESCRIPTION
+
+
+
+
+
+
+
+
+
+
+
@@ -4908,61 +5473,79 @@
Avec token
+
+
+ Un seul jet d'attaque
+
+
+
+
+
Règles optionnelles
+
+
+
Compagnon
+
+
-
- Règles optionnelles :
-
+
+
+
+ -
+
-
-
-
Compagnon de :
-
-
+
+
+
+
-
+
+
+
+
-
+
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
-
@@ -5009,11 +5592,114 @@
+
+
Listes d'actions
+
+
+
+
+
+
+
+
+
Prédicats
-
+
@@ -5426,11 +6112,30 @@
{{/rollWasCrit() broll}}
{{/dmcrit}}
+ {{#roll}}
+ {{#rollTotal() computed::roll 1}}
+
+ {{rolleffect}}
+
+ {{/rollTotal() computed::roll 1}}
+ {{#broll}}
+ {{#rollTotal() computed::broll 1}}
+
+ Si dé bonus : {{brolleffect}}
+
+ {{/rollTotal() computed::broll 1}}
+ {{/broll}}
+ {{/roll}}
{{#text}}
{{text}}
{{/text}}
+ {{#chance}}
+
+ {{chance}}
+
+ {{/chance}}
{{#alert}}
{{alert}}
@@ -5461,6 +6166,7 @@
/**
* COF2e Sheet Worker
+ * v1.3.0
*/
const SWData = {
@@ -5471,6 +6177,7 @@ const SWData = {
peoplePath: 1,
profilePaths: [ 2, 3 ],
level1Paths: [ 1, 2, 3 ],
+ minLevels: [ 1, 2, 3, 5, 7, 9, 11, 13 ],
},
SHEET_TYPE: {
PC: "pc",
@@ -5799,7 +6506,7 @@ const SWData = {
},
],
},
- ABILITIES: [
+ STATS: [
"agilité",
"constitution",
"force",
@@ -5827,7 +6534,7 @@ const SWData = {
"armure", "armure_eqp",
"bouclier", "bouclier_eqp",
"agi", "con", "def_car_bonus", "def_car_max",
- "capabilities",
+ "rangs_voies", "noms_voies",
"def_cond",
"def_buff",
"def_action"
@@ -5835,13 +6542,8 @@ const SWData = {
},
BUFFS: {
To: [
- "agi",
- "con",
- "for",
- "per",
- "cha",
- "int",
- "vol",
+ "agi", "con", "for", "per", "cha", "int", "vol",
+ "tagi", "tcon", "tfor", "tper", "tcha", "tint", "tvol",
"init",
"atkcac",
"atktir",
@@ -5963,9 +6665,6 @@ const SWData = {
]
}
],
- CAPABILITIES: [
- "peaudepierre"
- ],
SKILLS: [
{
name: "acrobaties",
@@ -6085,22 +6784,21 @@ const SWData = {
"pa",
"pc",
],
- GEARS: {
- props: [
- "dm-1m",
- "dm-2m",
- "type-dm",
- "portee",
- "critique",
- "bonus-def",
- "armure",
- "armure-malus",
- "bouclier",
- "bouclier-malus",
- "casque",
- "casque-malus",
- ]
- },
+ GEARPROPS: [
+ "dm-1m",
+ "dm-2m",
+ "type-dm",
+ "portee",
+ "critique",
+ "usure",
+ "bonus-def",
+ "armure",
+ "armure-malus",
+ "bouclier",
+ "bouclier-malus",
+ "casque",
+ "casque-malus",
+ ],
COMPANIONS: [
"def",
"pv_max",
@@ -6109,151 +6807,592 @@ const SWData = {
],
GEARS: [
{
- name: "Pétoire",
- props: "dm-1m: 1d10,portee: 20,type-dm: Perforants",
- },
- {
- name: "Epée longue",
- props: "dm-1m: 1d8,type-dm: Tranchants",
- },
- {
- name: "Dague",
- props: "dm-1m: 1d4,type-dm: Perforants",
- },
- {
- name: "Rapière",
- props: "critique: 19,dm-1m: 1d6,type-dm: Perforants",
- },
- {
- name: "Arc court",
- props: "dm-2m: 1d6,portee: 30,type-dm: Perforants",
- },
- {
- name: "Hache à deux mains",
- props: "dm-2m: 2d6,type-dm: Tranchants",
- },
- {
- name: "Javelot",
- props: "dm-1m: 1d6, type-dm: Perforants",
- },
- {
- name: "Lance de cavalerie",
- props: "dm-1m: 2d6, type-dm: Perforants",
+ name: "Bâton",
+ altname: "",
+ props: [
+ "dm-2m: 1d4",
+ "type-dm: Contondants",
+ ]
},
{
name: "Bâton ferré",
- props: "dm-2m: 1d6, type-dm: Contondants",
+ altname: "",
+ props: [
+ "dm-2m: 1d6",
+ "type-dm: Contondants",
+ ]
+ },
+ {
+ name: "Dague",
+ altname: "",
+ props: [
+ "dm-1m: 1d4",
+ "type-dm: Perforants",
+ ]
+ },
+ {
+ name: "Epée à deux mains",
+ altname: "epee a 2 mains,epee 2m",
+ props: [
+ "dm-2m: 2d6",
+ "type-dm: Tranchants",
+ ]
+ },
+ {
+ name: "Epée bâtarde",
+ altname: "",
+ props: [
+ "dm-1m: 1d8",
+ "dm-2m: 1d12",
+ "type-dm: Tranchants",
+ ]
+ },
+ {
+ name: "Epée courte",
+ altname: "",
+ props: [
+ "dm-1m: 1d6",
+ "type-dm: Perforants",
+ ]
+ },
+ {
+ name: "Epée longue",
+ altname: "",
+ props: [
+ "dm-1m: 1d8",
+ "type-dm: Tranchants",
+ ]
},
{
name: "Epieu",
- props: "dm-1m: 1d6,dm-2m: 1d10,type-dm: Perforants",
+ altname: "",
+ props: [
+ "dm-1m: 1d6",
+ "dm-2m: 1d10",
+ "type-dm: Perforants",
+ ]
},
{
- name: "Marteau de guerre",
- props: "dm-1m: 1d6,type-dm: Contondants",
+ name: "Fléau",
+ altname: "",
+ props: [
+ "dm-1m: 1d6",
+ "type-dm: Contondants",
+ ]
+ },
+ {
+ name: "Fléau à deux mains",
+ altname: "fleau a 2m,fleau 2m",
+ props: [
+ "dm-2m: 1d10",
+ "type-dm: Contondants",
+ ]
+ },
+ {
+ name: "Gourdin",
+ altname: "",
+ props: [
+ "dm-1m: 1d4",
+ "type-dm: Contondants",
+ ]
+ },
+ {
+ name: "Hache",
+ altname: "",
+ props: [
+ "dm-1m: 1d8",
+ "type-dm: Tranchants",
+ ]
+ },
+ {
+ name: "Hache à deux mains",
+ altname: "hache a 2m,hache 2m",
+ props: [
+ "dm-2m: 2d6",
+ "type-dm: Tranchants",
+ ]
+ },
+ {
+ name: "Lance",
+ altname: "",
+ props: [
+ "dm-1m: 1d6",
+ "dm-2m: 1d10",
+ "type-dm: Perforants",
+ ]
+ },
+ {
+ name: "Lance de cavalerie",
+ altname: "",
+ props: [
+ "dm-1m: 2d6",
+ "type-dm: Perforants",
+ ]
+ },
+ {
+ name: "Marteau",
+ altname: "marteau de guerre",
+ props: [
+ "dm-1m: 1d6",
+ "type-dm: Contondants",
+ ]
+ },
+ {
+ name: "Masse",
+ altname: "",
+ props: [
+ "dm-1m: 1d6",
+ "type-dm: Contondants",
+ ]
+ },
+ {
+ name: "Pique",
+ altname: "",
+ props: [
+ "dm-2m: 1d10",
+ "type-dm: Perforants",
+ ]
+ },
+ {
+ name: "Rapière",
+ altname: "",
+ props: [
+ "critique: 19",
+ "dm-1m: 1d6",
+ "type-dm: Perforants",
+ ]
+ },
+ {
+ name: "Stylet",
+ altname: "",
+ props: [
+ "dm-1m: 1d3",
+ "type-dm: Perforants",
+ ]
+ },
+ {
+ name: "Vivelame",
+ altname: "",
+ props: [
+ "critique: 19",
+ "dm-2m: 1d10",
+ "type-dm: Tranchants",
+ ]
+ },
+ {
+ name: "Arbalète de poing",
+ altname: "",
+ props: [
+ "dm-1m: 1d6",
+ "portee: 10",
+ "type-dm: Perforants",
+ ]
+ },
+ {
+ name: "Arbalète lègère",
+ altname: "",
+ props: [
+ "dm-2m: 2d4",
+ "portee: 30",
+ "type-dm: Perforants",
+ ]
+ },
+ {
+ name: "Arbalète lourde",
+ altname: "",
+ props: [
+ "dm-2m: 2d6",
+ "portee: 60",
+ "type-dm: Perforants",
+ ]
+ },
+ {
+ name: "Arc court",
+ altname: "",
+ props: [
+ "dm-2m: 1d6",
+ "portee: 30",
+ "type-dm: Perforants",
+ ]
+ },
+ {
+ name: "Arc long",
+ altname: "",
+ props: [
+ "dm-2m: 1d8",
+ "portee: 50",
+ "type-dm: Perforants",
+ ]
+ },
+ {
+ name: "Couteaux de lancer",
+ altname: "",
+ props: [
+ "dm-1m: 1d4",
+ "portee: 10",
+ "type-dm: Perforants",
+ ]
+ },
+ {
+ name: "Fronde",
+ altname: "",
+ props: [
+ "dm-1m: 1d4",
+ "portee: 20",
+ "type-dm: Contondants",
+ ]
+ },
+ {
+ name: "Hachette",
+ altname: "",
+ props: [
+ "dm-1m: 1d6",
+ "portee: 5",
+ "type-dm: Perforants",
+ ]
+ },
+ {
+ name: "Javelot",
+ altname: "",
+ props: [
+ "dm-1m: 1d6",
+ "type-dm: Perforants",
+ ]
+ },
+ {
+ name: "Lance-pierre",
+ altname: "lance-pierres,lance pierre,lance pierres",
+ props: [
+ "dm-1m: 1d3",
+ "type-dm: Contondants",
+ ]
+ },
+ {
+ name: "Pétoire",
+ altname: "",
+ props: [
+ "dm-1m: 1d10",
+ "portee: 20",
+ "type-dm: Perforants",
+ ]
+ },
+ {
+ name: "Mousquet",
+ altname: "",
+ props: [
+ "dm-2m: 2d6",
+ "portee: 50",
+ "type-dm: Perforants",
+ ]
+ },
+ {
+ name: "Tissus matelassés",
+ altname: "tissu matelasse",
+ props: [
+ "armure: +1",
+ "agi-max: +7",
+ ]
+ },
+ {
+ name: "Fourrures",
+ altname: "fourrure",
+ props: [
+ "armure: +1",
+ "agi-max: +7",
+ ]
},
{
name: "Cuir simple",
- props: "armure: +2,agi-max: +6",
+ altname: "armure de cuir",
+ props: [
+ "armure: +2",
+ "agi-max: +6",
+ ]
},
{
name: "Cuir renforcé",
- props: "armure: +3,agi-max: +5",
+ altname: "armure de cuir renforce",
+ props: [
+ "armure: +3",
+ "agi-max: +5",
+ ]
},
{
name: "Chemise de mailles",
- props: "armure: +4,agi-max: +4",
+ altname: "",
+ props: [
+ "armure: +4",
+ "agi-max: +4",
+ ]
},
{
name: "Cotte de mailles",
- props: "armure: +5,agi-max: +3",
+ altname: "",
+ props: [
+ "armure: +5",
+ "agi-max: +3",
+ ]
+ },
+ {
+ name: "Armure de plaques",
+ altname: "demi-plaque,demi plaque",
+ props: [
+ "armure: +6",
+ "agi-max: +2",
+ ]
+ },
+ {
+ name: "Plaque complète",
+ altname: "harnois",
+ props: [
+ "armure: +7",
+ "agi-max: +1",
+ ]
},
{
name: "Petit bouclier",
- props: "bouclier: +1",
+ altname: "",
+ props: [
+ "bouclier: +1",
+ ],
},
{
name: "Grand bouclier",
- props: "bouclier: +2",
+ altname: "",
+ props: [
+ "bouclier: +2",
+ ],
},
],
PVPATH: [
- { pv: 4, name: "artilleur" },
- { pv: 4, name: "explosifs" },
- { pv: 4, name: "mercenaire" },
- { pv: 4, name: "pistolero"},
- { pv: 4, name: "précision" },
- { pv: 4, name: "escrime" },
- { pv: 4, name: "musicien" },
- { pv: 4, name: "saltimbanque"},
- { pv: 4, name: "séduction" },
- { pv: 4, name: "vagabond" },
- { pv: 4, name: "archer" },
- { pv: 4, name: "compagnon animal" },
- { pv: 4, name: "survie" },
- { pv: 4, name: "traqueur" },
- { pv: 4, name: "combat à deux armes" },
- { pv: 4, name: "assassin" },
- { pv: 4, name: "aventurier" },
- { pv: 4, name: "déplacement" },
- { pv: 4, name: "roublard" },
- { pv: 4, name: "spadassin" },
- { pv: 5, name: "brute" },
- { pv: 5, name: "pagne" },
- { pv: 5, name: "pourfendeur" },
- { pv: 5, name: "primitif" },
- { pv: 5, name: "rage" },
- { pv: 5, name: "cavalier" },
- { pv: 5, name: "guerre" },
- { pv: 5, name: "preux" },
- { pv: 5, name: "meneur d'homme" },
- { pv: 5, name: "noblesse" },
- { pv: 5, name: "bouclier" },
- { pv: 5, name: "combat" },
- { pv: 5, name: "maître d'armes" },
- { pv: 5, name: "résistance" },
- { pv: 5, name: "soldat" },
- { pv: 3, name: "mage" },
- { pv: 3, name: "air" },
- { pv: 3, name: "divination" },
- { pv: 3, name: "envoûteur" },
- { pv: 3, name: "illusions" },
- { pv: 3, name: "invocation" },
- { pv: 3, name: "artefacts" },
- { pv: 3, name: "élixirs" },
- { pv: 3, name: "métal" },
- { pv: 3, name: "golem" },
- { pv: 3, name: "runes" },
- { pv: 3, name: "arcanes exhumés" },
- { pv: 3, name: "arcanes oubliés" },
- { pv: 3, name: "arcanes perdus" },
- { pv: 3, name: "magie des arcanes" },
- { pv: 3, name: "magie destructrice" },
- { pv: 3, name: "magie élémentaire" },
- { pv: 3, name: "magie protectrice" },
- { pv: 3, name: "magie universelle" },
- { pv: 3, name: "démon" },
- { pv: 3, name: "mort" },
- { pv: 3, name: "outre-tombe" },
- { pv: 3, name: "sang" },
- { pv: 3, name: "sombre magie" },
- { pv: 3, name: "arcanes obscurs" },
- { pv: 4, name: "animaux" },
- { pv: 4, name: "fauve" },
- { pv: 4, name: "nature" },
- { pv: 4, name: "protecteur" },
- { pv: 4, name: "végétaux" },
- { pv: 4, name: "énergie vitale" },
- { pv: 4, name: "maîtrise" },
- { pv: 4, name: "médiation" },
- { pv: 4, name: "poing" },
- { pv: 4, name: "vent" },
- { pv: 4, name: "foi" },
- { pv: 4, name: "guerre sainte" },
- { pv: 4, name: "prière" },
- { pv: 4, name: "soins" },
- { pv: 4, name: "spiritualité" },
- { pv: 4, name: "corruption" },
+ { pv: 4, name: "artilleur", profile: "arquebusier",
+ abilities: [ "mecanismes", "arme a repetition", "tir de barrage", "canon double", "couleuvrine" ]
+ },
+ { pv: 4, name: "explosifs", profile: "arquebusier",
+ abilities: [ "tir de grenaille", "demolition", "poudre puissante", "piege explosif", "boulet explosif" ]
+ },
+ { pv: 4, name: "mercenaire", profile: "arquebusier",
+ abilities: [ "pilier de bar", "mort ou vif", "combattant aguerri", "constitution heroique", "combat de masse" ]
+ },
+ { pv: 4, name: "pistolero", profile: "arquebusier",
+ abilities: [ "plus vite que son ombre", "ajuster le tir", "tir double", "agilite heroique", "as de la gachette" ]
+ },
+ { pv: 4, name: "precision", profile: "arquebusier",
+ abilities: [ "joli coup", "defaut dans la cuirasse", "tir precis", "tireur d'elite", "tir fatal" ]
+ },
+ { pv: 4, name: "escrime", profile: "barde",
+ abilities: [ "precision", "feinte", "intelligence du combat", "attaque flamboyante", "botte mortelle" ]
+ },
+ { pv: 4, name: "musicien", profile: "barde",
+ abilities: [ "chant des heros", "chant de reconfort", "attaque sonore", "zone de silence", "danse irresistible" ]
+ },
+ { pv: 4, name: "saltimbanque", profile: "barde",
+ abilities: [ "acrobate", "grace feline", "lanceur de couteau", "liberte d'action", "esquive acrobatique" ]
+ },
+ { pv: 4, name: "seduction", profile: "barde",
+ abilities: [ "charmant", "dentelle et rapiere", "baratineur de genie", "charisme heroique", "suggestion" ]
+ },
+ { pv: 4, name: "vagabond", profile: "barde",
+ abilities: [ "rumeurs et legendes", "eclectique", "attirail", "comprehension des langues", "deguisement" ]
+ },
+ { pv: 4, name: "archer", profile: "rodeur",
+ abilities: [ "archer emerite", "tir chirurgical", "dans le mille", "tir rapide", "fleche de mort" ]
+ },
+ { pv: 4, name: "compagnon animal", profile: "rodeur",
+ abilities: [ "le loup", "travail d'equipe", "lien empathique", "loup alpha", "tactique de meute" ]
+ },
+ { pv: 4, name: "survie", profile: "rodeur",
+ abilities: [ "survie", "nature nourriciere", "grand pas", "constitution heroique", "increvable" ]
+ },
+ { pv: 4, name: "traqueur", profile: "rodeur",
+ abilities: [ "eclaireur", "attaque eclair", "chasseur emerite", "perception heroique", "repli" ]
+ },
+ { pv: 4, name: "combat a deux armes", profile: "rodeur",
+ abilities: [ "attaque a suivre", "parade croisee", "droite - gauche", "combattant heroique", "double peine" ]
+ },
+ { pv: 4, name: "assassin", profile: "voleur",
+ abilities: [ "discretion", "attaque sournoise", "attaque par surprise", "disparition", "ouverture mortelle" ]
+ },
+ { pv: 4, name: "aventurier", profile: "voleur",
+ abilities: [ "baratin", "provocation", "souplesse du felin", "charisme heroique", "attaque paralysante" ]
+ },
+ { pv: 4, name: "deplacement", profile: "voleur",
+ abilities: [ "agile", "reflexes felins", "acrobaties", "agilite heroique", "esquive de la magie" ]
+ },
+ { pv: 4, name: "roublard", profile: "voleur",
+ abilities: [ "doigts agiles", "aux aguets", "feindre la mort", "expert en criminalite", "maitre du poison" ]
+ },
+ { pv: 4, name: "spadassin", profile: "voleur",
+ abilities: [ "attaque en finesse", "esquive fatale", "frappe chirurgicale", "ambidextrie", "botte secrete" ]
+ },
+ { pv: 5, name: "brute", profile: "barbare",
+ abilities: [ "argument de taille", "tour de force", "attaque brutale", "force heroique", "briseur d'os" ]
+ },
+ { pv: 5, name: "pagne", profile: "barbare",
+ abilities: [ "vigueur", "peau de pierre", "tatouages", "constitution heroique", "peau d'acier" ]
+ },
+ { pv: 5, name: "pourfendeur", profile: "barbare",
+ abilities: [ "reflexes eclair", "charge", "enchainement", "dechainement d'acier", "attaque tourbillon" ]
+ },
+ { pv: 5, name: "primitif", profile: "barbare",
+ abilities: [ "proche de la nature", "armure de vent", "vigilance", "resistance a la magie", "vitalite debordante" ]
+ },
+ { pv: 5, name: "rage", profile: "barbare",
+ abilities: [ "cri de guerre", "defier la mort", "rage du berserk", "meme pas mal", "furie du berserk" ]
+ },
+ { pv: 5, name: "cavalier", profile: "chevalier",
+ abilities: [ "fidele monture", "cavalier emerite", "charge", "monture magique", "monture fantastique" ]
+ },
+ { pv: 5, name: "guerre", profile: "chevalier",
+ abilities: [ "armure sur mesure", "encaisser un coup", "frappe du justicier", "force heroique", "mon armure est une arme" ]
+ },
+ { pv: 5, name: "preux", profile: "chevalier",
+ abilities: [ "ignorer la douleur", "piqures d'insectes", "laissez-le moi", "charisme heroique", "seul contre tous" ]
+ },
+ { pv: 5, name: "meneur d'homme", profile: "chevalier",
+ abilities: [ "sans peur", "intercepter", "exemplaire", "charge fantastique", "ordre de bataille" ]
+ },
+ { pv: 5, name: "noblesse", profile: "chevalier",
+ abilities: [ "eduquer", "ecuyer", "autorite naturelle", "massacrer la pietaille", "formation d'elite" ]
+ },
+ { pv: 5, name: "bouclier", profile: "guerrier",
+ abilities: [ "proteger un allie", "parer un coup", "defense au bouclier", "absorber un sort", "renvoi de sort" ]
+ },
+ { pv: 5, name: "combat", profile: "guerrier",
+ abilities: [ "vivacite", "manoeuvre", "attaque puissante", "double attaque", "attaque circulaire" ]
+ },
+ { pv: 5, name: "maitre d'armes", profile: "guerrier",
+ abilities: [ "arme de predilection", "science du critique", "specialisation", "attaque parfaite", "riposte" ]
+ },
+ { pv: 5, name: "resistance", profile: "guerrier",
+ abilities: [ "robustesse", "resilient", "armure lourde", "constitution heroique", "dur a cuire" ]
+ },
+ { pv: 5, name: "soldat", profile: "guerrier",
+ abilities: [ "teigneux", "prouesse", "piqure de rappel", "force heroique", "rempart" ]
+ },
+ { pv: 3, name: "mage", profile: "ensorceleur,forgesort,magicien,sorcier",
+ abilities: [ "occultisme", "maitrise de la magie", "tour de magie", "esprit superieur", "tempete de mana" ]
+ },
+ { pv: 3, name: "air", profile: "ensorceleur",
+ abilities: [ "murmures dans le vent", "sous tensiion", "telekinesie", "foudre", "forme etheree" ]
+ },
+ { pv: 3, name: "divination", profile: "ensorceleur",
+ abilities: [ "divination", "detection de l'invisible", "clairvoyance", "perception heroique", "prescience" ]
+ },
+ { pv: 3, name: "envouteur", profile: "ensorceleur",
+ abilities: [ "injonction", "sommeil", "confusion", "amitie", "domination" ]
+ },
+ { pv: 3, name: "illusions", profile: "ensorceleur",
+ abilities: [ "mirage", "image decalee", "sort illusoire", "imitation", "execution mentale" ]
+ },
+ { pv: 3, name: "invocation", profile: "ensorceleur",
+ abilities: [ "choc", "serviteur invisible", "arme de mana", "porte dimensionnelle", "mur de mana" ]
+ },
+ { pv: 3, name: "artefacts", profile: "forgesort",
+ abilities: [ "baton de mage", "ouverture - fermeture", "sac sans fond", "frappe des arcanes", "artefact etrange" ]
+ },
+ { pv: 3, name: "elixirs", profile: "forgesort",
+ abilities: [ "fortifiant", "feu gregeois", "elixir de guerison", "elixirs mineurs", "elixirs majeurs" ]
+ },
+ { pv: 3, name: "metal", profile: "forgesort",
+ abilities: [ "morsure de la forge", "metal brulant", "magnetisme", "metal hurlant", "endurant" ]
+ },
+ { pv: 3, name: "golem", profile: "forgesort",
+ abilities: [ "grosse tete", "golem", "protecteur", "statuette", "golem superieur" ]
+ },
+ { pv: 3, name: "runes", profile: "forgesort",
+ abilities: [ "runes de defense", "rune de puissance", "rune de protection", "rune d'energie", "rune de garde" ]
+ },
+ { pv: 3, name: "arcanes exhumes", profile: "ensorceleur,forgesort,magicien,sorcier",
+ abilities: [ "message magique", "toiles", "sphere de tenebres", "chien de garde", "nuage empoisonne" ]
+ },
+ { pv: 3, name: "arcanes oublies", profile: "ensorceleur,forgesort,magicien,sorcier",
+ abilities: [ "ventriloquie", "bouclier de mana", "corde magique", "retrecissement", "zone de magie nulle" ]
+ },
+ { pv: 3, name: "arcanes perdus", profile: "ensorceleur,forgesort,magicien,sorcier",
+ abilities: [ "disque de transport", "fuite rapide", "agrandissement", "inversion de la gravite", "metamorphose" ]
+ },
+ { pv: 3, name: "magie des arcanes", profile: "magicien",
+ abilities: [ "projectile de mana", "levitation", "forme gazeuse", "acceleration", "desintegration" ]
+ },
+ { pv: 3, name: "magie destructrice", profile: "magicien",
+ abilities: [ "arc de feu", "saper les forces", "fleche de feu", "explosion de feu", "appel de la foudre" ]
+ },
+ { pv: 3, name: "magie elementaire", profile: "magicien",
+ abilities: [ "asphyxie", "maitrise des elements", "arme elementaire", "respiration aquatique", "armure de pierre" ]
+ },
+ { pv: 3, name: "magie protectrice", profile: "magicien",
+ abilities: [ "arumre de mana", "chute ralentie", "dephasage", "cercle de protection", "interruption du temps" ]
+ },
+ { pv: 3, name: "magie universelle", profile: "magicien",
+ abilities: [ "lumiere", "familier", "invisibilite", "vol", "teleportation" ]
+ },
+ { pv: 3, name: "demon", profile: "sorcier",
+ abilities: [ "malediction", "beaute de la succube", "pacte demoniaque", "aspect du demon", "invocation d'un demon" ]
+ },
+ { pv: 3, name: "mort", profile: "sorcier",
+ abilities: [ "siphon des ames", "masque mortuaire", "baiser du vampire", "peur", "briser les coeurs" ]
+ },
+ { pv: 3, name: "outre-tombe", profile: "sorcier",
+ abilities: [ "un pied dans la tombe", "armure d'os", "animation des morts", "ensevelissement", "armee des morts" ]
+ },
+ { pv: 3, name: "sang", profile: "sorcier",
+ abilities: [ "saignements", "sang mordant", "exsangue", "rituel de sang", "lien de sang" ]
+ },
+ { pv: 3, name: "sombre magie", profile: "sorcier",
+ abilities: [ "tenebres", "reptation", "strangulation", "manteau d'ombre", "pacte tenebreux" ]
+ },
+ { pv: 3, name: "arcanes obscurs", profile: "sorcier",
+ abilities: [ "contact vampirique", "tentation du demon", "epidemie", "aura maudite", "extinction de masse" ]
+ },
+ { pv: 4, name: "animaux", profile: "druide",
+ abilities: [ "langage des animaux", "petit compagnon", "nuee d'insectes", "masque du predateur", "forme animale" ]
+ },
+ { pv: 4, name: "fauve", profile: "druide",
+ abilities: [ "vitesse du felin", "panthere", "attaque bondissante", "grand felin", "les sept vies du chat" ]
+ },
+ { pv: 4, name: "nature", profile: "druide",
+ abilities: [ "maitre de la survie", "terrains difficiles", "baton de druide", "constitution heroique", "resistant" ]
+ },
+ { pv: 4, name: "protecteur", profile: "druide",
+ abilities: [ "baies magiques", "foret vivante", "regeneration", "perception heroique", "forme d'arbre" ]
+ },
+ { pv: 4, name: "vegetaux", profile: "druide",
+ abilities: [ "peau d'ecorce", "prison vegetale", "fleche vivante", "animation d'un arbre", "porte vegetale" ]
+ },
+ { pv: 4, name: "energie vitale", profile: "moine",
+ abilities: [ "mains d'energie", "projection du ki", "invulnerable", "pression mortelle", "ascetisme" ]
+ },
+ { pv: 4, name: "maitrise", profile: "moine",
+ abilities: [ "agilite du singe", "griffes du tigre", "morsure du serpent", "fureur du dragon", "moment de perfection" ]
+ },
+ { pv: 4, name: "meditation", profile: "moine",
+ abilities: [ "pacifisme", "transe de guerison", "matrise du ki", "volonte heroique", "projection mentale" ]
+ },
+ { pv: 4, name: "poing", profile: "moine",
+ abilities: [ "poings de fer", "peau de fer", "parade de projectiles", "deluge de coups", "puissance du ki" ]
+ },
+ { pv: 4, name: "vent", profile: "moine",
+ abilities: [ "pas du vent", "course du vent", "course des airs", "agilite heroique", "passe-muraille" ]
+ },
+ { pv: 4, name: "foi", profile: "pretre",
+ abilities: [ "predicateur", "miracle mineur", "arme de lumiere", "ailes celestes", "foudres divines" ]
+ },
+ { pv: 4, name: "guerre sainte", profile: "pretre",
+ abilities: [ "arme benie", "bouclier de la foi", "chatiment divin", "marteau de la foi", "mot de pouvoir" ]
+ },
+ { pv: 4, name: "priere", profile: "pretre",
+ abilities: [ "benediction", "sanctuaire", "destruction du mal", "volonte heroique", "intervention divine" ]
+ },
+ { pv: 4, name: "soins", profile: "pretre",
+ abilities: [ "recuperation mineure", "vigueur divine", "recuperation majeure", "phenix", "retablissement" ]
+ },
+ { pv: 4, name: "spiritualite", profile: "pretre",
+ abilities: [ "vetements sacres", "augure", "delivrance", "charisme heroique", "marche des plans" ]
+ },
+ { pv: 4, name: "corruption", profile: "pretre",
+ abilities: [ "blessure mineure", "poison", "blessure majeure", "vengeance", "epidemie" ]
+ },
],
}
@@ -6303,36 +7442,37 @@ function strval(value, onError = "") {
/**
* Capitalize string
-* @param {string} str - string to capitalize
+* @param {string} s - string to capitalize
+* @param {boolean} [forceLower=true] - force lower to end of string
* @returns capitalized string
*/
-function capitalize(str) {
- if (str && str !== "")
- return str.charAt(0).toUpperCase() + str.substring(1);
+function capitalize(s, forceLower = true) {
+ if (s && s !== "")
+ return s.charAt(0).toUpperCase() + (forceLower ? s.substring(1).toLowerCase() : s.substring(1));
}
/**
- * Return ability short name
- * @param {string} ability - ability name
+ * Return stat short name
+ * @param {string} stat - stat name
* @returns {string}
*/
-function shorten(ability) {
- return ability.substring(0, 3);
+function shorten(stat) {
+ return stat.substring(0, 3);
+}
+
+function upperShort(stat) {
+ return shorten(stat).toUpperCase();
}
/**
- * @typedef clogOptions
- * @property {string} title - title of logged data
- * @property {boolean} debug - display only if debug_mode attribute enabled
- * @property {string} color - color name for log line
- * @property {string} style - CSS style for log line
- * @property {string} headerStyle - CSS style for log title
- */
-
-/**
-* Log to JS console with color & style
-* @param {any} data - data to log
-* @param {clogOptions} options - console display options
+ * Log to JS console with color & style
+ * @param {any} data - data to log
+ * @param {object} options - console display options
+ * @param {string} options.title - title of logged data
+ * @param {boolean} options.debug - display only if debug_mode attribute enabled
+ * @param {string} options.color - color name for log line
+ * @param {string} options.style - CSS style for log line
+ * @param {string} options.headerStyle - CSS style for log title
*/
function clog(data, options = {}) {
let {
@@ -6348,7 +7488,7 @@ function clog(data, options = {}) {
return;
title = strval(title);
- if (title !== "") title = ` Sheet-Worker : ${title} `;
+ if (title !== "") title = ` Sheet-Worker > ${title} `;
const titleStyle = `color:${textColor}; background-color:${backColor}; ${headerStyle} text-decoration:underline;`;
const textStyle = `color:${textColor}; background-color:${backColor}; ${style}`;
@@ -6370,12 +7510,22 @@ function clog(data, options = {}) {
}
}
+/**
+ * Return title for event log
+ * @param {object} eventInfo - Roll20 eventInfo object
+ * @returns {string}
+ */
+function logTitle(eventInfo) {
+ return `CHANGE:${ strval(eventInfo.triggerName) }`;
+}
+
/**
* Log and set attributes data
* @param {object} data - attributes name/value pairs
+ * @param {string} [title=""] - title for the JS console log
*/
-function logAndSet(data) {
- clog(data);
+function logAndSet(data, title = "") {
+ clog(data, { title, textColor: "white", backColor: "brown" });
setAttrs(data);
}
@@ -6410,6 +7560,53 @@ function parseNameValue(data, delim = ":") {
return { name, value };
}
+/**
+ * Clean-up text
+ * @param {string} t - text to cleanup (multiple spaces & accents)
+ * @param {boolean} [tags=true] - also remove COF2 tags (action types & spells)
+ * @returns {string}
+ */
+function cleanText(t, tags = true) {
+ t = strval(t).toLowerCase();
+ t = t.replace(/\s\s+/g, " ");
+ t = t.replace(/[àâä]/g, "a");
+ t = t.replace(/[éèêë]/g, "e");
+ t = t.replace(/[îï]/g, "i");
+ t = t.replace(/[ôö]/g, "o");
+ t = t.replace(/[ùûü]/g, "u");
+ t = t.replace(/ÿ/g, "y");
+ t = t.replace(/ç/g, "c");
+ t = t.replace(/œ/g, "oe");
+ t = t.replace(/æ/g, "ae");
+ t = t.replace(/’/g, "'");
+ if (tags) {
+ t = t.replace(/\(G\)/, "");
+ t = t.replace(/\(M\)/, "");
+ t = t.replace(/\(A\)/, "");
+ t = t.replace(/\(L\)/, "");
+ t = t.replace(/\*/, "");
+ }
+ return t.trim();
+}
+
+/**
+ * Return if a property exists
+ * @param {string} data - list of properties separated by \n
+ * @param {string} propName - property name to search
+ * @returns {boolean}
+ */
+function hasProp(data, propName) {
+ const props = data.split("\n").reduce((props, prop) => {
+ if (prop.trim() !== "") {
+ const { name } = parseNameValue(prop);
+ if (name)
+ props.push(name);
+ }
+ return props;
+ }, []);
+ return props.includes(propName);
+}
+
/**
* Get a property value
* @param {string} data - list of properties separated by \n
@@ -6468,6 +7665,21 @@ function eventList(eventName, attributeList, joinChar = "") {
return joinChar !== "" ? result.join(joinChar) : result;
}
+/**
+ * Return repeating attribute name
+ * @param {string} section - section name
+ * @param {string} name - attribute name
+ * @param {string} [id=""] - row Id
+ * @returns {string}
+ */
+function repeatingAttr(section, name, id = "") {
+ let attribute = `repeating_${section}`;
+ if (id !== "")
+ attribute += "_" + id;
+ attribute += "_" + name;
+ return attribute;
+}
+
/**
* Remove all rows for a repeating section
* @param {string} sectionName repeating section to clear
@@ -6495,18 +7707,21 @@ function getSectionIDsOrdered(sectionName, callback) {
});
};
-/** @typedef sendChatMsgOptions
+/**
+ * @typedef MsgOptions
* @property {string} title - Title of the message
* @property {string} template - Name of roll template
- * @property {string} whisper - Target for whispered message
+ * @property {string} whisper - Target for whispered message
+ * @property {number} rollTN - Target number for special roll effect
*/
/**
* Send a message to chat
* @param {string|string[]} msg - Message to send
- * @param {sendChatMsgOptions} options - Options for the message
+ * @param {MsgOptions} options - Options for the message
*/
-function buildChatMsg(msg, options = SWData.RT_OPTIONS) {
+function buildChatMsg(msg, options) {
+ options = { ...SWData.RT_OPTIONS, ...options };
let { title = "", template = "default", whisper = "@{togm}" } = options;
if (whisper.charAt(0) !== "@" && whisper !== "") whisper = "/w \"" + whisper + "\" ";
let chatMsg;
@@ -6525,11 +7740,15 @@ function buildChatMsg(msg, options = SWData.RT_OPTIONS) {
/**
* Send a message to chat
* @param {string|string[]} msg - Message to send
- * @param {sendChatMsgOptions} options - Options for the message
+ * @param {MsgOptions} options - Options for the message
*/
- function sendChatMsg(msg, options = SWData.RT_OPTIONS) {
+function sendChatMsg(msg, options) {
+ options = { ...SWData.RT_OPTIONS, ...options };
const chatCmd = buildChatMsg(msg, options);
startRoll(chatCmd, roll => {
+
+ const updates = new Map();
+
let save = 1;
if (roll.results.save)
save = roll.results.save.result;
@@ -6539,8 +7758,38 @@ function buildChatMsg(msg, options = SWData.RT_OPTIONS) {
if (roll.results.broll)
last_rolls += "|" + roll.results.broll.result;
if (last_rolls !== "" && save)
- setAttrs({ last_rolls });
- finishRoll(roll.rollId);
+ updates.set("last_rolls", last_rolls);
+
+ // check TN
+ const computed = {};
+ const rollTN = intval(options.rollTN);
+ if (rollTN > 0) {
+ const rollD20 = intval(roll.results.roll.dice[0]);
+ if (rollD20 >= rollTN)
+ computed.roll = 1;
+ if (!computed.roll) {
+ const brollD20 = intval(roll.results.broll.dice[0]);
+ if (brollD20 >= rollTN)
+ computed.broll = 1;
+ }
+ }
+
+ // check usure
+ if (options.usure) {
+ const attackId = strval(options.usure.attackId);
+ const name = strval(options.usure.name);
+ const usure = intval(options.usure.value);
+ if (usure > 0 && attackId !== "") {
+ const rollD20 = intval(roll.results.roll.dice[0]);
+ if (rollD20 === 1 && roll.results.usure)
+ updates.set("usure_check", JSON.stringify({ attack: attackId, name, usure, roll: roll.results.usure.result }));
+ }
+ }
+
+ if (updates.size > 0)
+ setAttrs(Object.fromEntries(updates));
+
+ finishRoll(roll.rollId, computed);
});
}
@@ -6570,9 +7819,10 @@ function askValue(prompt, callback) {
const askValue = `!{{ask=[[?{${prompt}}]]}}`;
startRoll(askValue, question => {
const query = question.results.ask.result;
- if(query && callback) {
- callback(query);
- }
+ if(query && callback) {
+ callback(query);
+ }
+ finishRoll(question.rollId);
});
}
@@ -6593,7 +7843,7 @@ function ask(askParams, callback) {
if(query && callback) {
callback(query, choices);
}
- //finishRoll(question.rollId);
+ finishRoll(question.rollId);
});
};
@@ -6659,26 +7909,134 @@ function hideArea(div) {
/**
* Send an /fx command to Roll20 chat
- * @param {string} fxValue - FX parameter "name [target] [count]"
+ * @param {string} fxValue - FX parameter "name {target} {count}[,name {target} {count}]"
* @returns {void}
*/
function playFX(fxValue) {
if (fxValue === "")
return;
- const [ fxName, ...params ] = (fxValue.replace(/\s\s+/g, ' ') + " ").split(" ");
- let count = 1;
- let fxCmd = `/fx ${fxName} @{selected|token_id}`;
- if (params[0] !== "")
- count = intval(params[0]);
-
- let fxChat = fxCmd;
- while (--count > 0) {
- fxChat += "\n" + fxCmd;
- }
- startRoll(fxChat, roll => {
- finishRoll(roll.rollId);
+ let fxChat = "";
+ fxValue.split(",").forEach((fx, ix) => {
+ if (ix > 0)
+ fxChat += "\n";
+ const [ fxName, ...params ] = (fx.replace(/\s\s+/g, " ") + " ").split(" ");
+ let count = 1;
+ const fxCmd = `/fx ${fxName} @{selected|token_id}`;
+ if (params[0] !== "")
+ count = intval(params[0]);
+
+ fxChat += fxCmd;
+ while (--count > 0) {
+ fxChat += "\n" + fxCmd;
+ }
});
+
+ if (fxChat !== "") {
+ startRoll(fxChat, roll => {
+ finishRoll(roll.rollId);
+ });
+ }
+}
+
+/**
+ * Return paths names
+ * @param {string} noms_voies - noms_voies attribute value
+ * @returns {string[]}
+ */
+function getPathNames(noms_voies) {
+ return strval(noms_voies).split("|").map(n => strval(n).trim());
+}
+
+/**
+ * Return paths ranks
+ * @param {string} rangs_voies - rangs_voies attribute value
+ * @returns {number[]}
+ */
+function getPathRanks(rangs_voies) {
+ return strval(rangs_voies).split(",").map(r => intval(r));
+}
+
+/**
+ * Return list of owned abilities
+ * @param {object} values - getAttrs() object
+ * @returns {string[]}
+ */
+function ownedAbilities(values) {
+ const ranks = getPathRanks(strval(values.rangs_voies));
+ let abilities = [];
+ getPathNames(strval(values.noms_voies)).forEach((pathName, ix) => {
+ if (ranks[ix] === 0)
+ return;
+ pathName = cleanText(pathName)
+ const path = SWData.PVPATH.find(path => path.name === pathName);
+ if (!path)
+ return;
+ const owned = path.abilities.slice(0, ranks[ix]);
+ abilities = [ ...abilities, ...owned ];
+ });
+ return abilities;
+}
+
+/**
+ * Return owned abilities tree
+ * @param {object} values - getAttrs() object
+ * @returns {{ name: string, profile: string, abilities: string[]}[]} ability tree
+ */
+function ownedAbilityTree(values) {
+ const ranks = getPathRanks(strval(values.rangs_voies));
+ let abilities = [];
+ getPathNames(strval(values.noms_voies)).forEach((pathName, ix) => {
+ if (ranks[ix] === 0)
+ return;
+ pathName = cleanText(pathName)
+ const path = SWData.PVPATH.find(path => path.name === pathName);
+ if (!path)
+ return;
+ const owned = path.abilities.slice(0, ranks[ix]);
+ abilities.push({ name: pathName, profile: path.profile, abilities: owned });
+ });
+ return abilities;
+}
+
+/**
+ * Check if ability name is owned
+ * @param {object} search - search parameters
+ * @param {string} search.name - ability name to search for
+ * @param {string} [search.inPath=""] - name of path to search ability in
+ * @param {object} values - getAttrs() object
+ * @returns {boolean}
+ */
+function hasAbility(search, values) {
+ const { name, inPath = "" } = search;
+ let owned;
+ if (inPath !== "") {
+ owned = ownedAbilityTree(values);
+ const path = owned.find(path => path.name === cleanText(inPath));
+ if (!path)
+ return false;
+ owned = path.abilities;
+ } else {
+ owned = ownedAbilities(values);
+ }
+ return owned.includes(cleanText(name));
+}
+
+/**
+ * Count the number of paths with attained rank
+ * @param {object} search - search option
+ * @param {string} search.profile - profile name to search
+ * @param {number} [search.minRank=5] - attained rank to search
+ * @param {object} values - getAtts() object values
+ * @returns {number}
+ */
+function getPathsWithRank(search, values) {
+ const { profile, minRank = 5 } = search;
+ const owned = ownedAbilityTree(values);
+ const withRank = owned
+ .filter(path => path.profile === cleanText(profile, false))
+ .filter(path => path.abilities.length >= minRank);
+ return withRank.length;
}
// ===============
@@ -6709,7 +8067,7 @@ on("change:sheet_type", function() {
*/
[
"pc_main", "pc_abilities", "pc_gears", "pc_config", "pc_script", "pc_version",
- "npc_main", "npc_config", "npc_script"
+ "npc_main", "npc_gears", "npc_config", "npc_script"
].forEach(button => {
on(`clicked:${button}-btn`, function() {
const [ type, value ] = button.split("_");
@@ -6732,6 +8090,12 @@ on("change:sheet_type", function() {
});
});
+[ "attacks", "abilities" ].forEach(button => {
+ on(`clicked:npc_${button}-btn`, function() {
+ setAttrs({ npc_subtab1: button });
+ });
+});
+
/**
* Populate the profiles datalist
* @param {string} family - name of profiles family
@@ -6772,50 +8136,47 @@ function getProfileData(profile, selectFamily = "") {
* - set DR type
* - set PC & DR base
*/
-on("change:famille", function() {
- const section = "repeating_buffs";
- getSectionIDs(section.split("_")[1], rowIds => {
- const allBuffs = [];
- rowIds.forEach(rowId => {
- allBuffs.push(`${section}_${rowId}_buff-nom`);
- });
- getAttrs([ "profil", "famille", ...allBuffs ], function(values) {
- const family = strval(values.famille);
- const profile = strval(values.profil);
- setProfilesList(family, profile);
+on("change:famille", function(eventInfo) {
+getAttrs([ "profil", "famille" ], function(values) {
+ const family = strval(values.famille);
+ const profile = strval(values.profil);
+ setProfilesList(family, profile);
- const updates = new Map();
+ const updates = new Map();
- let dRecup = 8;
- let pcBase = 2;
- let drBase = 2;
- switch (family) {
- case "Aventuriers":
- pcBase = 3;
- break;
- case "Combattants":
- dRecup = 10;
- break;
- case "Mages":
- dRecup = 6;
- break;
- case "Mystiques":
- drBase = 3;
- break;
- }
- updates.set("drecup", dRecup);
- updates.set("pc_base", pcBase);
- updates.set("dr_base", drBase);
+ let dRecup = 8;
+ let pcBase = 2;
+ let drBase = 2;
+ let voie1pv = 4;
+ switch (family) {
+ case "Aventuriers":
+ pcBase = 3;
+ break;
+ case "Combattants":
+ dRecup = 10;
+ voie1pv = 5;
+ break;
+ case "Mages":
+ dRecup = 6;
+ voie1pv = 3;
+ break;
+ case "Mystiques":
+ drBase = 3;
+ break;
+ }
+ updates.set("drecup", dRecup);
+ updates.set("pc_base", pcBase);
+ updates.set("dr_base", drBase);
+ updates.set("voie1pv", voie1pv);
- setAttrs(Object.fromEntries(updates));
- });
+ logAndSet(Object.fromEntries(updates), logTitle(eventInfo));
});
});
/**
* On change of profile, check skill proficiencies
*/
-on("change:profil", function() {
+on("change:profil", function(eventInfo) {
getAttrs([ "profil", "famille", "use_skills" ], function(values) {
const useSkills = boolval(values.use_skills);
if (!useSkills)
@@ -6838,7 +8199,7 @@ on("change:profil", function() {
});
if (proficiencies.size > 0)
- setAttrs(Object.fromEntries(proficiencies));
+ logAndSet(Object.fromEntries(proficiencies), logTitle(eventInfo));
});
});
@@ -6888,7 +8249,7 @@ function updateDEvol(level, attributes) {
/**
* On level change
*/
-on("change:niveau", function() {
+on("change:niveau", function(eventInfo) {
getAttrs([ "niveau", "cfg_use_epic", "cfg_use_capped6" ], function(values) {
const cappedAt6 = boolval(values.cfg_use_capped6);
// base progression
@@ -6908,7 +8269,7 @@ on("change:niveau", function() {
updateAttrs.set("atkmag_base", epicBase);
updateAttrs.set("def_base", epicBase);
}
- setAttrs(Object.fromEntries(updateAttrs))
+ logAndSet(Object.fromEntries(updateAttrs), logTitle(eventInfo));
});
});
@@ -6932,18 +8293,24 @@ on("clicked:alias-toggle", function() {
})
/**
- * On ability components change
+ * On stats components change
*/
-SWData.PC.ABILITIES.forEach(ability => {
- const short = shorten(ability);
- const attrs = [
- short,
- `${short}_buff`
+SWData.PC.STATS.forEach(stat => {
+ const short = shorten(stat);
+ const attrs = [
+ `${short}_base`,
+ `${short}_buff`,
+ `t${short}_buff`,
];
- on(eventList("change", attrs, " "), function () {
+ on(eventList("change", attrs, " "), function (eventInfo) {
getAttrs(attrs, function(values) {
- const score = addValues(values);
- setAttrs({ [`${short}_test`]: score });
+ const base = intval(values[`${short}_base`]);
+ const buff = intval(values[`${short}_buff`]);
+ const tbuff = intval(values[`t${short}_buff`]);
+ logAndSet({
+ [short]: base + buff,
+ [`${short}_test`]: base + buff + tbuff
+ }, logTitle(eventInfo));
});
});
});
@@ -6951,12 +8318,13 @@ SWData.PC.ABILITIES.forEach(ability => {
/**
* Update the init attribute
*/
-function updateInit() {
+function updateInit(eventInfo) {
getAttrs([ ...SWData.PC.COMBAT.init ], function(values) {
- values.agi *= intval(values.cfg_init_agi);
+ const car = Object.keys(values).find(k => k.startsWith("agi"));
+ values[car] *= intval(values.cfg_init_agi);
delete values.cfg_init_agi;
const init = addValues(values);
- setAttrs({ init });
+ logAndSet({ init }, logTitle(eventInfo));
});
}
@@ -6976,10 +8344,10 @@ SWData.PC.COMBAT.attacks.forEach(attk => {
`${attack}_cond`,
`${attack}_buff`
];
- on(eventList("change", changed, " "), function() {
+ on(eventList("change", changed, " "), function(eventInfo) {
getAttrs(changed, function(values) {
const score = addValues(values);
- setAttrs({ [attack]: score });
+ logAndSet({ [attack]: score }, logTitle(eventInfo));
});
});
});
@@ -6987,13 +8355,13 @@ SWData.PC.COMBAT.attacks.forEach(attk => {
/**
* Compute max DR
*/
-function updateDRMax() {
+function updateDRMax(eventInfo) {
getAttrs([ "drmax_force", "dr_base", "con", "dr_buff" ], function(values) {
if (boolval(values.drmax_force))
return;
const dr_max = intval(values.dr_base) + intval(values.con) + intval(values.dr_buff);
- setAttrs({ dr_max });
+ logAndSet({ dr_max }, logTitle(eventInfo));
});
}
@@ -7005,7 +8373,7 @@ on("change:dr_base change:con change:dr_buff", updateDRMax);
/**
* Compute pv_max
*/
-function updatePVMax() {
+function updatePVMax(eventInfo) {
const owned = [];
const other = [];
seq(9).forEach(path => {
@@ -7016,7 +8384,7 @@ function updatePVMax() {
});
});
getAttrs([ "cfg_pvmax_auto", "pvmax_force", "niveau", "con", "pv_buff", ...owned, ...other ], function(values) {
- if (!boolval(values.cfg_pvmax_auto))
+ if (!boolval(values.cfg_pvmax_auto) && strval(eventInfo.triggerName) !== "RESET")
return;
if (boolval(values.pvmax_force))
return;
@@ -7057,7 +8425,7 @@ function updatePVMax() {
pvMax += intval(values.pv_buff);
- setAttrs({ pv_max: pvMax });
+ logAndSet({ pv_max: pvMax }, logTitle(eventInfo));
});
}
@@ -7078,9 +8446,9 @@ function loadLimit(force) {
/**
* Compute load threshold
*/
-function updateLoadLimit() {
+function updateLoadLimit(eventInfo) {
getAttrs([ "for" ], function(value) {
- setAttrs({ limite_enc: loadLimit(intval(value.for)) });
+ logAndSet({ limite_enc: loadLimit(intval(value.for)) }, logTitle(eventInfo));
});
}
@@ -7092,7 +8460,7 @@ on("change:for", updateLoadLimit);
/**
* Compute max PM
*/
-function updatePMMax() {
+function updatePMMax(eventInfo) {
const owned = [];
const spells = [];
const bRanks = [];
@@ -7103,7 +8471,14 @@ function updatePMMax() {
spells.push(`v${p}r${r}_spell`);
});
});
- getAttrs([ "cfg_use_mana", "pmmax_force", "vol", "pm_buff", ...spells, ...owned, ...bRanks ], function(values) {
+ getAttrs([
+ "cfg_use_mana", "pmmax_force",
+ "vol", "pm_buff",
+ "cha", "rangs_voies", "noms_voies",
+ ...spells,
+ ...owned,
+ ...bRanks
+ ], function(values) {
if (boolval(values.pmmax_force))
return;
@@ -7117,15 +8492,22 @@ function updatePMMax() {
pmGain = 2;
return pm + (intval(values[ability]) * intval(values[spell]) * pmGain);
}, 0);
- const pm_max = intval(values.vol) + spellPM + intval(values.pm_buff);
- setAttrs({ pm_max });
+ let base = intval(values.vol);
+ if (hasAbility({ name: "charisme heroique", inPath: "seduction" }, values)) {
+ const cha = intval(values.cha);
+ if (cha > base)
+ base = cha;
+ }
+ const pm_max = base + spellPM + intval(values.pm_buff);
+ logAndSet({ pm_max }, logTitle(eventInfo));
});
}
/**
* On VOL & PM buff change
+ * On CHA & ranks change
*/
-on("change:vol change:pm_buff", updatePMMax);
+on("change:vol change:pm_buff change:cha change:rangs_voies", updatePMMax);
/**
* On change of an ability spell
@@ -7139,34 +8521,63 @@ seq(9).forEach(p => {
/**
* Compute max PC
*/
-function updatePCMax() {
- getAttrs([ "pcmax_force", "pc_base", "cha", "pc_buff" ], function(values) {
+function updatePCMax(eventInfo) {
+ getAttrs([ "pcmax_force", "pc_base", "cha", "pc_buff", "cfg_pc_var" ], function(values) {
if (boolval(values.pcmax_force))
return;
- const pc_max = addValues(values);
- setAttrs({ pc_max });
+ const doublePC = boolval(values.cfg_pc_var) ? 2 : 1;
+ delete values.cfg_pc_var;
+ const pc_max = addValues(values) * doublePC;
+ logAndSet({ pc_max, double_pc: doublePC === 2 ? " x 2" : "" }, logTitle(eventInfo));
});
}
/**
* On PC components change
*/
-on("change:pc_base change:cha change:pc_buff", updatePCMax);
+on("change:pc_base change:cha change:pc_buff change:cfg_pc_var", updatePCMax);
/**
- * Ability rolls
+ * Return list of attributes for luck roll button
+ * @returns {string[]}
*/
-SWData.PC.ABILITIES.forEach(ability => {
- const short = shorten(ability);
+function luckButtonAttrs() {
+ return [
+ "cfg_luck_btn",
+ "character_name",
+ "couleur_pj",
+ "pc"
+ ];
+}
+
+/**
+ * Return roll template field for luck button
+ * @param {Object} values - getAttrs() object
+ * @returns {string}
+ */
+function addLuckButton(values) {
+ if (!boolval(values.cfg_luck_btn))
+ return "";
+ const charName = strval(values.character_name);
+ const color = strval(values.couleur_pj);
+ const pc = intval(values.pc);
+ return `[Chance](~${charName}|luck_apply-btn${menuButton(color, { size: "1.3em" })}) (Reste ${pc} PC)`;
+}
+
+/**
+ * Stats rolls
+ */
+SWData.PC.STATS.forEach(stat => {
+ const short = shorten(stat);
- // Set the ability die rolls
- on(`change:${short}_sup change:condition_affaibli`, function() {
+ // Set the stat die rolls
+ on(`change:${short}_sup change:condition_affaibli`, function(eventInfo) {
getAttrs([ `${short}_sup`, "condition_affaibli" ], function(values) {
const [ dType ] = Object.keys(values);
const weakened = intval(values.condition_affaibli);
const roll = dieRoll(values[dType], weakened);
- setAttrs({ [`${short}_dsup`]: roll });
+ logAndSet({ [`${short}_dsup`]: roll }, logTitle(eventInfo));
});
});
@@ -7177,7 +8588,7 @@ SWData.PC.ABILITIES.forEach(ability => {
"cfg_bouclier_malus", "bouclier_malus", "bouclier_eqp",
"casque", "casque_eqp",
];
- getAttrs([ `${short}_dsup`, "notif_fx", ...defGearAttrs ], function(values) {
+ getAttrs([ `${short}_dsup`, "notif_fx", ...luckButtonAttrs(), ...defGearAttrs ], function(values) {
// Play FX !
const fxValue = strval(values.notif_fx);
if (fxValue !== "")
@@ -7195,8 +8606,9 @@ SWData.PC.ABILITIES.forEach(ability => {
const carac = `[[@{${roll}}[Dé] + @{${short}_test}[Bonus ${short.toUpperCase()}]${armor_malus}${shield_malus}${helmet_malus} ]] `;
const chatMsg = cof2RollTemplate({
lsub: "Test",
- rsub: capitalize(ability),
- roll: carac
+ rsub: capitalize(stat),
+ roll: carac,
+ chance: addLuckButton(values)
});
sendChatMsg(chatMsg);
});
@@ -7206,14 +8618,21 @@ SWData.PC.ABILITIES.forEach(ability => {
/**
* On initiative button click
+ * Used by PC & NPC sheet
*/
on("clicked:init-btn", function () {
- getAttrs(["casque_eqp", "casque"], function(values) {
- const init = `[[@{init}[Initiative]${ helmetMalus(values) } + @{cfg_init_variable}[Dé(s)] &{tracker} ]]`;
+ getAttrs([ "sheet_type", "cfg_init_mod", "per", "agi", "casque_eqp", "casque" ], function(values) {
+ let initMod = "";
+ if (boolval(values.cfg_init_mod))
+ initMod = ` + [[(@{per}+@{agi})/10]][Distinct]`;
+ let helmet = "";
+ if (strval(values.sheet_type) === SWData.SHEET_TYPE.PC)
+ helmet = helmetMalus(values);
+ const roll = `[[@{init}[Initiative]${helmet} + @{cfg_init_variable}[Dé(s)]${initMod} &{tracker} ]]`;
const chatMsg = cof2RollTemplate({
lsub: "Jet",
rsub: "Initiative",
- roll: init
+ roll
});
sendChatMsg(chatMsg);
});
@@ -7225,15 +8644,22 @@ on("clicked:init-btn", function () {
SWData.PC.COMBAT.attacks.forEach(attk => {
const [ attack, , description ] = attk;
on(`clicked:${attack}-btn`, function () {
- getAttrs([ "cfg_1attack_roll", "condition_affaibli" ], function (values) {
+ getAttrs([
+ "cfg_1attack_roll", "condition_affaibli", "condition_immobilise",
+ ...luckButtonAttrs()
+ ], function (values) {
const useOneRoll = boolval(values.cfg_1attack_roll);
- const roll = (intval(values.condition_affaibli) === 1) ? "2d20kl1" : "1d20";
+ const weakened = intval(values.condition_affaibli);
+ const restrained = intval(values.condition_immobilise);
+ const hasMalusDie = (weakened === 1) || (restrained === 1);
+ const roll = hasMalusDie ? "2d20kl1" : "1d20";
const atkRoll = `[[${roll}cs20cf1[Dé] + @{${attack}}[Bonus] ]]`;
const atkProps = {
lsub: "Attaque",
rsub: description,
roll: atkRoll,
- broll: ""
+ broll: "",
+ chance: addLuckButton(values)
};
if (roll === "1d20" && !useOneRoll)
atkProps.broll = atkRoll;
@@ -7411,23 +8837,23 @@ on("clicked:drecup-btn", function() {
/**
* Update the def attribute
*/
-function updateDef() {
+function updateDef(eventInfo) {
getAttrs(SWData.PC.COMBAT.def, function(values) {
- const capabilities = strval(values.capabilities).split(",");
- delete values.capabilities;
values.armure = intval(values.armure) * intval(values.armure_eqp);
delete values.armure_eqp;
values.bouclier = intval(values.bouclier) * intval(values.bouclier_eqp);
delete values.bouclier_eqp;
let def_car = "AGI";
let def_car_bonus = intval(values.agi);
- if (capabilities.includes("peaudepierre")) {
+ if (hasAbility({ name: "peau de pierre", inPath: "pagne" }, values)) {
const conValue = intval(values.con);
if (conValue > def_car_bonus) {
def_car = "CON";
def_car_bonus = conValue;
}
}
+ delete values.rangs_voies;
+ delete values.noms_voies;
values.agi = 0;
values.con = 0;
def_car_max = intval(values.def_car_max);
@@ -7435,7 +8861,7 @@ function updateDef() {
values.def_car_max = 0;
values.def_car_bonus = def_car_bonus;
def = addValues(values);
- setAttrs({ def, def_car, def_car_bonus });//, false);
+ logAndSet({ def, def_car, def_car_bonus }, logTitle(eventInfo));
});
}
@@ -7490,8 +8916,8 @@ function updateEquiped(attribute) {
* On DEF components change
*/
on(eventList("change", SWData.PC.COMBAT.def, " "), function(eventInfo) {
- updateDef();
- const attribute = eventInfo.triggerName;
+ updateDef(eventInfo);
+ const attribute = strval(eventInfo.triggerName, "");
if (attribute.endsWith("_eqp"))
updateEquiped(attribute);
});
@@ -7504,7 +8930,7 @@ on("change:casque_eqp", function() {
* On Luck apply button click
*/
on("clicked:luck_apply-btn", function() {
- getAttrs([ "pc", "last_rolls" ], function(values) {
+ getAttrs([ "pc", "last_rolls", "cfg_pc_var" ], function(values) {
const lsub = "Utilisation";
const rsub = "Point de Chance";
let pc = intval(values.pc);
@@ -7519,11 +8945,12 @@ on("clicked:luck_apply-btn", function() {
return;
}
+ const luck = boolval(values.cfg_pc_var) ? "1d10" : "10";
pc -= 1;
const [ last_roll, last_broll ] = strval(values.last_rolls).split("|");
- let roll = "[[10" + (last_roll !== "" ? ` + ${last_roll} ` : "" ) + "]]";
+ let roll = "[[" + luck + (last_roll !== "" ? ` + ${last_roll} ` : "" ) + "]]";
if (last_broll)
- roll += " | [[10" + ` + ${last_broll} ` + "]]";
+ roll += " | [[" + luck + ` + ${last_broll} ` + "]]";
const chatMsg = cof2RollTemplate({
lsub,
rsub,
@@ -7540,11 +8967,9 @@ on("clicked:luck_apply-btn", function() {
* On Luck roll button click
*/
on("clicked:luck_roll-btn", function() {
- const lsub = "Jet";
- const rsub = "Chance";
const chatMsg = cof2RollTemplate({
- lsub,
- rsub,
+ lsub: "Jet",
+ rsub: "Chance",
roll: "[[ 1d6![Dé] + @{pc}[Points de Chance restants] ]]",
save: "[[0]]"
});
@@ -7596,7 +9021,7 @@ function getAttackName(attribute) {
});
if (!found)
return "";
- const [ , , name ] = found;
+ const [ name ] = found.slice(2);
return name;
}
@@ -7664,10 +9089,6 @@ SWData.PC.CONDITIONS.forEach(condition => {
const notify = intval(values.notif_conditions);
if (hasCondition === 1 && notify !== 0)
displayCondition(label, effects, description, notify);
-
- const has_conditions = allConditions.some(attr => intval(values[attr]) === 1) ? 1 : 0;
- if (has_conditions !== intval(values.has_conditions))
- setAttrs({ has_conditions });
});
});
});
@@ -7685,17 +9106,17 @@ function matchRegExp(text, pattern) {
/**
* Get the path number from the path name
* @param {string} name - ability qualifier ('rang voie 1', 'rang voie ', 'rang ')
- * @param {string} pathNames - noms_voies attribute value (| delimited)
+ * @param {string[]} names - list of path names
* @returns {number}
*/
-function findPathByName(name, pathNames) {
- const q = name.split(" ").slice(1) || [];
+function findPathByName(name, names) {
+ const q = name.replace(/\s\s+/g, " ").trim().split(" ").slice(1) || [];
if (q.length === 0)
return 0;
- const qualifier = q.slice(1).join(" ");
+ const qualifier = q.slice(q.length - 1).join(" ");
if (qualifier === "")
- return 0
+ return 0;
if (q[0].toLowerCase() === "voie") {
if (Number.isInteger(intval(qualifier)) && intval(qualifier) > 0) {
@@ -7703,37 +9124,33 @@ function findPathByName(name, pathNames) {
}
}
- const pathNumber = pathNames.split("|").findIndex(pathName => pathName.toLowerCase() === qualifier.toLowerCase());
+ const pathNumber = names.findIndex(pathName => pathName.toLowerCase() === qualifier.toLowerCase());
return pathNumber + 1;
}
-/**
- * @typedef insertRollsOptions
- * @property {string} pathNames - comma-separated list of path names
- * @property {boolean} isNPC - processing NPC abilities
- * @property {string} companionOf = name of PC the NPC is a companion of (ends with |)
- */
-
/**
* Substitute expressions with in-line rolls
- * @param {string} text - text to process
- * @param {insertRollsOptions} [options={}] - options
+ * @param {string} text - text to process
+ * @param {object} options - options
+ * @param {string[]} options.pathNames - list of path names
+ * @param {boolean} options.isNPC - processing NPC abilities
+ * @param {string} options.companionOf - name of PC the NPC is a companion of (ends with |)
* @returns {string}
*/
function insertRolls(text, options = {}) {
if (!text)
return "";
- const { pathNames = "", isNPC = false, companionOf = "" } = options;
+ const { pathNames = [], isNPC = false, companionOf = "" } = options;
const devol = `@{${ isNPC ? companionOf : "" }devol}` ;
// Search for " [#d4° + XXX] " or " [#dE + XXX] "
- matchRegExp(text, /([ ]*)\[([1-9])d(4°|E)[ ]*\+[ ]*(AGI|CON|FOR|PER|CHA|INT|VOL|rang voie [1-9])\]([ ]*)/gm).forEach(result => {
- let [ matched, sb, dice, , ability, sa ] = result;
- if (ability.startsWith("rang "))
- ability = `rang_voie${ findPathByName(ability, pathNames) }`;
- const replace = `${sb}[[${dice}${devol} + @{${ability.toLowerCase()}}[${ability}] ]]${sa}`;
+ matchRegExp(text, /([ ]*)\[([1-9])d(4°|E)[ ]*\+[ ]*(AGI|CON|FOR|PER|CHA|INT|VOL|rang voie [1-9]).*\]([ ]*)/gm).forEach(result => {
+ let [ matched, sb, dice, , stat, sa ] = result;
+ if (stat.startsWith("rang "))
+ stat = `rang_voie${ findPathByName(stat, pathNames) }`;
+ const replace = `${sb}[[${dice}${devol} + @{${stat.toLowerCase()}}[${stat}] ]]${sa}`;
text = text.replace(matched, replace);
});
@@ -7745,7 +9162,7 @@ function insertRolls(text, options = {}) {
});
// Search for " [# + XXX] "
- matchRegExp(text, /([ ]*)\[([0-9]+)[ ]*\+[ ]*(AGI|CON|FOR|PER|CHA|INT|VOL|rang voie [1-9])\]([ ]*)/gm).forEach(result => {
+ matchRegExp(text, /([ ]*)\[([0-9]+)[ ]*\+[ ]*(AGI|CON|FOR|PER|CHA|INT|VOL|rang voie [1-9]).*\]([ ]*)/gm).forEach(result => {
let [ matched, sb, value, ability, sa ] = result;
if (ability.startsWith("rang ")) {
ability = `rang_voie${ findPathByName(ability, pathNames) }`;
@@ -7755,7 +9172,7 @@ function insertRolls(text, options = {}) {
});
// Search for " [XXX] "
- matchRegExp(text, /([ ]+)\[(AGI|CON|FOR|PER|CHA|INT|VOL)\]([ ]+)/gm).forEach(result => {
+ matchRegExp(text, /([ ]+)\[(AGI|CON|FOR|PER|CHA|INT|VOL).*\]([ ]+)/gm).forEach(result => {
let [ matched, sb, ability, sa ] = result;
const replace = `${sb}[[@{${ability.toLowerCase()}}]]${sa}`;
text = text.replace(matched, replace);
@@ -7821,7 +9238,8 @@ on("change:repeating_armes:arme-atktype change:repeating_npcarmes:arme-atktype",
/**
* Roll an attack in chat
*/
-on("clicked:repeating_armes:attack-btn", function() {
+on("clicked:repeating_armes:attack-btn", function(eventInfo) {
+ const attackId = eventInfo.triggerName.split("_")[2];
const section = "repeating_armes_arme-"
const attackAttrs = [
"nom",
@@ -7835,8 +9253,10 @@ on("clicked:repeating_armes:attack-btn", function() {
"dmdiv",
"portee",
"special",
+ "atktype",
"atkmods",
- "options"
+ "options",
+ "usure"
];
const sitModAttrs = [
"couvert",
@@ -7845,30 +9265,34 @@ on("clicked:repeating_armes:attack-btn", function() {
"visibilite"
];
getAttrs([
- "cfg_1attack_roll",
- "cfg_crit_diff",
+ "cfg_1attack_roll", "cfg_crit_diff", "cfg_use_ammo", "cfg_usure",
"bouclier_eqp",
- "condition_affaibli",
- "tactique",
- "tactdiv",
- "tactdmdiv",
- "noms_voies",
+ "condition_affaibli", "condition_immobilise",
+ "tactique", "tactdiv", "tactdmdiv",
+ "noms_voies", "rangs_voies",
+ "en_selle",
+ ...luckButtonAttrs(),
...sitModAttrs.map(a => `modsit_${a}`),
- ...attackAttrs.map(a => `${section}${a}`)
+ ...attackAttrs.map(a => `${section}${a}`),
+ "repeating_armes_jet-resid"
], function (values) {
// general options
const useOneRoll = boolval(values.cfg_1attack_roll);
const useCritDiff = intval(values.cfg_crit_diff);
+ const useAmmo = boolval(values.cfg_use_ammo);
+ const useWear = boolval(values.cfg_usure);
const shieldUsed = boolval(values.bouclier_eqp);
const tactics = intval(values.tactique);
const tactBM = intval(values.tactdiv);
const tactDM = strval(values.tactdmdiv);
const weakened = intval(values.condition_affaibli);
- const pathNames = strval(values.noms_voies);
+ const restrained = intval(values.condition_immobilise);
+ const pathNames = getPathNames(strval(values.noms_voies));
+ const mounted = boolval(values.en_selle);
// situational modifiers
- const modCover = intval(values.modsit_couvert) * -1;
+ let modCover = intval(values.modsit_couvert) * -1;
const modMelee = intval(values.modsit_melee) * -1;
const modContact = intval(values.modsit_contact);
const modVisible = intval(values.modsit_visibilite) * -1;
@@ -7876,17 +9300,20 @@ on("clicked:repeating_armes:attack-btn", function() {
// attack parameters
const atk = strval(values[section + "atk"]);
const atkdiv = intval(values[section + "atkdiv"]);
- const name = capitalize(strval(values[section + "nom"], getAttackName(atk)));
+ const name = capitalize(strval(values[section + "nom"], getAttackName(atk)), false);
const portee = strval(values[section + "portee"]);
- const range = (portee !== "") ? ` (${portee})` : "";
- const crit = intval(values[section + "crit"], 20);
+ let range = (portee !== "") ? ` (${portee})` : "";
+ let crit = intval(values[section + "crit"], 20);
const isRanged = (atk === "atktir" && portee !== "");
+ const atkType = strval(values[section + "atktype"]);
const modifiers = strval(values[section + "atkmods"]).split(",").map(mod => mod.trim().toLowerCase());
const options = strval(values[section + "options"]);
+ const usure = strval(values[section + "usure"]);
+ const ressourceId = strval(values["repeating_armes_jet-resid"]);
// 2 mains & bouclier
const twoHanded = boolval(values[section + "2m"]);
- if (twoHanded && shieldUsed) {
+ if (atk !== "atkmag" && twoHanded && shieldUsed) {
sendChatMsg(cof2RollTemplate({
lsub: "Attaque",
rsub: name + range,
@@ -7926,14 +9353,25 @@ on("clicked:repeating_armes:attack-btn", function() {
// Build situational modifiers
let modSit = "";
if (isRanged) {
- modSit += modCover !== 0 ? ` ${modCover}[Cible à ouvert]` : "";
+ if (hasAbility({ name: "joli coup", inPath: "precision" }, values)) {
+ if (modCover === -2)
+ modCover = 0;
+ if (modCover === -5)
+ modCover = -2;
+ }
+ modSit += modCover !== 0 ? ` ${modCover}[Cible à couvert]` : "";
modSit += modMelee !== 0 ? ` ${modMelee}[Cible en mêlée]` : "";
modSit += modVisible !== 0 ? ` ${modVisible}[Visibilité]` : "";
+ if (hasAbility({ name: "tir precis", inPath: "precision" }, values))
+ crit = 19;
+ if (hasAbility({ name: "tir fatal", inPath: "precision" }, values))
+ crit = 18;
}
// Determine normal or malus die roll
- let roll = (weakened === 1) ? "2d20kl1" : "1d20";
- let bRoll = (weakened !== 1);
+ const withMalusDie = (weakened === 1) || (restrained === 1);
+ let roll = withMalusDie ? "2d20kl1" : "1d20";
+ let bRoll = !withMalusDie;
if (isRanged && modContact === 1)
roll = "2d20kl1";
if (modifiers.includes("debonus") && !modifiers.includes("demalus")) {
@@ -7993,9 +9431,29 @@ on("clicked:repeating_armes:attack-btn", function() {
if (modifiers.includes("reroll1"))
dmOpt += "r";
+ let extraDM = "";
+
+ if (mounted && atk === "atkcac") {
+ if (hasAbility({ name: "monture fantastique", inPath: "cavalier" }, values)) {
+ extraDM = " +2[Cavalier rang 5]"
+ } else if (hasAbility({ name: "cavalier emerite", inPath: "cavalier" }, values)) {
+ extraDM = " +1[Cavalier rang 2]"
+ }
+ }
+
+ if (modifiers.includes("poudre")) {
+ let powderBonus = 0;
+ if (hasAbility({ name: "poudre puissante", inPath: "explosifs" }, values)) {
+ powderBonus = 1 + getPathsWithRank({ profile: "arquebusier", minRank: 5 }, values);
+ range = ` ([[${intval(portee)}[Base] + 10[Poudre puissante]]])`;
+ }
+ if (powderBonus > 0)
+ extraDM = ` +${powderBonus}[Poudre puissante]`;
+ }
+
let dmroll = "";
if (dm !== "" )
- dmroll = `[[${dm}${dmOpt}]][DM] + ${dmbonus}[Bonus] + ${dmdiv}[Divers]${tacticDM}`;
+ dmroll = `[[${dm}${dmOpt}]][DM] + ${dmbonus}[Bonus] + ${dmdiv}[Divers]${tacticDM}${extraDM}`;
if (dmroll !== "" && tactics === 1)
dmroll = `floor((${dmroll})/2)`;
if (dmroll !== "")
@@ -8037,23 +9495,84 @@ on("clicked:repeating_armes:attack-btn", function() {
if (fxValue !== "")
playFX(fxValue);
+ // Check special effect on die roll
+ const rollOptions = {};
+ let rollEffect = "";
+ if (hasProp(options, "effet")) {
+ const [ tn, effect ] = getProp(options, "effet").replace(/\s\s+/g, " ").split("?");
+ rollOptions["rollTN"] = intval(tn.trim());
+ rollEffect = effect.trim();
+ }
+
// Roll attack !
const atkProps = {
lsub: atkTitle.replace("/", " /\n"),
rsub: name + range,
roll: attack,
+ rolleffect: rollEffect,
broll: "",
+ brolleffect: rollEffect,
dm: dmroll,
dmdesc,
critdiff: `[[${useCritDiff}]]`,
dmcrit,
powder,
- text: special
+ text: special,
+ chance: addLuckButton(values),
+ usure: ""
};
if (roll === "1d20" && bRoll)
atkProps.broll = attack;
+ if ([ "main", "jet" ].includes(atkType) && useWear) {
+ atkProps.usure = "[[1d10]]";
+ rollOptions["usure"] = { attackId, value: usure, name };
+ }
const chatMsg = cof2RollTemplate(atkProps);
- sendChatMsg(chatMsg);
+ sendChatMsg(chatMsg, rollOptions);
+
+ if (useAmmo && ressourceId !== "") {
+ const resVal = `repeating_ressources_${ressourceId}_res-val`;
+ getAttrs([ resVal ], function(values) {
+ const munitions = Math.max(0, intval(values[resVal]) -1);
+ setAttrs({ [resVal]: munitions });
+ });
+ }
+ });
+});
+
+/**
+ * Check usure
+ */
+on("change:usure_check", function() {
+ getAttrs([ "usure_check" ], function(values) {
+ const check = strval(values.usure_check);
+ if (check === "")
+ return;
+
+ const updates = new Map();
+
+ try {
+ let { attack, name, usure, roll } = JSON.parse(check);
+ console.log(attack, name, usure, roll)
+ if (attack !== "") {
+ usure = Math.max(0, usure - 1);
+ updates.set(`repeating_armes_${attack}_arme-usure`, usure);
+ if (roll > usure) {
+ sendChatMsg(cof2RollTemplate({
+ lsub: "Usure",
+ rsub: name,
+ text: `L'arme se brise (usure : ${usure}) !`,
+ textclass: "fumble"
+ }));
+ }
+ }
+ }
+ catch (e) {}
+ finally {
+ updates.set("usure_check", "");
+ setAttrs(Object.fromEntries(updates));
+ }
+
});
});
@@ -8197,13 +9716,16 @@ function actionAbility(path, rank) {
const updates = new Map();
- // alert on usage
let alert = "";
let alertCSS = "";
+ let expended = false;
+
+ // alert on usage
if (maxUses !== 0) {
if (nbUsed > maxUses) {
- alert = `Déjà utilisée ${maxUses} fois / ${usage.toLowerCase()}`;
description = "";
+ expended = true;
+ alert = `Déjà utilisée ${maxUses} fois / ${usage.toLowerCase()}`;
alertCSS = "fumble";
} else {
alert = `Utilisée ${nbUsed} fois`;
@@ -8211,16 +9733,17 @@ function actionAbility(path, rank) {
}
// alert on spells & PM
- if (abilityOwned === 1 && abilitySpell === 1) {
+ if (!expended && abilityOwned === 1 && abilitySpell === 1) {
let pm = intval(values.pm);
+ alert += (alert !== "") ? "\n" : "";
if (rank > pm) {
- alert = "Plus assez de Points de Mana\n";
+ alert += "Plus assez de Points de Mana\n";
alert += `${pm} restants pour ${rank} nécessaires\n`;
alert += `Brûlure de Mana : [[${rank - pm}d@{drecup}]] PV perdus\n`
alertCSS = "fumble";
} else {
pm -= rank;
- alert = `${pm} Points de Mana restants`;
+ alert += `${pm} Points de Mana restants`;
updates.set("pm", pm);
}
}
@@ -8253,47 +9776,25 @@ function actionAbility(path, rank) {
});
}
-/**
- * Update the list of special capabilities
- * @param {string} name - name of ability
- * @param {number} owned - owend ability 0/1
- * @param {string} capabilities - comma-separated of special capabilities
- * @returns {string}
- */
-function processCapabilities(name, owned, capabilities) {
- name = name.replace(/ /gm, "").toLowerCase();
- if (SWData.PC.CAPABILITIES.includes(name)) {
- capabilities += ((capabilities !== "") ? "," : "") + name;
- if (!owned) {
- capabilities = capabilities.split(",").filter(cp => cp !== name).join(",");
- }
- }
- return capabilities;
-}
-
/**
* Update rank in path
* @param {number} path - Path #
*/
function setRank(path) {
rankAttrs = [
- "capabilities",
...seq(5).map(rank => `v${path}r${rank}`),
...seq(5).map(rank => `voie${path}-t${rank}`)
];
getAttrs(rankAttrs, function (values) {
let rank = 0;
- let capabilities = strval(values.capabilities);
- delete values.capabilities;
Object.keys(values).filter(a => !a.startsWith("voie")).forEach(ability => {
const owned = intval(values[ability]);
const hasRank = intval(ability.charAt(ability.length - 1)) * owned;
if (hasRank > rank)
rank = hasRank;
- const name = values[`voie${path}-t${ ability.charAt(ability.length - 1) }`];
- capabilities = processCapabilities(name, owned, capabilities);
});
- setAttrs({ [`rang_voie${path}`]: rank, capabilities });
+ const updates = { [`rang_voie${path}`]: rank };
+ setAttrs(updates);
});
}
@@ -8302,18 +9803,22 @@ function setRank(path) {
* @param {number} path - path number changed
*/
function updateAllRanks(path) {
- getAttrs([ `rang_voie${path}`, `v${path}br`, "rangs_voies" ], function(values) {
+ getAttrs([ `rang_voie${path}`, `v${path}br`, "rangs_voies", "noms_voies" ], function(values) {
const [ rankPath, rankBase ] = Object.keys(values);
let rank = intval(values[rankPath]);
const base = intval(values[rankBase]);
- const ranks = strval(values.rangs_voies);
+ const names = getPathNames(strval(values.noms_voies));
let allRanks = Array(9);
- if (ranks !== "")
- allRanks = ranks.split(",").map(r => intval(r));
+ const rangs_voies = strval(values.rangs_voies);
+ if (rangs_voies !== "")
+ allRanks = getPathRanks(rangs_voies);
if (rank > 0 && base > 1)
rank += base - 1;
allRanks[path - 1] = rank;
- setAttrs({ rangs_voies: allRanks.join(",") });
+ const updates = { rangs_voies: allRanks.join(",") };
+ if (cleanText(names[path - 1], false) === "cavalier")
+ updates.rang_cavalier = rank;
+ setAttrs(updates);
});
}
@@ -8341,44 +9846,68 @@ function updateAllPathNames(path) {
* Check max ability points
*/
function checkAbilityPoints() {
+ const names = [];
const owned = [];
const props = [];
+ const baseRank = [];
seq(9).forEach(v => {
+ baseRank.push(`v${v}br`);
seq(5).forEach(r => {
+ names.push(`voie${v}-t${r}`);
const rank = `v${v}r${r}`;
owned.push(rank);
props.push(rank + "_props");
});
});
- getAttrs([ "cfg_pts_capas", "niveau", ...owned, ...props ], function(values) {
+ getAttrs([
+ "cfg_pts_capas",
+ "niveau",
+ ...owned,
+ ...names,
+ ...props,
+ ...baseRank
+ ], function(values) {
const checkAP = boolval(values.cfg_pts_capas);
if (!checkAP)
return;
const level = intval(values.niveau);
+ const alerts = [];
const maxPoints = level * 2;
let usedPoints = 0;
+
owned.forEach(ability => {
if (!boolval(values[ability]))
return;
const path = intval(ability.charAt(1));
+ const base = intval(baseRank[`v${path}br`], 1);
const rank = intval(ability.charAt(3));
+ const rankPath = rank + base - 1;
- if (path === SWData.settings.peoplePath && rank === 1)
+ if (path === SWData.settings.peoplePath && rankPath === 1)
return;
const props = strval(values[ability + "_props"]);
- if (SWData.settings.level1Paths.includes(path) && rank === 2 && getProp(props, "initial"))
+ if (SWData.settings.level1Paths.includes(path) && rankPath === 2 && getProp(props, "initial"))
return;
+ const minLevel = SWData.settings.minLevels[rankPath - 1];
+ if (level < minLevel) {
+ const rankName = values[`voie${path}-t${rankPath}`];
+ alerts.push(`La capacité ${rankName} (rang ${rankPath}) n'est pas accessible avant le niveau ${minLevel}`);
+ }
+
usedPoints += (rank > 2) ? 2 : 1;
});
if (usedPoints > maxPoints) {
+ let text = `${usedPoints} points utilisés mais ${maxPoints} au maximum`;
+ if (alerts.length > 0)
+ text += "\n" + alerts.join("\n");
const chatMsg = cof2RollTemplate({
lsub: "Contrôle",
rsub: "Points de Capacités",
- text : `${usedPoints} utilisés mais ${maxPoints} maximum`
+ text
});
sendChatMsg(chatMsg, {
...SWData.RT_OPTIONS,
@@ -8390,11 +9919,15 @@ function checkAbilityPoints() {
/**
* Return simple / unqualified path name
- * @param {string} fullPathName - full path name (voie de ...)
+ * @param {string} fullPathName - full path name (voie du|de la|des ...)
+ * @param {boolean} [cleanup=true] - clean up text (accents & COF tags)
* @returns {string}
*/
-function simplePathName(fullPathName) {
- return fullPathName.toLowerCase().replace(/voie[ ]+(du|des|de la|de l'|de l’)/, "").trim();
+function simplePathName(fullPathName, cleanup = true) {
+ let simplePathName = fullPathName.toLowerCase().replace(/voie[ ]+(du|des|de la|de l'|de l’)/, "");
+ if (cleanup)
+ simplePathName = cleanText(simplePathName);
+ return simplePathName.trim();
}
/**
@@ -8427,13 +9960,13 @@ seq(9).forEach(path => {
actionAbility(path, rank);
});
- on(`change:v${path}r${rank} change:v${path}r${rank}_param`, function() {
+ on(`change:v${path}r${rank} change:v${path}r${rank}_param`, function(eventInfo) {
// Update rank in path
setRank(path);
// Update PV
- updatePVMax();
+ updatePVMax(eventInfo);
// Check ability points
checkAbilityPoints();
@@ -8455,8 +9988,8 @@ seq(9).forEach(path => {
});
// Update PV max
- on(`change:voie${path}pv`, function () {
- updatePVMax();
+ on(`change:voie${path}pv`, function (eventInfo) {
+ updatePVMax(eventInfo);
});
});
@@ -8514,7 +10047,7 @@ on("clicked:reset_uses-btn", function() {
}
// Ask for which abilities to reset
- ask([ "Capacités ?", ...freqs ], function(selected, choices) {
+ ask([ "Utilisations capacités ?", ...freqs ], function(selected, choices) {
const freq = choices[selected];
const titles = abilities[freq].map(a => `voie${ a.charAt(1) }-t${ a.charAt(3) }`);
const usages = abilities[freq].map(a => `v${ a.charAt(1) }r${ a.charAt(3) }_use_max`);
@@ -8572,8 +10105,8 @@ function abilityUsages(path = 0) {
if (matches.length === 0)
return; // no regex match
- const [ , use, freq ] = matches[0];
- if ([ "round", "tour" ].includes(freq.toLowerCase()))
+ const [ use, freq ] = matches[0].slice(1);
+ if ([ "round", "tour" ].includes(freq.toLowerCase()))
return; // no per round usage
const useMax = [ "une", "deux", "trois" ].indexOf(use.toLowerCase()) + 1;
@@ -8593,8 +10126,8 @@ function abilityUsages(path = 0) {
*/
function abilityRollQuery() {
return "?{Carac ?|"
- + SWData.PC.ABILITIES
- .map(a => `${ a.substring(0,3).toUpperCase() },@{${ a.substring(0,3) }}`)
+ + SWData.PC.STATS
+ .map(stat => `${ upperShort(stat) },@{${ shorten(stat) }_test}`)
.join("|")
+ "}";
}
@@ -8612,7 +10145,7 @@ on("clicked:repeating_rolls:roll-btn", function() {
"voie",
"bonus"
].map(attribute => `${section}${attribute}`);
- getAttrs([ "rangs_voies", "noms_voies", ...rollAttrs ], function (values) {
+ getAttrs([ "rangs_voies", "noms_voies", ...luckButtonAttrs(), ...rollAttrs ], function (values) {
const rollName = strval(values[section + "nom"]);
const type = strval(values[section + "type"]);
let carac = strval(values[section + "carac"]);
@@ -8620,10 +10153,10 @@ on("clicked:repeating_rolls:roll-btn", function() {
const bonus = intval(values[section + "bonus"]);
let rollCmd = strval(values[section + "cmd"]);
- const ranks = values.rangs_voies.split(",").map(r => intval(r));
- const names = values.noms_voies.split("|").map(r => strval(r));
+ const ranks = getPathRanks(strval(values.rangs_voies));
+ const names = getPathNames(strval(values.noms_voies));
- let lsub = "Compétence";
+ let lsub = "Capacité";
let text = "";
if (type !== "0") {
@@ -8643,7 +10176,6 @@ on("clicked:repeating_rolls:roll-btn", function() {
// + Rang
if (path > 0) {
rollCmd += ` + (${ranks[path - 1]})[Rang ${names[path - 1]}]`;
- lsub = "Capacité";
}
// + Bonus
if (bonus !== 0)
@@ -8660,7 +10192,8 @@ on("clicked:repeating_rolls:roll-btn", function() {
lsub,
rsub: rollName,
roll: rollCmd,
- text
+ text,
+ chance: addLuckButton(values)
});
sendChatMsg(chatMsg);
});
@@ -8673,7 +10206,7 @@ SWData.PC.SKILLS.forEach(skill => {
on(`clicked:skill-${skill.name}`, function () {
const skillName = (skill.label !== "") ? skill.label : capitalize(skill.name);
const skillAttrs = [ "carac", "bonus", "def" ].map(a => `${skill.name}_${a}`);
- getAttrs(skillAttrs, function (values) {
+ getAttrs([ ...skillAttrs, ...luckButtonAttrs() ], function (values) {
const [ carac, bonus, def ] = Object.keys(values);
let caracBonus = strval(values[carac]);
const skillBonus = intval(values[bonus]);
@@ -8699,7 +10232,8 @@ SWData.PC.SKILLS.forEach(skill => {
lsub: "Compétence",
rsub: skillName,
roll: rollCmd,
- broll: rollCmd
+ broll: rollCmd,
+ chance: addLuckButton(values)
});
sendChatMsg(chatMsg);
@@ -8756,25 +10290,31 @@ on("change:repeating_buffs remove:repeating_buffs", function () {
* @param {string} attribute - attribute name
*/
function parseBuffList(attribute) {
- getAttrs([ `${attribute}_buff_list`, ...SWData.PC.BUFFS.From ], function(values) {
+ getAttrs([ `${attribute}_buff_list`, ...SWData.PC.BUFFS.From, "noms_voies" ], function(values) {
const [ buff ] = Object.keys(values);
const buffList = strval(values[buff]).split("; ");
- const allRanks = values.rangs_voies.split(",");
+ const allRanks = getPathRanks(values.rangs_voies);
let buffTotal = 0;
buffList.forEach(buff => {
if (buff.trim() === "")
return;
let value = 0;
- const [ expression ] = buff.split(":").map(e => e.trim()).slice(1);
+ const expression = strval(buff.split(":")[1]).trim();
const attrBuff = SWData.PC.BUFFS.From.find(a => `[${a}]` === expression.toLowerCase());
if (attrBuff) {
value = intval(values[attrBuff]);
} else {
- const result = expression.match(/\[rang[ ]*voie[ ]*([1-9])\][ ]*\+?[ ]*([0-9]*)?/i);
+ let result = expression.match(/\[rang[ ]*voie[ ]*([1-9])\][ ]*\+?[ ]*([0-9]*)?/i);
+ if (!result)
+ result = expression.match(/\[rang[ ]*(.*)\][ ]*\+?[ ]*([0-9]*)?/i);
if (result) {
const [ matched, path, bonus ] = result;
- if (matched && intval(path) > 0)
- value = intval(allRanks[intval(path) - 1]);
+ let pathNumber = intval(path);
+ if (pathNumber === 0) {
+ pathNumber = findPathByName("rang " + path, getPathNames(values.noms_voies));
+ }
+ if (matched && pathNumber > 0)
+ value = allRanks[pathNumber - 1];
if (bonus)
value += intval(bonus);
} else {
@@ -8805,6 +10345,31 @@ function rebuildBuffValues() {
});
}
+// Recalculate buffs values on change of source attributes
+SWData.PC.BUFFS.From.forEach(attribute => {
+ on(`change:${attribute}`, function () {
+ rebuildBuffValues();
+ })
+});
+
+/**
+ * On click on the buffs syntax help button
+ */
+on("clicked:helpbuff-btn", function() {
+ sendChatMsg([
+ "title=Buffs/De-buffs",
+ "Valeur=Valeur fixe",
+ "[CAR]=Score de la caractéristique *CAR* (AGI, FOR, CHA...)",
+ "[rang voie *N*]=Rang dans la voie n° *N*",
+ "[rang voie *Nom*]=Rang dans la voie *Nom*",
+ "[rang *Nom*]=Rang dans la voie *Nom*",
+ "desc=Vous pouvez ajouter un bonus après la voie" + "\n" + "Ex. : *[rang voie 2] + 1*"
+ ], {
+ template: "custom",
+ whisper: "gm"
+ });
+});
+
/**
* Parse abilities path text
* @param {string} text - text of abilities path
@@ -8815,11 +10380,11 @@ function rebuildBuffValues() {
function importFromPDF(text, path, base = 1) {
const updates = new Map();
- let ability = "";
+ let ability = `voie${path}notes`;
text.split("\n").forEach((line, index) => {
// first line
if (index === 0) {
- let pathName = simplePathName(line.toLowerCase());
+ let pathName = simplePathName(line.toLowerCase(), false);
if (pathName !== "")
updates.set(`voie${path}nom`, capitalize(pathName));
return;
@@ -8855,7 +10420,7 @@ seq(9).forEach(path => {
return;
const base = values[`v${path}br`];
- const updates = importFromPDF(text, base); /*
+ const updates = importFromPDF(text, path, base); /*
const updates = new Map();
let ability = "";
text.split("\n").forEach((line, index) => {
@@ -8927,11 +10492,11 @@ function encumbrance() {
});
const effects = [];
- if (encumbrance * useEncumbrance > threshold )
+ if (encumbrance * useEncumbrance > threshold)
effects.push("Essouflé-e et dé malus aux tests d'AGI");
- if (encumbrance * useEncumbrance > 2 * threshold )
+ if (encumbrance * useEncumbrance > 2 * threshold)
effects.push("Ralenti");
- if (encumbrance * useEncumbrance > 3 * threshold )
+ if (encumbrance * useEncumbrance > 3 * threshold)
effects.push("Immobilisé");
if (effects.length > 0) {
const text = "@{character_name} est encombré-e et subit les effets suivants :\n"
@@ -8957,11 +10522,12 @@ on("clicked:helpgear-btn", function() {
sendChatMsg([
"title=Propriétés d'équipement",
"subtitle=Liste des codes et valeurs",
- "dm-1m:=Dés de DM une main",
- "dm-2m:=Dés de DM deux mains",
+ "dm-1m:=Dés de DM à une main",
+ "dm-2m:=Dés de DM à deux mains",
"type-dm:=Types de dommages",
"critique:=Seuil de critique",
"portee:=Portée arme à distance",
+ "usure:=Seuil d'usure",
"bonus-def:=Bonus DEF de l'armure ou du bouclier",
"armure:=Bonus DEF de l'armure",
"armure-malus:=Malus d'armure portée",
@@ -8977,13 +10543,20 @@ on("clicked:helpgear-btn", function() {
});
/**
- * Get gear properties from name
+ * Get gear properties list from name
* @param {string} name - gear name
- * @returns {string}
+ * @returns {string[]}
*/
function getGearProps(name) {
- const gear = SWData.GEARS.find(g => g.name.toLowerCase() === name.toLowerCase());
- return gear ? gear.props : "";
+ const found = SWData.GEARS.find(gear => {
+ if (cleanText(gear.name, false) === cleanText(name, false))
+ return gear;
+ if (strval(gear.altname) !== "") {
+ if (gear.altname.split(",").includes(cleanText(name)))
+ return gear;
+ }
+ });
+ return found ? found.props : [];
}
/**
@@ -9203,18 +10776,48 @@ on("change:repeating_equipement remove:repeating_equipement", function() {
encumbrance();
});
+/**
+ * Display equipment details in chat
+ */
+on("clicked:repeating_equipement:equipshow-btn", function () {
+ const section = "equipement";
+ const gearAttrs = [ "nom", "detail", "props" ].map(a => repeatingAttr(section, `equip-${a}`));
+ getAttrs([ ...gearAttrs, "character_name" ], function(values) {
+ const nom = strval(values[repeatingAttr(section, "equip-nom")]);
+ const detail = strval(values[repeatingAttr(section, "equip-detail")]);
+ const props = strval(values[repeatingAttr(section, "equip-props")]);
+ if (detail === "" && props === "")
+ return
+ const chatMsg = [
+ "title=Equipement",
+ `subtitle=${nom}`,
+ `desc=${ strval(values.character_name) }`
+ ];
+ if (detail !== "")
+ chatMsg.push(`Description=${detail}`);
+ if (props !== "") {
+ props.split("\n").filter(prop => strval(prop) !== "").forEach(prop => {
+ const [ code, value ] = strval(prop).split(":");
+ chatMsg.push(`Prop: ${code}=${value} `);
+ });
+ }
+ sendChatMsg(chatMsg, { template: "custom" });
+ });
+});
+
/**
* Retrieve props from name
*/
on("change:repeating_equipement:equip-nom", function() {
- getAttrs([ "repeating_equipement_equip-nom", "repeating_equipement_equip-props" ], function(values) {
- const name = strval(values["repeating_equipement_equip-nom"]);
- const hasProps = strval(values["repeating_equipement_equip-props"]);
+ const section = "equipement";
+ const gearAttrs = [ "nom", "props" ].map(a => repeatingAttr(section, `equip-${a}`));
+ getAttrs(gearAttrs, function(values) {
+ const name = strval(values[repeatingAttr(section, "equip-nom")]);
+ const hasProps = strval(values[repeatingAttr(section, "equip-props")]);
if (name !== "" && hasProps === "") {
- let props = getGearProps(name);
- if (props !== "") {
- props = props.split(",").join("\n");
- setAttrs({ ["repeating_equipement_equip-props"]: props });
+ const props = getGearProps(name);
+ if (props.length > 0) {
+ setAttrs({ [repeatingAttr(section, "equip-props")]: props.join("\n") });
}
}
})
@@ -9229,7 +10832,7 @@ on("change:repeating_equipement:equip-on", function(eventInfo) {
const hands = [
"main_princ",
"main_autre"
- ]
+ ];
processAllGear([ "on", "nom", "props" ], function(gearIds, allGears) {
getAttrs([
`${section}_equip-on`,
@@ -9250,7 +10853,6 @@ on("change:repeating_equipement:equip-on", function(eventInfo) {
return;
// unequip weapon in hand
- console.log(equiped, atkId)
if (equiped === 0 && atkId !== "") {
let unEquiped;
hands.forEach(hand => {
@@ -9272,8 +10874,9 @@ on("change:repeating_equipement:equip-on", function(eventInfo) {
// search for armure-malus: | bouclier-malus: | casque-malus:
// or set to armor | shield | casque bonus
- let defMalus = intval(getProp(props, defense + "-malus"));
- defMalus = defMalus === 0 ? defBonus : defMalus;
+ const malus = defense + "-malus";
+ let defMalus = intval(getProp(props, malus));
+ defMalus = defMalus === 0 && !hasProp(props, malus) ? defBonus : defMalus;
const defCarMax = intval(getProp(props, "agi-max"));
@@ -9317,6 +10920,27 @@ on("change:repeating_equipement:equip-on", function(eventInfo) {
})
});
+/**
+ * Delete equiped defense gear
+ */
+on("remove:repeating_equipement", function (eventInfo) {
+ const rowId = eventInfo.sourceAttribute + "_";
+ const equiped = boolval(eventInfo.removedInfo[rowId + "equip-on"]);
+ if (!equiped)
+ return;
+ const name = strval(eventInfo.removedInfo[rowId + "equip-nom"]);
+ const props = strval(eventInfo.removedInfo[rowId + "equip-props"]);
+ const defense = [];
+ if (isArmor(name, props))
+ defense.push(...[ "armure", "armure_eqp", "armure_malus" ]);
+ if (isShield(name, props))
+ defense.push(...[ "bouclier", "bouclier_eqp", "bouclier_malus" ]);
+ if (isHelmet(props))
+ defense.push(...[ "casque", "casque_eqp" ]);
+ if (defense.length > 0)
+ setAttrs(Object.fromEntries(defense.map(a => [ a, 0])));
+});
+
/**
* Handle attacks
*/
@@ -9332,8 +10956,9 @@ on(eventList("change", [
"atk",
"atkid"
].map(a => `repeating_equipement_equip-${a}`);
- getAttrs(gearAttrs, function(values) {
- const [ name, detail, props, atk, atkid ] = Object.keys(values);
+ getAttrs([ "cfg_usure", ...gearAttrs ], function(values) {
+ const usure = boolval(values.cfg_usure);
+ const [ name, detail, props, atk, atkid ] = Object.keys(values).slice(1);
const equipment = strval(values[name]);
const description = strval(values[detail]);
const properties = strval(values[props]);
@@ -9391,6 +11016,8 @@ on(eventList("change", [
attributes.set(rowId + "portee", strval(attackRow.get("portee")));
attributes.set(rowId + "special", description);
attributes.set(rowId + "gearid", gearId);
+ if (usure && attackRow.has("usure"))
+ attributes.set(rowId + "usure", intval(attackRow.get("usure")));
}
if (attributes.size > 0)
@@ -9414,9 +11041,10 @@ function loadBaseGear(profile, attacks = false) {
const rowId = `repeating_equipement_${ generateRowID() }_`;
baseGear.set(rowId + "equip-nom", gear.name);
baseGear.set(rowId + "equip-qte", gear.number);
- const props = getGearProps(gear.name);
- if (props !== "") {
- baseGear.set(rowId + "equip-props", props.split(",").join("\n"));
+ const propList = getGearProps(gear.name);
+ if (propList.length > 0) {
+ const props = propList.join("\n");
+ baseGear.set(rowId + "equip-props", props);
if (props.includes("bonus-def") || props.includes("armure") || props.includes("bouclier"))
baseGear.set(rowId + "equip-on", 1);
if (props.includes("dm-1m") || props.includes("dm-2m"))
@@ -9441,6 +11069,67 @@ on("clicked:profequip-btn", function() {
});
});
+on("change:repeating_ressources:res-props", function(eventInfo) {
+ const rsRessources = "repeating_ressources";
+ const rsArmes = "repeating_armes";
+ const resourceId = eventInfo.sourceAttribute.split("_")[2];
+ getSectionIDs(rsArmes, rowIds => {
+ const allAttacks = [];
+ rowIds.forEach(rowId => {
+ allAttacks.push(...[ "arme-atktype", "jet-nom" ].map(a => `${rsArmes}_${rowId}_${a}`));
+ });
+ getAttrs([ "cfg_use_ammo", `${rsRessources}_res-desc`, `${rsRessources}_res-props`, ...allAttacks ], function(values) {
+ const useAmmo = boolval(values.cfg_use_ammo);
+ if (!useAmmo)
+ return;
+
+ const desc = strval(values[`${rsRessources}_res-desc`]);
+ const props = strval(values[`${rsRessources}_res-props`]);
+ if (getProp(props, "type").toLowerCase() !== "munitions")
+ return;
+
+ const thrownIds = allAttacks
+ .filter(a => a.endsWith("arme-atktype"))
+ .filter(a => values[a] === "jet")
+ .map(a => a.split("_")[2]);
+ let updates;
+ thrownIds.forEach(id => {
+ const name = `${rsArmes}_${id}_jet-nom`;
+ if (strval(values[name]).trim() === desc.trim())
+ updates = { [`${rsArmes}_${id}_jet-resid`]: resourceId };
+ });
+ if (updates)
+ setAttrs(updates);
+ });
+ })
+});
+
+on("change:repeating_ressources:res-val", function(eventInfo) {
+ const section = "repeating_ressources";
+ const attrs = [ "desc", "val", "props" ].map(a => `${section}_res-${a}`)
+ getAttrs([ "cfg_use_ammo", ...attrs ], function (values) {
+ const useAmmo = boolval(values.cfg_use_ammo);
+ if (!useAmmo)
+ return;
+
+ const desc = strval(values[`${section}_res-desc`]);
+ const props = strval(values[`${section}_res-props`]);
+ if (getProp(props, "type").toLowerCase() !== "munitions")
+ return;
+
+ const value = intval(values[`${section}_res-val`]);
+ if (value <= 0) {
+ const chatMsg = cof2RollTemplate({
+ lsub: "Attaque",
+ rsub: "Munitions",
+ text: `Plus de ${desc}`,
+ textclass: "fumble"
+ });
+ sendChatMsg(chatMsg);
+ }
+ });
+});
+
/**
* Rebuild all ranks in path
*/
@@ -9495,7 +11184,7 @@ function linkAttacks() {
if ([ "sort", "naturel" ].includes(type))
return;
const nom = strval(values[`repeating_armes_${id}_arme-nom`]);
- const gear = allGears.find(gear => nom.toLowerCase().includes(strval(values[gear]).toLowerCase()));
+ const gear = allGears.find(gear => cleanText(nom, false).includes(cleanText(strval(values[gear]), false)));
if (gear) {
const gearId = gear.split("_")[2];
if (gearId)
@@ -9513,18 +11202,19 @@ function linkAttacks() {
* On clicking the Reset button
*/
on("clicked:reset_all-btn", function() {
- updateDRMax();
- updatePMMax();
- updatePCMax();
- updatePVMax();
- updateInit();
- updateDef();
- abilityUsages();
- encumbrance();
- rebuildRanks();
- rebuildBuffLists();
- rebuildBuffValues();
- ask("Lier attaques et équipement ?", function(answer) {
+ const e = { triggerName: "RESET" };
+ updateDRMax(e);
+ updatePMMax(e);
+ updatePCMax(e);
+ updatePVMax(e);
+ updateInit(e);
+ updateDef(e);
+ abilityUsages(e);
+ encumbrance(e);
+ rebuildRanks(e);
+ rebuildBuffLists(e);
+ rebuildBuffValues(e);
+ ask("Lier attaques et équipements ?", function(answer) {
if (answer === 1)
linkAttacks();
});
@@ -9642,7 +11332,7 @@ function importGears(gearData, attributes) {
attributes.set(rowId + "equip-detail", gear.special);
const props = gear.props
- .filter(prop => SWData.GEARS.props.includes(prop.id))
+ .filter(prop => SWData.GEARPROPS.includes(prop.id))
.map(prop => `${prop.id}: ${prop.valeur}`);
if (props.length > 0)
attributes.set(rowId + "equip-props", props.join("\n"));
@@ -9706,12 +11396,18 @@ on("clicked:helpdoc-btn", function() {
/**
* Return CSS style for chat menu buttons
- * @param {string} color - PC color name
+ * @param {string|string[]} color - PC color name | [ bckg, text ] color array
+ * @param {object} [font={}] - font properties object
* @returns {string}
*/
-function menuButton(color) {
+function menuButton(color, font = {}) {
color = color || "default";
- const [ backColor, textColor ] = SWData.COLORS[color];
+ font = { size: "0.9em", weight: "bold", ...font };
+ let backColor, textColor;
+ if (Array.isArray(color))
+ [ backColor, textColor = "white" ] = color;
+ else
+ [ backColor, textColor ] = SWData.COLORS[color];
return "\" style=\"" + [
"border: none",
"border-radius: 3px",
@@ -9719,8 +11415,8 @@ function menuButton(color) {
`color: ${textColor}`,
"padding: 2px",
"margin: 1px",
- "font-size: 0.9em",
- "font-weight: bold"
+ `font-size: ${font.size}`,
+ `font-weight: ${font.weight}`
].join("; ");
}
@@ -9731,9 +11427,9 @@ on("clicked:caract_menu-btn", function () {
getAttrs([ "character_name", "couleur_pj" ], function (values) {
const charName = strval(values.character_name);
const color = strval(values.couleur_pj);
- const chatMsg = SWData.PC.ABILITIES.map(ability => {
- const short = shorten(ability);
- return `${capitalize(ability)}=[${short.toUpperCase()}](~${charName}|${short}-btn${menuButton(color)})`
+ const chatMsg = SWData.PC.STATS.map(stat => {
+ const short = shorten(stat);
+ return `${capitalize(stat)}=[${short.toUpperCase()}](~${charName}|${short}-btn${menuButton(color)})`
});
sendChatMsg([
`title=${charName}`,
@@ -9747,6 +11443,30 @@ on("clicked:caract_menu-btn", function () {
});
});
+/**
+ * Display conditions menu
+ */
+on("clicked:etats_menu-btn", function() {
+ const allConditions = SWData.PC.CONDITIONS.map(c => `condition_${c.name}`);
+ getAttrs([ "character_name", "couleur_pj", "notif_conditions", ...allConditions ], function (values) {
+ const charName = strval(values.character_name);
+ const whisper = intval(values.notif_conditions) === 2 ? "gm" : "";
+ const chatMsg = SWData.PC.CONDITIONS.map(condition => {
+ const color = boolval(values[`condition_${condition.name}`]) ? [ "#dc3545" ] : [ "#198754" ];
+ return `${condition.label}=[⚪](~${charName}|${condition.name}-icon${menuButton(color)})`;
+ });
+ sendChatMsg([
+ `title=${charName}`,
+ "subtitle=Etats préjudiciables",
+ "color=" + strval(values.couleur_pj),
+ ...chatMsg
+ ], {
+ whisper,
+ template: "custom"
+ });
+ });
+});
+
/**
* Display attack rolls chat menu
* @param {string} [npc=""] - PC ("") or NPC ("npc") attacks
@@ -9936,36 +11656,22 @@ on("clicked:pc_menus-btn", function () {
/**
* On NPC ability button click
*/
-SWData.PC.ABILITIES.forEach(ability => {
- const short = shorten(ability);
+SWData.PC.STATS.forEach(stat => {
+ const short = shorten(stat);
on(`clicked:pnj_${short}-btn`, function () {
- getAttrs([ `${short}_sup` ], function (value) {
- const [ dType ] = Object.keys(value);
- const roll = intval(value[dType]) === 1 ? "2d20kh1" : "1d20";
- const carac = `[[${roll}[Dé] + (@{${short}})[Bonus ${short.toUpperCase()}] ]] `;
+ getAttrs([ `${short}_sup` ], function (values) {
+ const [ dType ] = Object.keys(values);
+ const roll = `[[${ dieRoll(values[dType]) }[Dé] + (@{${short}})[Bonus ${short.toUpperCase()}] ]] `;
const chatMsg = cof2RollTemplate({
lsub: "Test",
- rsub: capitalize(ability),
- roll: carac
+ rsub: capitalize(stat),
+ roll
});
sendChatMsg(chatMsg);
});
});
});
-/**
- * On NPC init button click
- */
-on("clicked:pnj_init-btn", function () {
- const init = `[[@{init}[Initiative] + @{cfg_init_variable}[Dé(s)] &{tracker} ]]`;
- const chatMsg = cof2RollTemplate({
- lsub: "Jet",
- rsub: "Initiative",
- roll: init
- });
- sendChatMsg(chatMsg);
-});
-
/**
* Set NPC attack score
*/
@@ -10025,34 +11731,93 @@ on("clicked:repeating_npcarmes:npcatk-btn", function() {
"dmtype",
"dmdiv",
"portee",
- "special"
+ "special",
+ "options"
];
- getAttrs([ "cfg_crit_diff", "compagnon", ...attackAttrs.map(attribute => `${section}${attribute}`) ], function (values) {
+ getAttrs([
+ "cfg_1attack_roll",
+ "cfg_crit_diff",
+ "compagnon",
+ "atkgroup", "nbgroup",
+ ...attackAttrs.map(attribute => `${section}${attribute}`)
+ ], function (values) {
+ const useOneRoll = boolval(values.cfg_1attack_roll);
const useCritDiff = intval(values.cfg_crit_diff);
let companionOf = strval(values.compagnon);
companionOf = companionOf !== "" ? companionOf + "|" : "";
+ const group = intval(values.atkgroup);
+ const groupNb = intval(values.nbgroup);
const name = capitalize(strval(values[section + "nom"], "Attaque"));
- const use_multi = intval(values[section + "multi"]);
+ const useMulti = intval(values[section + "multi"]);
const roll = strval(values[section + "roll"]);
const atk = strval(values[section + "atk"]);
const portee = strval(values[section + "portee"]);
const range = (portee !== "") ? ` (${portee})` : "";
const crit = intval(values[section + "crit"], 20);
+ const grouped = "Attaque groupée";
+ const bonus5 = { nb: 2, bonus: ` +5[${grouped}]` };
+ const bonus10 = { nb: 3, bonus: ` +10[${grouped}]` };
+ const bonusAuto = { nb: 4, bonus: "" };
+ const autoHit = "Touche automatique";
+ const atkgrp = [];
+ switch (group) {
+ case 1:
+ atkgrp.push({ nb: 1, bonus: "" });
+ break;
+ case 2:
+ atkgrp.push(bonus5);
+ break;
+ case 3:
+ atkgrp.push(bonus10);
+ break;
+ case 4:
+ atkgrp.push(bonusAuto);
+ break;
+ case 5:
+ if (groupNb === 5) {
+ atkgrp.push(bonus10);
+ atkgrp.push(bonus5);
+ } else if (groupNb === 6) {
+ atkgrp.push(bonus10);
+ atkgrp.push(bonus10);
+ } else if (groupNb === 7) {
+ atkgrp.push(bonusAuto);
+ atkgrp.push(bonus10);
+ } else {
+ atkgrp.push(bonusAuto);
+ atkgrp.push(bonusAuto);
+ }
+ break;
+ }
+
let multiAttacks = 1;
- if (use_multi === 1) {
+ if (useMulti === 1) {
const result = name.match(/\(([0-9]*)[ ]*attaques\)/);
if (result) {
- [ , multiAttacks ] = result;
+ [ multiAttacks ] = result.slice(1);
}
if (intval(multiAttacks) <= 0 )
multiAttacks = 1;
}
- let attack = "";
- if (atk !== "0")
- attack = `[[${roll}cs>${crit}cf1[Dé] + ${atk}[Bonus] ]]`;
-
+ const attacks = [];
+ if (atk !== "0" && atkgrp[0].nb !== 4) {
+ // (grouped ?) attack
+ attacks.push(`[[${roll}cs>${crit}cf1[Dé] + ${atk}[Bonus]${atkgrp[0].bonus} ]]`);
+ } else {
+ // auto-hit
+ attacks.push(autoHit);
+ }
+ // second grouped attack ?
+ if (atkgrp.length === 2) {
+ if (atk !== "0" && atkgrp[1].nb !== 4) {
+ attacks.push(`[[${roll}cs>${crit}cf1[Dé] + ${atk}[Bonus]${atkgrp[1].bonus} ]]`);
+ } else {
+ attacks.push(autoHit);
+ }
+ }
+
let [ dm, ...dmextra ] = strval(values[section + "dm"]).split(" ");
const dmtype = strval(values[section + "dmtype"]);
const dmdesc = [ dmtype, ...dmextra ].join(" ").trim();
@@ -10095,25 +11860,59 @@ on("clicked:repeating_npcarmes:npcatk-btn", function() {
break;
}
}
-
- let chatAtk = {
- lsub: "Attaque",
- rsub: name + range,
- roll: attack,
- broll: attack,
- dm: dmroll,
- dmdesc,
- critdiff: `[[${useCritDiff}]]`,
- dmcrit,
- text: special
- };
-
- for (let atk = 1; atk <= multiAttacks; atk++) {
- if (use_multi === 1)
- chatAtk = { ...chatAtk, lsub: `${atk}${ atk > 1 ? "ème" : "ère" } attaque` };
- const chatMsg = cof2RollTemplate(chatAtk);
- sendChatMsg(chatMsg);
+
+ // Check special effect on die roll
+ const options = strval(values[section + "options"]);
+ let rollTN = {};
+ let rollEffect = "";
+ if (hasProp(options, "effet")) {
+ const [ tn, effect ] = getProp(options, "effet").replace(/\s\s+/g, " ").split("?");
+ rollTN = { rollTN: intval(tn.trim()) };
+ rollEffect = effect.trim();
}
+
+ const fxValue = strval(getProp(options, "fx"));
+
+ attacks.forEach((attack, ix) => {
+
+ // Play FX !
+ if (fxValue !== "")
+ playFX(fxValue);
+
+ // base attack roll
+ let chatAtk = {
+ lsub: "Attaque",
+ rsub: name + range,
+ roll: attack,
+ rolleffect: rollEffect,
+ broll: "",
+ brolleffect: "",
+ dm: dmroll,
+ dmdesc,
+ critdiff: `[[${useCritDiff}]]`,
+ dmcrit,
+ text: special,
+ };
+
+ // process grouped attacks
+ if (atkgrp[ix].nb < 4) {
+ if (!useOneRoll)
+ chatAtk = { ...chatAtk, broll: attack, brolleffect: rollEffect };
+ } else {
+ chatAtk.save = 0;
+ }
+ if (atkgrp[ix].nb > 1)
+ chatAtk = { ...chatAtk, alert: `Groupe n°${ix + 1} : [[${atkgrp[ix].nb}]] créatures` };
+
+ // process multi-attacks
+ for (let atk = 1; atk <= multiAttacks; atk++) {
+ if (useMulti === 1)
+ chatAtk = { ...chatAtk, lsub: `${atk}${ atk > 1 ? "ème" : "ère" } attaque` };
+ const chatMsg = cof2RollTemplate(chatAtk);
+ sendChatMsg(chatMsg, rollTN);
+ }
+ });
+
})
});
@@ -10146,7 +11945,10 @@ function setCompanion(values) {
return;
// build the list of values to retrieve
- let askValues;
+ let askValues =
+ `{{mounted=[[@{${companionOf}|en_selle}]] }} ` +
+ `{{rider=[[@{${companionOf}|rang_cavalier}]] }} ` +
+ `{{defowner=[[@{${companionOf}|def}]] }} `
SWData.COMPANIONS.forEach(attr => {
let expr = strval(values[`comp_${attr}`]);
if (expr === "")
@@ -10164,6 +11966,9 @@ function setCompanion(values) {
if (askValues) {
startRoll("!" + askValues, values => {
const updates = new Map();
+ const mounted = boolval(values.results.mounted.result);
+ const rider = intval(values.results.rider.result);
+ const defOwner = intval(values.results.defowner.result);
SWData.COMPANIONS.forEach(attr => {
if (values.results[attr]) {
const value = values.results[attr].result;
@@ -10172,6 +11977,8 @@ function setCompanion(values) {
updates.set(attr, value);
}
});
+ if (mounted && rider >= 2 && defOwner > intval(updates.get("def")))
+ updates.set("def", defOwner);
if (updates.size > 0)
setAttrs(Object.fromEntries(updates));
});
@@ -10196,9 +12003,9 @@ SWData.COMPANIONS.forEach(attr => {
* @returns {Map} updated Map
*/
function importNPCAbilities(sbLine, npcAttrs) {
- SWData.PC.ABILITIES.forEach(ability => {
- const short = shorten(ability);
- const rx = new RegExp(` ${short.toUpperCase()} (.)([0-9]*)(\\*)? `);
+ SWData.PC.STATS.forEach(stat => {
+ const short = shorten(stat);
+ const rx = new RegExp(` ${ short.toUpperCase() } (.)([0-9]*)(\\*)? `);
const match = sbLine.match(rx);
if (!match)
return;
@@ -10577,6 +12384,44 @@ function migrateCOF1Sheet(sheetType, verCOF1) {
});
}
+/**
+ * Update migration log
+ * @param {string} log - current migration log
+ * @param {string} newVer - new version label
+ * @param {string} text - migration text to add
+ * @returns {string}
+ */
+function migrateLog(log, newVer, text) {
+ text = `${new Date().toLocaleString() } - v${newVer}: ${text}`;
+ return text + "\n" + log;
+}
+
+/**
+ * Migrate to Version 1.3.0
+ * @param {string} sheetType - type of sheet
+ * @param {object} curVer - semantic version parts
+ * @returns {void}
+ *
+ * Update xxx_base caracteristics attributes with xxx values
+ */
+function migrateSheetToV1_3(sheetType, curVer = { major: 1, minor: 2, patch: 0 }) {
+ if (sheetType !== SWData.SHEET_TYPE.PC)
+ return;
+
+ getAttrs([ "migration", ...SWData.PC.STATS.map(s => shorten(s)) ], function(values) {
+ let migration = strval(values.migration);
+ delete values.migration;
+ const updates = new Map();
+ Object.keys(values).forEach(attr => {
+ const base = `${attr}_base`;
+ updates.set(base, intval(values[attr]));
+ migration = migrateLog(migration, "1.3.0", `${base} = ${ updates.get(base) }`)
+ });
+ updates.set("migration", migration);
+ setAttrs(Object.fromEntries(updates));
+ });
+}
+
/**
* Apply sheet migrations
* @param {string} sheetType - character sheet type
@@ -10584,8 +12429,8 @@ function migrateCOF1Sheet(sheetType, verCOF1) {
* @returns {void}
*/
function migrateSheet(sheetType, curVer) {
- if (curVer.major === 1 && curVer.minor < 1)
- //migrateSheetVMaj_min(sheetType);
+ if (curVer.major === 1 && curVer.minor < 3)
+ migrateSheetToV1_3(sheetType, curVer);
return;
}
@@ -10699,7 +12544,8 @@ function loadAttackLists(section) {
*/
function parseSemVer(version) {
version += ".0.0";
- if (version.toLowerCase().startsWith("v")) version = version.slice(1);
+ if (version.toLowerCase().startsWith("v"))
+ version = version.slice(1);
const [ major, minor, patch ] = version.split(".");
return {
major: intval(major),
@@ -10717,20 +12563,11 @@ on("sheet:opened", function() {
...seq(9).map(p => `voie${p}nom`),
];
getAttrs([
- "version",
- "verfdp",
- "debug_mode",
- "scriptVersion",
- "scriptVersion_max",
+ "version", "verfdp",
+ "debug_mode", "scriptVersion", "scriptVersion_max",
"character_token",
- "sheet_type",
- "profil",
- "famille",
- "compagnon",
- "comp_def",
- "comp_pv",
- "comp_init",
- "comp_pnjatk",
+ "sheet_type", "profil", "famille",
+ "compagnon", "comp_def", "comp_pv", "comp_init", "comp_pnjatk",
"pnjatk",
"pc_subtab1",
...pathAttrs
@@ -10788,6 +12625,7 @@ on("sheet:opened", function() {
if (values.version !== values.verfdp)
updates.set("version", values.verfdp);
+
setAttrs(Object.fromEntries(updates));
});
});
@@ -10796,9 +12634,11 @@ on("sheet:opened", function() {
* Run charactermancer
*/
on("clicked:charmancer-btn", function() {
- deleteCharmancerData([], function() {
- startCharactermancer("main");
- });
+ startCharactermancer("main");
+});
+
+on("page:main", function() {
+ deleteCharmancerData();
});
/**
@@ -10832,7 +12672,7 @@ on("mancerchange:peuple", function (eventInfo) {
peopleMods = "+1 AGI ou VOL, -1 FOR";
break;
case "Humain":
- peopleMods = "+1 à la plus faible caract.";
+ peopleMods = "+1 à la plus faible carac.";
break;
case "Nain":
peopleMods = "+1 en CON ou VOL, -1 en AGI";
@@ -10843,27 +12683,27 @@ on("mancerchange:peuple", function (eventInfo) {
});
/**
- * Return ability final value
- * @param {string} ability - short ability name
+ * Return stat final value
+ * @param {string} stat - short stat name
* @param {object} values - charmancer data object
* @returns {number}
*/
-function abilityValue(ability, values) {
+function statValue(stat, values) {
const statArrays = {
"pol": [ 2, 2, 2, 1, 1, 0, -1 ],
"exp": [ 3, 2, 1, 1, 0, 0, -1 ],
"spe": [ 4, 2, 1, 0, 0, -1, -1 ]
};
- const ix = intval(values[`${ability}_rb`]);
+ const ix = intval(values[`${stat}_rb`]);
const value = statArrays[strval(values.type, "exp")][ix];
- const mod = intval(values[`${ability}_mod`]);
+ const mod = intval(values[`${stat}_mod`]);
return (value + mod);
}
/**
* Swap radio buttons values
*/
-SWData.PC.ABILITIES.map(a => shorten(a)).forEach(short => {
+SWData.PC.STATS.map(stat => shorten(stat)).forEach(short => {
const radioBtn = short + "_rb";
const select = short + "_mod";
on(`mancerchange:${radioBtn} mancerchange:${select}`, function (eventInfo) {
@@ -10871,12 +12711,12 @@ SWData.PC.ABILITIES.map(a => shorten(a)).forEach(short => {
const values = getCharmancerData()[page].values;
if (eventInfo.triggerName.endsWith("_rb")) {
const index = intval(eventInfo.newValue);
- const others = SWData.PC.ABILITIES.map(a => shorten(a) + "_rb").filter(a => a !== short + "_rb");
+ const others = SWData.PC.STATS.map(stat => shorten(stat) + "_rb").filter(stat => stat !== short + "_rb");
const sameValue = others.find(a => intval(values[a]) === index);
if (sameValue)
setAttrs({ [sameValue]: eventInfo.previousValue }, { silent: true });
}
- setCharmancerText({ [`${short}-val`]: abilityValue(short, values) });
+ setCharmancerText({ [`${short}-val`]: statValue(short, values) });
});
});
@@ -10898,11 +12738,11 @@ on("mancerfinish:finish", function(mancerData) {
const updates = ({ famille, profil, peuple });
- // abilities
+ // stats
const type = strval(values.type);
if (type !== "") {
- SWData.PC.ABILITIES.map(a => shorten(a)).forEach(short => {
- updates[short] = abilityValue(short, values);
+ SWData.PC.STATS.map(s => shorten(s)).forEach(stat => {
+ updates[stat + "_base"] = statValue(stat, values);
});
}
@@ -10926,8 +12766,9 @@ on("mancerfinish:finish", function(mancerData) {
updates[`v${path}r2_props`] = "initial";
}
});
- if (voies.filter(v => v !== "").length > 0)
- abilityUsages(path);
+ if (voies.filter(v => v !== "").length > 0) {
+ abilityUsages();
+ }
// gear and attacks
const baseGear = boolval(values.base_gear);