diff --git a/ChroniquesOublieesFantasy2/cof2e.html b/ChroniquesOublieesFantasy2/cof2e.html index 0c98ebddd4..613fec6d04 100644 --- a/ChroniquesOublieesFantasy2/cof2e.html +++ b/ChroniquesOublieesFantasy2/cof2e.html @@ -1,9 +1,9 @@
- + - + @@ -14,8 +14,9 @@ + - + @@ -23,6 +24,15 @@ + + + + + + + + + @@ -50,6 +60,8 @@ + + @@ -57,6 +69,13 @@ + + + + + +
@@ -65,7 +84,7 @@
@@ -237,8 +256,8 @@

+

Base

Mod

-

Bonus

Test

@@ -252,7 +271,7 @@
- @@ -260,14 +279,29 @@
- +
- +
+ + + + + Base: +
+ + Buff: +
- - + +
+ + + + Base: +
+ + Buff : +
@@ -280,7 +314,7 @@
- @@ -288,14 +322,29 @@
- +
- + +
+ + + + Base: +
+ + Buff: +
- +
+ + + + Base: +
+ + Buff : +
@@ -308,7 +357,7 @@
- @@ -316,14 +365,29 @@
- +
- + +
+ + + + Base: +
+ + Buff : +
- +
+ + + + Base: +
+ + Buff : +
@@ -336,7 +400,7 @@
- @@ -344,14 +408,29 @@
- +
- + +
+ + + + Base: +
+ + Buff : +
- +
+ + + + Base: +
+ + Buff : +
@@ -364,7 +443,7 @@
- @@ -372,14 +451,29 @@
- +
- + +
+ + + + Base: +
+ + Buff : +
- +
+ + + + Base: +
+ + Buff : +
@@ -392,7 +486,7 @@
- @@ -400,14 +494,29 @@
- +
- + +
+ + + + Base: +
+ + Buff : +
- +
+ + + + Base: +
+ + Buff : +
@@ -420,7 +529,7 @@
- @@ -428,14 +537,29 @@
- +
- + +
+ + + + Base: +
+ + Buff : +
- +
+ + + + Base: +
+ + Buff : +
@@ -594,7 +718,7 @@ /  - + @@ -614,9 +738,10 @@
PM
- + + / - +
@@ -635,7 +760,7 @@
- +
@@ -648,15 +773,19 @@
-
Etats préjudiciables
- +
+ Etats préjudiciables  + +
-
+
+ +
Div.
Action défensive
Score
@@ -881,15 +1012,18 @@
- +  /  - -  =  + +  (  +   +  + )  + +  
@@ -996,23 +1130,12 @@  +   +  @@ -1045,6 +1168,7 @@ +
@@ -1052,12 +1176,17 @@
+
+
+ + +
Disponibles :  @@ -1239,6 +1368,10 @@
+
+ +  En selle +
@@ -2188,6 +2321,7 @@ W
+
@@ -2283,6 +2417,7 @@ W
+
@@ -2378,6 +2513,7 @@ W
+
@@ -2477,6 +2613,7 @@ W
+
@@ -2572,6 +2709,7 @@ W
+
@@ -2667,6 +2805,7 @@ W
+
@@ -2766,6 +2905,7 @@ W
+
@@ -2861,6 +3001,7 @@ W
+
@@ -2956,6 +3097,7 @@ W
+
@@ -3152,13 +3294,13 @@  +  + Rang @@ -3776,7 +3918,12 @@

Nom

Attribut affecté

-

Valeur

+

+ Valeur + +

@@ -3805,6 +3952,18 @@ + + + + + + + + + +

Prédicats

- +
@@ -4446,7 +4831,30 @@
  • - Version 1.2.0 (2025-02-02) + Version 1.3.0 (2025-03-09) + +
      +
    • Ajout de buffs aux caractéristiques pris en compte dans les attributes dérivés
    • +
    • Ajout d'un contrôle sur le niveau minimum requis pour les rangs de capacités
    • +
    • Ajout d'une case "en selle" (pour gestion de la voie du cavalier)
    • +
    • Affichage optionnel d'un bouton Chance dans tous les jets de D20
    • +
    • Ajout d'un menu de chat pour les états préjudiciables
    • +
    • Prise en compte de l'état Immobilisé pour les jets d'attaque avec dé malus
    • +
    • Ajout de toutes les armes et armures dans la liste des équipements reconnus
    • +
    • Gestion simplifée du suivi des munitions
    • +
    • Gestion de l'usure des armes
    • +
    • Ajout de 4 listes d'actions sur l'onglet Script des fiches de PJ et PNJ
    • +
    • Prise en compte des attaques groupées sur la fiche de PNJ
    • +
    • Ajout d'un onglet Equipement simplifié sur la fiche de PNJ
    • +
    • Correction d'un bug sur l'appel du Persomancien
    • +
    +
    +
  • + +
  • +
    + + Version 1.2.0 (2025-02-04)
    • Ajout d'une gestion des armures, boucliers et casques équipés
    • @@ -4480,7 +4888,7 @@
    • Rangs 1 des voies 1,2 & 3 cochés par défaut
    • Ajout d'une option de calcul automatique des PV max
    • Ajout de la gestion de compagnons
    • -
    • Ajout d'options d'attaque pour script COFantasy2
    • +
    • Ajout d'options d'attaque pour le script COFantasy2
    • Remplacement import profil en JSON par import de voie depuis PDF
    • Ajout d'un bouton de génération de l'équipement de base du profil
    • Ajout de la gestion de l'AGI/CON maximum selon l'armure
    • @@ -4507,6 +4915,12 @@
      +

      Migrations

      +
      + Détail des mises à jour + +
      +
  • @@ -4520,6 +4934,7 @@
    +
    @@ -4657,7 +5072,7 @@
    - @@ -4702,190 +5117,340 @@
    + + +
    + + +
    +
    -
    +
    + +
    -
    -
    -

    - ARME / SORT - -

    -

    ATTAQUE

    -

    CRIT.

    -

    DM

    -

    PORTEE

    -

    SPECIAL

    -
    - - - -
    -
    - -
    - -
    -
    - -   - -
    -
    -   -   - -
    -
    - -
    -
    - - -  +  - -
    -
    - -
    -
    - - -
    - + +

    ATTAQUE

    +

    CRIT.

    +

    DM

    +

    PORTEE

    +

    SPECIAL

    - - -
    -
    -
    -
    - + +
    + +
    + +
    + +
    +
    + +   + +
    +
    +   +   + +
    +
    + +
    +
    + + +  +  +
    -
    - +
    +
    -
    - -
    -
    - - -
    -
    -
    -
    - Disponibles :  - -
    -
    - Possédés :  - -
    -
    - Taux perte :  - -  % -
    +
    + +
    +
    -
    - -
    - -
    - -
    - -
    - -
    -
    -

    - CAPACITÉ - -

    -

    DESCRIPTION

    + +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + + +
    +
    +
    +
    + Disponibles :  + +
    +
    + Possédés :  + +
    +
    + Taux perte :  + +  % +
    +
    +
    + +
    + +
    +
    -
    - -
    +
    -
    - +
    + +
    + +

    Attaque groupée

    + +
    +
    + + 1 créature +
    +
    +
    -
    - +
    +
    + + 2 créatures +
    +
    + +5 en attaque +
    -
    - +
    +
    + + 3 créatures +
    +
    + +10 en attaque +
    +
    + +
    +
    + + 4 créatures +
    +
    + Touche automatique +
    +
    + +
    +
    + + Jusqu'à 8 créatures +
    +
    + Nombre :  + +
    -
    +
    + +
    + +
    +
    + +
    + +
    +
    +

    + CAPACITÉ + +

    +

    DESCRIPTION

    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + + +
    + +
    + Equipements + +
    + +
    +
    + + +  Trésor :  +
    +
    +
    + Bourse +
    + PC: + PA: + PO: + PP: +
    +
    +
    +
    + +
    + +
    + +
    + + + + + + Nbre + + + + +
    + +
    + +
    + +
    + +
    + + +
    + +
    +
    + +
    + +
    @@ -4908,61 +5473,79 @@ Avec token +
  • + + Un seul jet d'attaque +
  • + + + +

    Règles optionnelles

    +
      + +
    • + + Initiative variable  +
    • + +
    • + + Départager les init. identiques +
    • + +
    • + + Critiques différenciés  +
    • + +
    + +

    Compagnon

    +
      +
    • - Règles optionnelles : -
        -
      • - Initiative variable  - -
      • -
      • - Critiques différenciés  - -
      • -
      +
      +
      Nom du PJ
      +
      + : +
      +
      +
    • + +
    • +
      +
      Défense
      +
      + : +
      +
    • -
      -
      Compagnon de : 
      -
      - +
      +
      Vigueur
      +
      + : +
      +
      +
    • + +
    • +
      +
      Initiative
      +
      + : +
      +
      +
    • + +
    • +
      +
      Attaque
      +
      + :
      -
        -
      • -
        -
        Défense : 
        -
        - -
        -
        -
      • -
      • -
        -
        Vigueur : 
        -
        - -
        -
        -
      • -
      • -
        -
        Initiative : 
        -
        - -
        -
        -
      • -
      • -
        -
        Attaque : 
        -
        - -
        -
        -
      • -
    @@ -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);