diff --git a/ChroniquesOublieesFantasy2/cof2e.html b/ChroniquesOublieesFantasy2/cof2e.html index c5e66d7560..4206d2a72a 100644 --- a/ChroniquesOublieesFantasy2/cof2e.html +++ b/ChroniquesOublieesFantasy2/cof2e.html @@ -1,8 +1,8 @@
- + - + @@ -127,7 +127,7 @@
@@ -301,6 +301,7 @@ + - @@ -424,7 +425,7 @@
- @@ -468,7 +469,7 @@
- @@ -512,7 +513,7 @@
- @@ -556,7 +557,7 @@
- @@ -600,7 +601,7 @@
- @@ -644,7 +645,7 @@
- @@ -706,7 +707,7 @@
- @@ -731,7 +732,7 @@
- @@ -754,7 +755,7 @@
- @@ -777,7 +778,7 @@
- @@ -838,11 +839,11 @@
- PV -
@@ -863,7 +864,7 @@
Etats préjudiciables  -
@@ -1163,11 +1164,11 @@
- PC -
@@ -1245,8 +1246,12 @@
-   - + + + +   2M : 
@@ -1327,12 +1332,8 @@
- + - - @@ -1419,8 +1420,8 @@
-

Options Tactiques

- +

Options Tactiques

+ +
+
+ +
+
+ +   +
+
+ +
+ +
+
+ + +
+
+ +   +   + +
+
+ +
@@ -1621,12 +1654,12 @@ - -
@@ -3962,7 +3998,7 @@
@@ -3991,7 +4027,7 @@
@@ -4017,7 +4053,7 @@
@@ -4043,7 +4079,7 @@
@@ -4069,7 +4105,7 @@
@@ -4095,7 +4131,7 @@
@@ -4123,7 +4159,7 @@
@@ -4149,7 +4185,7 @@
@@ -4175,7 +4211,7 @@
@@ -4201,7 +4237,7 @@
@@ -4234,7 +4270,7 @@
@@ -4267,7 +4303,7 @@
@@ -4297,7 +4333,7 @@
@@ -4323,7 +4359,7 @@
@@ -4349,7 +4385,7 @@
@@ -4375,7 +4411,7 @@
@@ -4401,7 +4437,7 @@
@@ -4427,7 +4463,7 @@
@@ -4453,7 +4489,7 @@
@@ -4479,7 +4515,7 @@
@@ -4505,7 +4541,7 @@
@@ -4531,7 +4567,7 @@
@@ -4557,7 +4593,7 @@
@@ -4590,7 +4626,7 @@
@@ -4623,7 +4659,7 @@
@@ -4712,7 +4748,7 @@ - +
-
@@ -4767,7 +4802,7 @@ - + @@ -4905,11 +4940,11 @@
-
+ @@ -5075,6 +5110,9 @@
Ressources +
@@ -5085,7 +5123,7 @@ Nombre : -
@@ -5249,6 +5287,10 @@ Attaques ciblées +
  • + + Options tactiques par round +
  • Affichage

    @@ -5280,7 +5322,7 @@ Initiative + AGI -
  • +
  • Départager les init. identiques
  • @@ -5301,11 +5343,11 @@ Points de chance variables
  • - + Utiliser le malus de bouclier
  • - + Suivre les munitions  
  • @@ -5334,7 +5376,7 @@
    -

     High fantasy

    +

     High fantasy

    • @@ -5354,7 +5396,7 @@
      -

       Low Fantasy

      +

       Low Fantasy

      • @@ -5398,7 +5440,7 @@
        -

        Forcer les valeurs maxi.

        +

        Forcer les valeurs maxi.

        • @@ -5956,6 +5998,26 @@
            +
          • +
            + + Version 1.14.0 (2026-01-31) + +
              +
            • Ajout d'une liste d'options tactiques activables avec bonus d'attaque, dé bonus/malus, DM supplémentaires
            • +
            • Ajout d'une option de configuration pour désactiver les options tactiques après une attaque
            • +
            • Suppression des buffs au DM et conversion en options tactiques
            • +
            • Meilleure prise en charge des jets liés aux capacités (propriété 'roll:')
            • +
            • Amélioration des buffs de capacités (propriété 'buff:')
            • +
            • Ajout d'un contrôle du bonus maximum de compétence
            • +
            • Gestion des DM temporaires
            • +
            • Ajout des emojis des compétences dans le popup Roll20
            • +
            • Remplacement du menu de chat des états préjudiciables par un popup Roll20
            • +
            • Assignation automatique des paramètres de base d'une nouvelle attaque
            • +
            +
            +
          • +
          • @@ -5964,9 +6026,8 @@
            • Prise en charge de l'action Aider (script MOD SetChatAttr nécessaire)
            • Ajout de 4 compétences personnalisées optionnelles avec caractéristiques associées
            • -
            • Ajout d'un champ pour le score d'attaque à distance de base à la fiche de PNJ
            • -
            • Ajout d'un champ Genre à la fiche de PNJ
            • Mise à jour automatique des PV en cas d'utilisation d'un consommable de soins
            • +
            • Prise en charge de l'import de texte (voies et créatures) depuis le DRS officiel
            • Gestion des effets prolongés des attaques et capacités
            • Prise en charge du script MOD AddCustomTurn pour améliorer les effets prolongés
            @@ -5983,7 +6044,6 @@
          • Plus d'insertion automatique d'inline-roll [[ ]] dans le texte d'une capacité si ce dernier en comporte déjà
          • Effacement de la liaison entre une ligne d'arme et de munition quand le type d'attaque n'est pas trait
          • Pas d'affichage des champs fréquence d'utilisation pour les capacités dont le nombre d'utilisation maximum est 0
          • -
          • Ajout de la gestion des Points de Chance à la fiche de PNJ
      • @@ -5995,11 +6055,10 @@
        • Correction DM x 2 si le seuil de critique est inférieur à 20
        • -
        • Ajout de boutons Récupérations et Soins au menu d'actions du PJ
        • +
        • Ajout de boutons Récupérations et Soins au menu d'actions
        • Ajout d'une mention pour les manoeuvres avec modificateur de tailles
        • Ajout du jet opposé sur l'utilisation d'une manoeuvre
        • -
        • Ajout de champs pour les scores d'attaque au contact (jet de manoeuvre opposé) et magique aux fiches de PNJ
        • -
        • Possibilité de combiner les options tactiques avec une option spécifique au PJ
        • +
        • Possibilité de combiner les options tactiques avec une option spécifique du profil
    • @@ -6012,18 +6071,15 @@
      • Réinitialisation des capacités utilisées par combat / par jour en cas de récupération rapide ou complète
      • Réinitialisation des PM maximum ec cas de récupération complète
      • -
      • Ajout d'un sous-onglet pour les RD et Résistances aux DM sur la fiche de PJ
      • -
      • Ajout d'une section dépliante pour les RD et Résistances aux DM sur la fiche de PNJ
      • +
      • Ajout d'un sous-onglet pour les RD et Résistances aux DM
      • Ajout d'attributs vitesse pour les déplacements
      • Prise en charge des valeurs de buffs qui changent à certains rangs dans une voie
      • Prise en charge des manoeuvres spéciales dans les jets d'attaque
      • -
      • Ajout de listes de tailles et de type de créatures sur la fiche de PNJ
      • Ajout d'une option pour cibler un token lors d'un jet d'attaque
      • Effacement de l'option tactique choisie après un jet d'attaque
      • Ajout d'une macro pc_actions-btn pour une liste d'actions dans le chat
      • Prise en charge des ressources consommables de type soins
      • Ajout d'un bouton Consommable à la liste des ressources
      • -
      • Ajout du nombre d'utilisations par jour/combat aux capacités de PNJ
    @@ -6062,7 +6118,6 @@
  • Ajout d'une option dans la configuration Token Mod pour appliquer une teinte au token quand le nombre de PV tombe au-dessous de sa valeur maximum.
  • Ajout d'un bouton pour copier la configuration par défaut depuis une fiche nommée PJBase.
  • Option jet:N déplacée du champ Spécial d'une attaque au champ Modificateurs d'attaque
  • -
  • Ajout d'une option dans les coups spéciaux des PNJ pour n'affecter que la prochaine attaque
  • @@ -6075,7 +6130,6 @@
    • Sous-onglet Maîtrises dans l'onglet Capacités pour les langues et les maîtrises spéciales d'armes / armures
    • Ajout d'un popup Roll20 sur le bouton DR pour choisir entre Récupération Rapide ou Complète ou Se dépasser
    • -
    • Ajout de la possibilité d'indiquer jet:N dans le champ Spécial d'une attaque de PNJ pour afficher la description du jet correspondant dans le chat
    • Ajout d'une option Token Mod dans l'onglet Script pour permettre aux joueurs d'interagir avec le token de leur personnage
    @@ -6091,8 +6145,6 @@
  • Ajout d'un champ Prédicats si porté aux lignes d'attaques et d'équipements
  • Création de 2 attaques pour les armes avec propriété lancer depuis l'onglet équipement
  • Ajout des jets des capacités aux boutons épinglables à la barre d'actions Roll20
  • -
  • Ajout d'une gestion de compétences dans la fiche de PNJ
  • -
  • Ajout des options tactiques et de 3 coups spéciaux dans la fiche de PNJ
  • Ajout d'une liste de munitions sur l'onglet équipement (script COFantasy2)
  • @@ -6104,7 +6156,7 @@ Version 1.5.0 (2025-05-08)
      -
    • Ajout d'un onglet Notes aux fiches de PJ
    • +
    • Ajout d'un onglet Notes
    • Ajout d'une couleur de fond aux PV selon l'état courant (Jaune si moins de 50% de PV restants, Rouge si 1 PV restant, Rouge foncé si 0 PV restant)
    • Ajout d'une '*' aux capacités de sort en mode affichage
    • Ajout d'une option pour ajuster les PM dépensés par une capacité de sort
    • @@ -6128,7 +6180,7 @@
      • Ajout d'un bouton pour afficher un check-up de l'état du personnage
      • Ajout d'un bouton pour activer la concentration sur les capacités de sort
      • -
      • Ajout des tables de coups et d'échecs critiques sur les fiches de PJ et PNJ
      • +
      • Ajout des tables de coups et d'échecs critiques
      • Ajout d'une question pour les DM des sorts de zone en attaque
      • Prise en compte des propriétés magiques des armes
      • Prise en compte des règles optionnelles de peur
      • @@ -6156,9 +6208,7 @@
      • 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
      • +
      • Ajout de 4 listes d'actions sur l'onglet Script
      • Correction d'un bug sur l'appel du Persomancien
      @@ -6177,10 +6227,8 @@
    • Modification du calcul des DR max avec une base de 2 sauf pour les profils de Mystiques (3 DR)
    • Modification du calcul des PC max avec une base de 2 sauf pour les profils d'Aventuriers (3 PC)
    • Ajout d'options de configuration pour forcer la valeur max des PV, DR, PC et PM
    • -
    • Modification du champ RD dans la fiche de PNJ
    • Amélioration de la reconnaissance d'inline-rolls dans les descriptions de capacités
    • Ajout du détail de la bourse par type de pièces et total du trésor en PA
    • -
    • Ajout des boutons des menus d'action sur la fiche de PNJ
    • Ajout gestion des objets en main
    • Ajout de la possibilité de lier un FX Roll20 aux jets de caractéristiques, d'attaques et de capacités
    • Persomancien (version expérimentale)
    • @@ -6205,7 +6253,6 @@
    • 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
    • -
    • Ajout de l'onglet Script et des options d'attaque sur les fiches de PNJ
    • Ajout d'un champ propriétés sur les ressources
    @@ -6250,6 +6297,7 @@ +
    @@ -6275,7 +6323,7 @@
    - @@ -6288,7 +6336,7 @@
    - @@ -6301,7 +6349,7 @@
    - @@ -6314,7 +6362,7 @@
    - @@ -6331,7 +6379,7 @@
    - @@ -6344,7 +6392,7 @@
    - @@ -6357,7 +6405,7 @@
    - @@ -6374,7 +6422,7 @@
    -
    @@ -6385,7 +6433,7 @@
    -
    @@ -6457,7 +6505,7 @@
    - @@ -6777,7 +6825,12 @@ l -

    UTILISATIONS

    +

    + UTILISATIONS + +

    DESCRIPTION

    @@ -6830,6 +6883,8 @@
    Tactique + +
    @@ -6839,6 +6894,8 @@
    Notes + +
    @@ -6920,45 +6977,79 @@
    - -
    +
    + +
    +
    + Ressources + +
    -
    - Munitions -
    - -
    - -
    - -
    - -   - -
    +
    -
    - -  /  - -  %  - -   - -
    +
    -
    - -
    +
    + + Nombre : + + +
    -
    +
    + +
    +
    + +
    +
    + + +
    + +
    + Munitions +
    + +
    + +
    + +
    + +   + +
    + +
    + +  /  + +  %  + +   + +
    + +
    + +
    + +
    + +
    +
    @@ -7378,6 +7469,12 @@  
    +
    +
    Tactique (Tooltip)
    +
    + +
    +

    ChatSetAttr @@ -7389,6 +7486,217 @@
    + +
    +

    + Notes de version (v) + +

    + +
    + +
    + +
      + +
    • +
      + + Version 1.14.0 (2026-01-31) + +
        +
      • Ajout d'un bouton de réinitialisation des utilisation de capacités
      • +
      • Ajout d'une case à cocher pour identifier le PNJ comme un mook (TokenMod)
      • +
      • Ajout de l'onglet Version
      • +
      • Ajout de la liste des ressources
      • +
      • Ajout d'une option pense-bête pour le script TokenMod
      • +
      • Ajout de la possibilité d'afficher les informations de tactique et/ou les notes en infobulle visible du MJ uniquement
      • +
      +
      +
    • + +
    • +
      + + Version 1.13.0 (2026-01-04) + +
        +
      • Ajout d'un champ pour le score d'attaque à distance de base
      • +
      • Ajout d'un champ Genre
      • +
      • Prise en charge de l'import de texte (voies et créatures) depuis le DRS officiel
      • +
      +
      +
    • + +
    • +
      + + Version 1.12.0 (2025-11-30) + +
        +
      • Ajout de la gestion des Points de Chance
      • +
      +
      +
    • + +
    • +
      + + Version 1.11.0 (2025-11-11) + +
        +
      • Ajout de champs pour les scores d'attaque au contact (jet de manoeuvre opposé) et magique
      • +
      +
      +
    • + +
    • +
      + + Version 1.10.0 (2025-10-31) + +
        +
      • Ajout d'une section dépliante pour les RD et Résistances aux DM
      • +
      • Ajout d'attributs vitesse pour les déplacements
      • +
      • Ajout de listes de tailles et de type de créatures
      • +
      • Ajout d'une option pour cibler un token lors d'un jet d'attaque
      • +
      • Effacement de l'option tactique choisie après un jet d'attaque
      • +
      • Ajout du nombre d'utilisations par jour/combat aux capacités
      • +
      +
      +
    • + +
    • +
      + + Version 1.8.0 (2025-08-31) + +
        +
      • Ajout d'un bouton pour copier la configuration par défaut depuis une fiche nommée PNJBase.
      • +
      • Ajout d'une option dans les coups spéciaux pour n'affecter que la prochaine attaque
      • +
      +
      +
    • + +
    • +
      + + Version 1.7.0 (2025-07-26) + +
        +
      • Ajout de la possibilité d'indiquer jet:N dans le champ Spécial d'une attaque pour afficher la description du jet de capacité correspondant dans le chat
      • +
      • Ajout d'une option Token Mod dans l'onglet Script pour permettre la configuration rapide du token
      • +
      +
      +
    • + +
    • +
      + + Version 1.6.0 (2025-06-22) + +
        +
      • Ajout d'une gestion de bonus de compétence par caractéristiques
      • +
      • Ajout des options tactiques et de 3 coups spéciaux
      • +
      • Ajout d'une liste de munitions sur l'onglet équipement (script COFantasy2)
      • +
      +
      +
    • + +
    • +
      + + Version 1.5.0 (2025-05-08) + +
        +
      • Ajout de la possibilité d'épingler certains jets à la barre d'actions Roll20
      • +
      • Ajout d'une case attaque active/inactive pour le script COFantasy2
      • +
      +
      +
    • + +
    • +
      + + Version 1.4.0 (2025-04-08) + +
        +
      • Ajout des tables de coups et d'échecs critiques
      • +
      • Ajustements cosmétiques pour les cases à cocher et boutons radio
      • +
      +
      +
    • + +
    • +
      + + Version 1.3.0 (2025-03-09) + +
        +
      • Ajout de 4 listes d'actions sur l'onglet Script
      • +
      • Prise en compte des attaques groupées
      • +
      • Ajout d'un onglet Equipement simplifié
      • +
      +
      +
    • + +
    • +
      + + Version 1.2.0 (2025-02-04) + +
        +
      • Modification du champ RD
      • +
      • Ajout du détail de la bourse par type de pièces et total du trésor en PA
      • +
      • Ajout des boutons des menus d'action
      • +
      • Améliorations visuelles diverses (dont support du darkmode dans le chat)
      • +
      +
      +
    • + +
    • +
      + + Version 1.1.0 (2025-01-05) + +
        +
      • Ajout de la gestion de compagnons
      • +
      • Ajout d'options d'attaque pour le script COFantasy2
      • +
      • Ajout de l'onglet Script et des options d'attaque
      • +
      +
      +
    • + +
    • +
      + + Version 1.0.0 (2024-12-24) + +
        +
      • Première version de la fiche
      • +
      +
      +
    • + +
    + +
    + +
    + +

    Migrations

    +
    + Détail des mises à jour + +
    + +
    + +
    +
    +
    @@ -7935,7 +8243,7 @@ /** * COF2e Sheet Worker - * v1.13.0 + * v1.14.0 */ const SWData = { @@ -7977,6 +8285,16 @@ const SWData = { bar4_link: "vitesse_cases", }, NPC: { + bar1: "", + bar1_link: "", + bar2: "", + bar2_link: "def", + bar3: "", + bar3_link: "pv", + bar4: "", + bar4_link: "vitesse_cases" + }, + MOOK: { bar1: "", bar1_link: "", bar2: "", @@ -7995,6 +8313,12 @@ const SWData = { "actions": "🔆", // bright "skills": "💪", // flexed biceps "heal": "🧡", // red heart + "recovery": { + "fast": "🍵", // tea + "full": "🛏", // bed + }, + "red": "🔴", // red circle + "green": "🟢", // green circle }, SHEET_TYPE: { PC: "pc", @@ -8441,7 +8765,7 @@ const SWData = { "atktir", "atkmag", "def", - "dm", + // "dm", "pv", "dr", "pm", @@ -8464,6 +8788,7 @@ const SWData = { { name: "inconscient", label: "Inconscient", + emoji: "😴", lowFantasy: false, effects: { }, description: [ @@ -8471,18 +8796,20 @@ const SWData = { "Perd 1 Dé de Récupération" ] }, - { + { name: "affaibli", label: "Affaibli", + emoji: "❤️‍🩹", lowFantasy: false, effects: { }, description: [ "Dé malus à tous les tests" ] }, - { + { name: "aveugle", label: "Aveuglé", + emoji: "👁", lowFantasy: false, effects: { init: -5, @@ -8494,9 +8821,10 @@ const SWData = { "Pas d'attaque magique nécessitant de voir la cible" ] }, - { + { name: "effraye", label: "Effrayé", + emoji: "😨", lowFantasy: true, effects: { atkcac: -2, @@ -8507,18 +8835,20 @@ const SWData = { "Malus -2 à tous les tests" ] }, - { + { name: "essoufle", label: "Essouflé", + emoji: "💨", lowFantasy: false, effects: { }, description: [ "Déplacement limité à 5 m par action de mouvement" ] - }, - { + }, + { name: "etourdi", label: "Etourdi", + emoji: "😝", lowFantasy: false, effects: { def: -5 @@ -8526,29 +8856,32 @@ const SWData = { description: [ "Aucune action possible" ] - }, - { + }, + { name: "immobilise", label: "Immobilisé", + emoji: "⛓", lowFantasy: false, effects: { }, description: [ "Pas de déplacement", "Dé malus aux tests d'attaque" ] - }, - { - name: "invalide", + }, + { + name: "invalide", label: "Invalide", + emoji: "🩼", lowFantasy: false, effects: { }, description: [ "Déplacement limité à 5 m par action de mouvement" ] }, - { + { name: "panique", label: "Paniqué", + emoji: "😱", lowFantasy: true, effects: { def: -2 @@ -8557,29 +8890,32 @@ const SWData = { "Malus -5 à tous les tests", "Impossible d'attaquer ou d'incanter" ] - }, - { - name: "paralyse", + }, + { + name: "paralyse", label: "Paralysé", + emoji: "🕸", lowFantasy: false, effects: { }, description: [ "Aucune action possible", "Touché et critique automatique en cas d'attaque" ] - }, - { + }, + { name: "ralenti", label: "Ralenti", + emoji: "🐌", lowFantasy: false, effects: { }, description: [ "Une seule action par round (attaque ou mouvement)" ] - }, - { + }, + { name: "renverse", label: "Renversé", + emoji: "🤸", lowFantasy: false, effects: { atkcac: -5, @@ -8590,10 +8926,11 @@ const SWData = { description: [ "Nécessite une action d'attaque pour se relever" ] - }, - { + }, + { name: "surpris", label: "Surpris", + emoji: "😲", lowFantasy: false, effects: { def: -5 @@ -8601,7 +8938,7 @@ const SWData = { description: [ "Pas d'action au premier round de combat" ] - } + } ], SKILLS: [ { @@ -9691,7 +10028,8 @@ 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}`; @@ -10310,17 +10648,29 @@ function askValue(prompt, callback) { } /** - * Send a drop-down roll query to chat and process response - * @param {string|string[]} askParams - Parameters of the roll query - * @param {(query:integer, choices:string[]) => void} callback - Callback function to process input + * Build ask roll query + * @param {string|string[]} askParams - Parameters of the roll query + * @returns {{ ask: string, choices: string[] }} */ -function ask(askParams, callback) { +function askCommand(askParams) { if (!Array.isArray(askParams)) { askParams = [ askParams, "Non", "Oui" ]; } const [ prompt, ...choices ] = askParams; const choice = choices.map((ch, ix) => `${ch},${ix + 1}`).join("|"); - const ask = `! {{ask=[[?{${prompt}|${choice}}]]}}`; + return { + ask: `! {{ask=[[?{${prompt}|${choice}}]]}}`, + choices + }; +} + +/** + * Send a drop-down roll query to chat and process response + * @param {string|string[]} askParams - Parameters of the roll query + * @param {(query:integer, choices:string[]) => void} callback - Callback function to process input + */ +function ask(askParams, callback) { + const { ask, choices } = askCommand(askParams); startRoll(ask, question => { const query = question.results.ask.result; if(query && callback) { @@ -10330,6 +10680,20 @@ function ask(askParams, callback) { }); }; +/** + * Send a drop-down roll query to chat and return response + * @async + * @param {string|string[]} askParams - Parameters of the roll query + * @returns {Promise<{ selected: number, choices: string[] }>} + */ +async function askAsync(askParams) { + const { ask, choices } = askCommand(askParams); + const question = await startRoll(ask); + const selected = question.results.ask.result; + finishRoll(question.rollId); + return ({ selected, choices }); +} + /** * Extract string data from CRP roll object * @param {object} rollInfo - startRoll roll object @@ -10532,7 +10896,6 @@ function playFX(fxValue) { fxChat += "\n" + fxCmd; } }); - if (fxChat !== "") sendChatCmd(fxChat); } @@ -10903,7 +11266,7 @@ function replaceDevol(expr) { /** * Change identity heading */ -on("change:sheet_type", function() { +on("change:sheet_type", function(eventInfo) { getAttrs([ "sheet_type" ], function(values) { const sheetType = strval(values.sheet_type); if (!sheetType) @@ -10917,8 +11280,12 @@ on("change:sheet_type", function() { } }); - if (sheetType === SWData.SHEET_TYPE.NPC) - setAttrs({ atkcac: 0, atkmag: 0 }); + if (sheetType === SWData.SHEET_TYPE.NPC) { + let updates = { atkcac: 0, atkmag: 0 }; + if (strval(eventInfo.previousValue) === SWData.SHEET_TYPE.PC) + updates = { ...updates, pc: 0, pc_max: 0 }; + setAttrs(updates); + } }); }); @@ -10948,7 +11315,8 @@ on("change:cfg_d20roll", function() { [ "pc_main", "pc_abilities", "pc_gears", "pc_notes", "pc_config", "pc_script", "pc_version", - "npc_main", "npc_gears", "npc_config", "npc_script" + "npc_main", "npc_gears", + "npc_config", "npc_script", "npc_version", ].forEach(button => { on(`clicked:${button}-btn`, function() { const [ type, value ] = button.split("_"); @@ -11107,7 +11475,7 @@ on("change:peuple", function(eventInfo) { getAttrs([ "peuple", ...rankNames ], function(values) { const updates = new Map(); const people = strval(values.peuple); - + if (people.trim() === "") { updates.set(abilityInfo({ path }).pathName, ""); // reset abilities @@ -11129,7 +11497,8 @@ on("change:peuple", function(eventInfo) { const peopleInfo = SWData.HERITAGE.find(p => p.name.includes(cleanText(people))); if (!peopleInfo) return; - + + updates.set(abilityInfo({ path }).pathName, people); rankNames.forEach((rank, ix) => { if (strval(values[rank]) === "") updates.set(abilityInfo({ path, rank: ix + 1 }).rankName, capitalize(peopleInfo.abilities[ix])); @@ -11225,7 +11594,10 @@ on(changeEvent([ let updateAttrs = new Map(); // reset PC - if (intval(eventInfo.newValue) === intval(eventInfo.previousValue) + 1) { + if ( + strval(eventInfo.sourceAttribute) === "niveau" && + intval(eventInfo.newValue) === intval(eventInfo.previousValue) + 1 + ) { const pcMax = intval(values.pc_max); if (intval(values.pc) < pcMax) updateAttrs.set("pc", pcMax); @@ -11808,6 +12180,7 @@ SWData.PC.STATS.forEach(stat => { sendChatCmd(actions[0]); return; } + ask([ "Jet ?", short.toUpperCase(), ...rolls, ...skills ], (selected) => { sendChatCmd(actions[selected - 1]); }); @@ -11827,7 +12200,8 @@ on("clicked:init-btn", function () { "per", "agi", "casque_eqp", "casque", - "pnj_tactique" + "pnj_tactique", + "pnj_tactique_tip" ], function(values) { let initMod = ""; if (boolval(values.cfg_init_mod)) @@ -11836,7 +12210,9 @@ on("clicked:init-btn", function () { const isNPC = strval(values.sheet_type) === SWData.SHEET_TYPE.NPC; if (!isNPC) helmet = helmetMalus(values); - const tactique = isNPC ? strval(values.pnj_tactique) : ""; + const tactique = isNPC && !boolval(values.pnj_tactique_tip) + ? strval(values.pnj_tactique) + : ""; const initBonus = isNPC ? " + ?{Bonus ?|0}[Bonus]" : ""; const roll = `[[@{init}[Initiative]${helmet}${initBonus} + @{cfg_init_variable}[Dé(s)]${initMod} &{tracker} ]]`; const chatMsg = rollTemplate({ @@ -12111,7 +12487,7 @@ on("clicked:vigloss-btn clicked:vigheal-btn", function (eventInfo) { on("change:pv change:pvig", function (eventInfo) { const PV = strval(eventInfo.triggerName) === "pvig" ? "PVIG" : "pv"; getAttrs([ - "sheet_type", "character_name", + "sheet_type", "character_name", "tokenmod", "token_tint_on", "token_tint", "tkmarker_dead_on", "tkmarker_dead", "pv", "pv_max", "PVIG" ], function (values) { @@ -12120,7 +12496,7 @@ on("change:pv change:pvig", function (eventInfo) { const tokenmod = boolval(values.tokenmod); const markerDead = boolval(values.tkmarker_dead_on) ? strval(values.tkmarker_dead).trim() : ""; const wounds = wounded(values); - + const tokenProps = {}; if (tokenTint) { const tokenTintColor = strval(values.token_tint).split("").every(ch => ch.match(/[0-9A-Fa-f]/)) @@ -12143,10 +12519,6 @@ on("change:pv change:pvig", function (eventInfo) { }); }); -function recovery(typeRecup) { - -} - /** * On Recup button click */ @@ -12279,7 +12651,10 @@ on("clicked:drecup-btn clicked:recovery_fast-btn clicked:recovery_full-btn", fun return; } - const recups = [ "Rapide", "Complète" ]; + const recups = [ + SWData.emojis.recovery.fast + " Rapide", + SWData.emojis.recovery.full + " Complète ", + ]; if (effort !== "") recups.push(effort); @@ -12722,6 +13097,7 @@ function insertRolls(text, options = {}) { /** * Numbering PC attacks + * Detect weapon name and get properties */ on("change:repeating_armes:arme-nom clicked:renum_atk-btn", function(eventInfo) { const renumber = (eventInfo.triggerName === "clicked:renum_atk-btn"); @@ -12730,7 +13106,11 @@ on("change:repeating_armes:arme-nom clicked:renum_atk-btn", function(eventInfo) atkId = strval(eventInfo.triggerName.split("_")[2]); getSectionIDsOrdered("repeating_armes", function (rowIds) { const labels = rowIds.map(id => rptAttr("armes", "arme-label", id)); - getAttrs([ "character_name", "max_attack_label", "scriptVersion", ...labels ], function (values) { + getAttrs([ + "character_name", "max_attack_label", "scriptVersion", + ...rptAttrs("armes", [ "arme-nom", "arme-gearid" ]), + ...labels + ], function (values) { const updates = new Map(); if (!renumber) { @@ -12751,6 +13131,51 @@ on("change:repeating_armes:arme-nom clicked:renum_atk-btn", function(eventInfo) if (renumber || numLabel > maxLabel) updates.set("max_attack_label", numLabel); + const gearId = strval(values[rptAttr("armes", "arme-gearid")]); + if (gearId === "") { + const atkName = cleanText(strval(values[rptAttr("armes", "arme-nom")])); + const weapon = SWData.GEARS.find(gear => { + if (atkName.includes(cleanText(gear.name))) + return true; + if (gear.altname !== "") { + for (const altn of gear.altname.split(",")) { + if (atkName.includes(altn)) + return true; + } + } + return false; + }); + if (weapon) { + updates.set(rptAttr("armes", "arme-atk"), "atkcac"); + let dm1m = "", + dm2m = "", + dm = ""; + weapon.props.forEach(prop => { + const { name, value } = parseNameValue(prop); + if (name === "dm-1m") + dm1m = value; + if (name === "dm-2m") + dm2m = value; + if (name === "portee") { + updates.set(rptAttr("armes", "arme-portee"), value); + updates.set(rptAttr("armes", "arme-atk"), "atktir"); + updates.set(rptAttr("armes", "arme-bonus"), "0"); + } + if (name === "critique") + updates.set(rptAttr("armes", "arme-crit"), value); + if (name === "type-dm") + updates.set(rptAttr("armes", "arme-dmtype"), value); + }); + if (dm1m !== "" && dm2m !== "") + dm = dm1m + "/" + dm2m; + else + dm = dm1m + dm2m; + updates.set(rptAttr("armes", "arme-dm"), dm); + if (dm1m === "" && dm2m !== "") + updates.set(rptAttr("armes", "arme-2m"), 1); + } + } + if (updates.size > 0) setAttrs(Object.fromEntries(updates)); }); @@ -12804,9 +13229,12 @@ on("clicked:repeating_armes:ongoing-btn", function() { let name = strval(values[rptAttr("armes", "etendu-nom")]); if (name === "") name = strval(values[rptName]); + let duree = strval(values[rptAttr("armes", "etendu-duree")]); + if (!duree.match(/[0-9]+d[0-9]+/)) + duree = insertRolls(" " + duree + " ").trim(); let stop = strval(values[rptAttr("armes", "etendu-stop")]); - if (stop === "") - stop = "Pendant " + strval(values[rptAttr("armes", "etendu-duree")]); + if (stop === "" && duree !== "") + stop = "Pendant " + duree; const text = [ effect, stop ].filter(v => v !== "").join("\n"); // send to chat @@ -12949,11 +13377,20 @@ function maneuversRollQuery(cha) { /** * Roll an attack in chat */ -on("clicked:repeating_armes:attack-btn", function(eventInfo) { +on("clicked:repeating_armes:attack-btn", async function(eventInfo) { const { rowId: attackId } = rptInfo(eventInfo.triggerName); const section = "repeating_armes_arme-"; const rptThrown = "repeating_armes_jet-"; const rptOngoing = "repeating_armes_etendu-"; + const tacticIds = await getSectionIDsAsync("repeating_tactic"); + const rptTacticAll = []; + tacticIds.forEach(id => { + rptTacticAll.push(...rptAttrs( + "tactic", + [ "on", "name", "div", "roll", "dmdiv" ].map(attr => `tact-${attr}`), + id) + ); + }); const attackAttrs = [ "nom", "atk", "atkdiv", "crit", "2m", "dm", "dmtype", "bonus", "dmdiv", @@ -12969,7 +13406,7 @@ on("clicked:repeating_armes:attack-btn", function(eventInfo) { getAttrs([ "scriptVersion", "character_name", - "cfg_1attack_roll", "cfg_cible", + "cfg_1attack_roll", "cfg_cible", "cfg_optact", "cfg_d20roll", "d20roll", "cfg_crit_diff", "cfg_tbl_crit", "cfg_tbl_fumble", "cfg_use_ammo", "cfg_usure", @@ -12980,14 +13417,14 @@ on("clicked:repeating_armes:attack-btn", function(eventInfo) { "condition_affaibli", "condition_immobilise", ...fearConditionsAttrs(), "tactique", - "tactautre", "tactroll", "tactname", "tactdiv", "tactdmdiv", + ...rptTacticAll, "noms_voies", "rangs_voies", "en_selle", ...luckButtonAttrs(), ...sitModAttrs.map(attr => `modsit_${attr}`), ...attackAttrs.map(attr => rptAttr("armes", `arme-${attr}`)), ...[ "resid", "nbre" ].map(attr => rptAttr("armes", `jet-${attr}`)), - "dm_buff_list", "allattacks_buff", "allattacks_desc", + "allattacks_buff", "allattacks_desc", ...ongoingAttrs().map(attr => rptAttr("armes", `etendu-${attr}`)), ], function (values) { @@ -13000,11 +13437,7 @@ on("clicked:repeating_armes:attack-btn", function(eventInfo) { const useWear = boolval(values.cfg_usure); const shieldUsed = boolval(values.bouclier_eqp); const tactics = intval(values.tactique); - const tactOther = boolval(values.tactautre); - const tactName = strval(values.tactname, "Option tactique"); - const tactBM = intval(values.tactdiv); - const tactRoll = intval(values.tactroll); - const tactDM = strval(values.tactdmdiv); + const tactRound = boolval(values.cfg_optact); const weakened = intval(values.condition_affaibli); const restrained = intval(values.condition_immobilise); const pathNames = getPathNames(strval(values.noms_voies)); @@ -13025,14 +13458,13 @@ on("clicked:repeating_armes:attack-btn", function(eventInfo) { let range = (portee !== "") ? ` *(${ rangeWithUnits(portee) })*` : ""; let crit = intval(values[section + "crit"], 20); const atkType = strval(values[section + "atktype"]); - const modifiers = strval(values[section + "atkmods"]).split(",").map(mod => mod.trim().toLowerCase()); + let modifiers = strval(values[section + "atkmods"]).split(",").map(mod => mod.trim().toLowerCase()); const options = strval(values[section + "options"]); const usure = strval(values[section + "usure"]); const resourceId = strval(values[`${rptThrown}resid`]); const resourceNbr = Math.max(1, intval(values[`${rptThrown}nbre`])); - // DM + all attacks buffs - const dmBuffs = strval(values.dm_buff_list).split(";").map(b => b.trim()); + // all attacks buffs const allAtkBuffs = allRollsBuff(values, "attacks"); // 2 mains & bouclier @@ -13099,16 +13531,31 @@ on("clicked:repeating_armes:attack-btn", function(eventInfo) { extraDM.push(` + [[2@{devol}]] ${atkTitle}`); break; } - if (tactOther) { + + tacticIds.forEach(id => { + if (!boolval(values[rptAttr("tactic", "tact-on", id)])) + return; + const tactName = strval(values[rptAttr("tactic", "tact-name", id)]); + const tactBM = intval(values[rptAttr("tactic", "tact-div", id)]); if (tactBM !== 0) - tacticBonus += ` + (${tactBM})[${tactName}]`; + tacticBonus += ` + (${tactBM})[${tactName}]`; + const tactDM = strval(values[rptAttr("tactic", "tact-dmdiv", id)]); if (tactDM !== "") - extraDM.push(` + ([[${ replaceDevol(tactDM) }]]) ${tactName}`); - if (tactRoll === 1) - modifiers.push("debonus"); - if (tactRoll === 2) - modifiers.push("demalus"); - } + extraDM.push(` + [[${ replaceDevol(tactDM) }]] ${tactName}`); + const tactRoll = intval(values[rptAttr("tactic", "tact-roll", id)]); + if (tactRoll === 1) { + if (modifiers.includes("demalus")) + modifiers = modifiers.filter(m => m !== "demalus"); + else + modifiers.push("debonus"); + } + if (tactRoll === 2) { + if (modifiers.includes("debonus")) + modifiers = modifiers.filter(m => m !== "debonus"); + else + modifiers.push("demalus"); + } + }); // Build situational modifiers let modSit = ""; @@ -13247,32 +13694,6 @@ on("clicked:repeating_armes:attack-btn", function(eventInfo) { dmAbilities +=` + ${powderBonus}[Poudre puissante]`; } - // DM buffs - dmBuffs.filter(b => b !== "").forEach(buff => { - let [ buffName, buffDM ] = buff.split(":").map(s => s.replaceAll("|", " ").trim()); - buffDM = replaceDevol(buffDM); - if (buffName.charAt(0) === "?") { - buffName = buffName.slice(1).trim(); - const delim = "&"; - let moreDM = buffDM; - buffDM = buffDM.split(delim)[0]; - let buffMore = ""; - while (moreDM.includes(delim)) { - const [ dm, ...dms ] = moreDM.split(delim).slice(1); - const [ label, value ] = dm.split("=").map(v => v.trim()); - buffMore += `|${label},${value}`; - moreDM = [ ...dms ].join(delim); - } - buffDM = `?{${buffName} (DM)|Non,0|Oui,${buffDM}${buffMore}}`; - } - const matched = buffDM.match(/\[(.+)\]/); - if (matched) { - const [ match, name ] = matched; - buffDM = buffDM.replace(match, `@{${ name.toLowerCase() }}`); - } - extraDM.push(` + [[${buffDM}]] ${buffName}`); - }); - let dmroll = ""; if (dm !== "" ) dmroll = `${dm}${dmOpt} + ${dmbonus}[Bonus]${magicDM}${ shadow.dm } + ${dmdiv}[Divers]${dmAbilities}`; @@ -13422,6 +13843,14 @@ on("clicked:repeating_armes:attack-btn", function(eventInfo) { if (tactics > 0) setAttrs({ tactique: "0" }); + if (tactRound) { + const reset = new Map(); + tacticIds.map(id => rptAttr("tactic", "tact-on", id)).forEach(attr => { + reset.set(attr, 0); + }); + if (reset.size > 0) + setAttrs(Object.fromEntries(reset)); + } if (useAmmo && resourceId !== "" && resourceNbr > 0) { const resVal = rptAttr("ressources", "res-val", resourceId); @@ -13434,10 +13863,10 @@ on("clicked:repeating_armes:attack-btn", function(eventInfo) { const ongoingPrms = ongoingAttrs().map(attr => strval(values[rptOngoing + attr])); if(ongoingPrms.some(v => v !== "")) { const [ ogEff, ogName, ogTime ] = ongoingPrms; - let effectName = name; + let effectName = name + " " + ogEff; if (ogName !== "") effectName = ogName; - effectName += " " + ogEff; + //effectName += " " + ogEff; let timeSpan = ""; if (ogTime !== "") timeSpan = insertRolls(" " + ogTime + " ").trim(); @@ -13448,6 +13877,22 @@ on("clicked:repeating_armes:attack-btn", function(eventInfo) { }); }); +/** + * Display tactical option description + */ +on("change:tactique", function() { + getAttrs([ "tactique" ], function(values) { + const tactique = numval(values.tactique); + const tactdesc = [ + "", + "+5 en attaque, DM divisés par 2", + "-3 en attaque, +1d4° DM", + "-7 en attaque, +2d4° DM", + ][tactique]; + setAttrs({ tactdesc }); + }); +}); + /** * Check usure */ @@ -13474,7 +13919,9 @@ on("change:usure_check", function() { } } } - catch (e) {} + catch (e) { + console.error(e.message + "@change:usure_check"); + } finally { updates.set("usure_check", ""); setAttrs(Object.fromEntries(updates)); @@ -13671,7 +14118,7 @@ function actionAbility(path, rank) { ...abilityAttrs, "character_name", "pm", "concentration", "rangs_voies", - ], function(values) { + ], async function(values) { const [ name, ability, desc, owned, spell, used, uses, freq, props, br ] = Object.keys(values); const pathName = strval(values[name]); let abilityName = strval(values[ability]); @@ -13772,10 +14219,10 @@ function actionAbility(path, rank) { // check ongoing effect let ongoing = strval(getProp(abilityProps, "etendu")); if (ongoing) { - let effectName = strval(getProp(abilityProps, "etendu-nom")); - if (effectName === "") - effectName = abilityName; - effectName += " " + ongoing; + let effectName = abilityName + " " + ongoing; + const ogName = strval(getProp(abilityProps, "etendu-nom")); + if (ogName !== "") + effectName = ogName; let timeSpan = strval(getProp(abilityProps, "etendu-duree")); if (timeSpan !== "") timeSpan = insertRolls(" " + timeSpan + " ").trim(); @@ -13796,13 +14243,24 @@ function actionAbility(path, rank) { { atk: "atktir", alias: "distance" }, { atk: "atkmag", alias: "magie" } ]; - let roll = getProp(abilityProps, "roll"); + let roll = getProp(abilityProps, "roll").trim(); search.forEach(s => { if (cleanText(roll).includes(s.alias)) roll = s.atk; if (roll === s.atk) roll += "-btn"; }); + roll = roll.replaceAll("%{", "").replaceAll("}", ""); + if (roll.startsWith("repeating_")) { + let [ ...rptRoll ] = roll.split("_"); + if (rptRoll[2].charAt(0) === "$") + rptRoll[2] = strval(intval(rptRoll[2].slice(1)) + 1); + if (intval(rptRoll[2]) !== 0) { + const rowIds = await getSectionIDsOrderedAsync(rptRoll[1]); + rptRoll[2] = rowIds[intval(rptRoll[2]) - 1]; + roll = rptRoll.join("_"); + } + } nextRoll = `%{${charName}|${roll}}`; } @@ -13845,13 +14303,16 @@ function ongoingEffect(path, rank) { const [ kpath, kname, kprops ] = Object.keys(values); const pathName = strval(values[kpath]); const props = strval(values[kprops]); - const effect = insertRolls(" " + getProp(props, "etendu") + " ").trim(); + const effect = insertRolls(" " + strval(getProp(props, "etendu")) + " ").trim(); let name = strval(getProp(props, "etendu-nom")); if (name === "") name = strval(values[kname]); + let duree = strval(getProp(props, "etendu-duree")); + if (!duree.match(/[0-9]+d[0-9]+/)) + duree = insertRolls(" " + duree + " ").trim(); let stop = strval(getProp(props, "etendu-stop")); - if (stop === "") - stop = "Pendant " + strval(getProp(props, "etendu-duree")); + if (stop === "" && duree !== "") + stop = "Pendant " + duree; const text = [ effect, stop ].join("\n"); // send to chat @@ -13909,7 +14370,6 @@ function updateAllRanks(path) { } /** - * Update the voieN_buff_xxx attribute value based on rank * Update the buffs_voies attribute with the list of buff attribute names * @param {number} path - path number changed */ @@ -13919,19 +14379,24 @@ function updateBuffsByRank(path) { const rank = intval(values[`rang_voie${path}`]); rankProps.filter(a => strval(values[a]) !== "").forEach(props => { values[props].split("\n").filter(prop => prop.startsWith("buff:")).forEach(prop => { - const value = prop.split(":")[1].replace(/\s+/g, " "); - let [ attr, ...buffs ] = value.split(" "); + const propValue = prop.split(":")[1].replace(/\s+/g, " "); + let [ attr, ...buffs ] = propValue.trim().split(" "); if (attr === "") return; - buffs = buffs.join(" "); + buffs = buffs.join(" ") + .replaceAll("[", '["') + .replaceAll(",", '","') + .replaceAll("]", '"]'); try { const byRank = JSON.parse(buffs.trim()); const buffAttr = `voie${path}_buff_${ attr.trim() }`; - const buffByRank = rank > 0 ? byRank[rank - 1] : 0; + const buffByRank = rank > 0 ? byRank[rank - 1] : ""; const buffs_voies = JSON.parse(strval(values.buffs_voies, "{}")); buffs_voies[buffAttr] = buffByRank; setAttrs({ buffs_voies: JSON.stringify(buffs_voies) }); - } catch (e) { } + } catch (e) { + console.error(e.message + " @ updateBuffsByRank()"); + } }); }); }); @@ -14169,6 +14634,11 @@ seq(9).forEach(path => { setAttrs({ [`${ability}-btn`]: `%{${charName}|${ability}-btn}` }); }); }); + + on(`change:${ abilityInfo(ability).rankProps }`, function () { + updateBuffsByRank(path); + }); + }); // Update all ranks attribute @@ -14192,6 +14662,22 @@ seq(9).forEach(path => { }); }); +/** + * On change of buffs_voies + * Create custom attributes + */ +on("change:buffs_voies", function() { + getAttrs([ "buffs_voies" ], function (values) { + const buffs = objval(strval(values.buffs_voies)); + const updates = new Map(); + Object.entries(buffs).forEach(([ attribute, value ]) => { + updates.set(attribute, value); + }); + if (updates.size > 0) + setAttrs(Object.fromEntries(updates)); + }); +}); + /** * On click on concentration button */ @@ -14319,6 +14805,32 @@ on("clicked:reset_uses-btn", function() { }); }); +/** + * On click of NPC reset abilities usage button + */ +on("clicked:reset_npcuses-btn", async function() { + const rptRolls = "npcrolls"; + const rollIds = await getSectionIDsAsync("repeating_" + rptRolls); + if (rollIds.length === 0) + return; + const { selected } = await askAsync([ "Capacités ?", "Par combat", "Par jour" ]); + const daily = (selected === 2); + const allFreqs = rollIds.map(id => rptAttr(rptRolls, "npcroll-freq", id)); + getAttrs(allFreqs, function (values) { + const updates = new Map(); + rollIds.forEach(id => { + const freq = strval(values[rptAttr(rptRolls, "npcroll-freq", id)]); + const uses = rptAttr(rptRolls, "npcroll-uses", id); + if (freq.toLowerCase() === "combat") + updates.set(uses, 0); + if (daily && freq.toLowerCase() === "jour") + updates.set(uses, 0); + }); + if (updates.size > 0) + setAttrs(Object.fromEntries(updates)); + }); +}); + /** * Scan abilities for usage in description * @param {number} [path=0] - search usages in a single path @@ -14560,6 +15072,21 @@ SWData.PC.SKILLS.forEach(skill => { let events = `clicked:skill-${skill.name}`; if (Array.isArray(skill.ability)) events += " " + eventList("clicked", skill.ability.map(ability => `skill-${skill.name}-${ability}`), " "); + + const skillBonusAttr = `${skill.name}_bonus`; + on("change:" + skillBonusAttr, function() { + getAttrs([ "character_name", "niveau", skillBonusAttr ], function(values) { + const charName = strval(values.character_name); + const level = intval(values.niveau); + const skillMax = Math.min(level + 5, 15); + let skillBonus = intval(values[skillBonusAttr]); + if (skillBonus > skillMax) { + sendChatCmd(`/w "${charName}" Le bonus de compétence est limité à **${skillMax}**`); + setAttrs({ [skillBonusAttr]: skillMax }); + } + }); + }); + on(events, function (eventInfo) { const useCarac = strval(eventInfo.triggerName.split("-")[2]); let skillName = getSkillName(skill); @@ -14680,6 +15207,7 @@ SWData.PC.SKILLS.forEach(skill => { setAttrs(Object.fromEntries(updates)); }); }); + }); /** @@ -14745,9 +15273,13 @@ function parseBuffList(attribute) { const buffList = strval(values[buff]).split("; "); const allRanks = getPathRanks(values.rangs_voies); let pathBuffs; + let pathBuffList = []; try { pathBuffs = JSON.parse(strval(values.buffs_voies, "{}")); - } catch (e) { } + pathBuffList = Object.keys(pathBuffs).map(k => `[${k}]`); + } catch (e) { + console.error(`${e.message} @ parseBuffList(${attribute})`); + }; let buffTotal = 0; buffList.forEach(buff => { buff = buff.trim(); @@ -14755,7 +15287,7 @@ function parseBuffList(attribute) { return; let value = 0; let expression = strval(buff.split(":")[1]).trim(); - if (pathBuffs && Object.keys(pathBuffs).map(k => `[${k}]`).includes(expression)) { + if (pathBuffs && pathBuffList.includes(expression)) { expression = expression.replace("[", "").replace("]", ""); value = pathBuffs[expression] || 0; } else { @@ -14856,6 +15388,30 @@ on("clicked:multibuff-btn", function () { }); }); +/** + * On click on the abilities props help button + */ +on("clicked:helprops-btn", function() { + const props = [ + { name: "fx", text: "Animation", url: "animation-fx" }, + { name: "pm", text: "Pts Mana", url: "points-de-mana-pm" }, + { name: "selonRang", text: "Effet selon rang", url: "effet-selon-le-rang-selonrang" }, + { name: "epic", text: "Capacité épique", url: "capacit-pique-epic" }, + { name: "buff", text: "Buff selon rang", url: "buff-selon-rang-buff" }, + { name: "roll", text: "Jet/Bouton lié", url: "jet-roll" }, + { name: "action", text: "Menu des actions", url: "menu-des-actions-action" }, + { name: "etendu", text: "Effets prolongés", url: "effets-tendus-etendu" }, + ].map(p => `${ p.name }:=[${ p.text }](https://stephaned68.github.io/COF2e/pc-abilities#${ p.url })`); + sendChatMsg([ + "title=Propriétés des capacités", + ...props, + "desc=Une propriété par ligne" + ], { + template: "custom", + whisper: "gm" + }); +}); + /** * On click on the buffs syntax help button */ @@ -15106,43 +15662,60 @@ function encumbrance() { /** * On click on the gear props help button */ -on("clicked:helpgear-btn", function() { - ask([ +on("clicked:helpgear-btn", async function() { + const { selected, choices } = await askAsync([ "Type ?", "Armes", "Défense", "Armes magiques" - ], function(selected, choices) { - if (selected === 0) - return; - sendChatMsg([ - "title=Propriétés d'équipement", - `subtitle=Liste des codes et valeurs (${choices[selected - 1]})`, - selected === 1 ? "dm-1m:=Dés de DM à une main" : "", - selected === 1 ? "dm-2m:=Dés de DM à deux mains" : "", - selected === 1 ? "type-dm:=Types de dommages" : "", - selected === 1 ? "critique:=Seuil de critique" : "", - selected === 1 ? "portee:=Portée arme à distance" : "", - selected === 1 ? "usure:=Seuil d'usure" : "", - selected === 2 ? "bonus-def:=Bonus DEF de l'armure ou du bouclier" : "", - selected === 2 ? "armure:=Bonus DEF de l'armure" : "", - selected === 2 ? "armure-malus:=Malus d'armure portée" : "", - selected === 2 ? "agi-max:=Bonus d'AGI maximum" : "", - selected === 2 ? "bouclier:=Bonus DEF du bouclier" : "", - selected === 2 ? "bouclier-malus:=Malus de bouclier porté" : "", - selected === 2 ? "casque:=RD du casque contre les DM critiques" : "", - selected === 2 ? "casque-malus:=Malus de PER et Init du casque porté" : "", - selected === 3 ? "magie:=Bonus magique pour toucher et DM" : "", - selected === 3 ? "magieAtt:=Bonus magique pour toucher" : "", - selected === 3 ? "magieDmg:=Bonus magique aux DM" : "", - selected === 3 ? "affutee:=Arme affûtée" : "", - selected === 3 ? "element:=Nom de l'élément (+' intense')" : "", - selected === 3 ? "fleau:=Nom du type de créature" : "", - "desc=Une propriété par ligne" - ].filter(t => t !== ""), { - template: "custom", - whisper: "gm" - }); + ]); + if (selected === 0) + return; + + sendChatMsg([ + "title=Propriétés d'équipement", + `subtitle=Liste des codes et valeurs (${choices[selected - 1]})`, + selected === 1 ? "dm-1m:=Dés de DM à une main" : "", + selected === 1 ? "dm-2m:=Dés de DM à deux mains" : "", + selected === 1 ? "type-dm:=Types de dommages" : "", + selected === 1 ? "critique:=Seuil de critique" : "", + selected === 1 ? "portee:=Portée arme à distance" : "", + selected === 1 ? "usure:=Seuil d'usure" : "", + selected === 2 ? "bonus-def:=Bonus DEF de l'armure ou du bouclier" : "", + selected === 2 ? "armure:=Bonus DEF de l'armure" : "", + selected === 2 ? "armure-malus:=Malus d'armure portée" : "", + selected === 2 ? "agi-max:=Bonus d'AGI maximum" : "", + selected === 2 ? "bouclier:=Bonus DEF du bouclier" : "", + selected === 2 ? "bouclier-malus:=Malus de bouclier porté" : "", + selected === 2 ? "casque:=RD du casque contre les DM critiques" : "", + selected === 2 ? "casque-malus:=Malus de PER et Init du casque porté" : "", + selected === 3 ? "magie:=Bonus magique pour toucher et DM" : "", + selected === 3 ? "magieAtt:=Bonus magique pour toucher" : "", + selected === 3 ? "magieDmg:=Bonus magique aux DM" : "", + selected === 3 ? "affutee:=Arme affûtée" : "", + selected === 3 ? "element:=Nom de l'élément (+' intense')" : "", + selected === 3 ? "fleau:=Nom du type de créature" : "", + "desc=Une propriété par ligne" + ].filter(t => t !== ""), { + template: "custom", + whisper: "gm" + }); +}); + +/** + * On click on the resource props help button + */ +on("clicked:helpres-btn", function() { + sendChatMsg([ + "title=Propriétés des ressources", + "subtitle=Liste des codes et valeurs", + "type: munition=Pour décompter une munition par le nom de la ressource", + "type: soins=Potion de soins *standard* (1d4° PV)", + "type: soins NdX=Potion de soins (NdX PV)", + "desc=Une propriété par ligne" + ].filter(t => t !== ""), { + template: "custom", + whisper: "gm" }); }); @@ -15612,7 +16185,7 @@ on("remove:repeating_equipement", function (eventInfo) { }); /** - * Handle attacks + * Handle linked attacks */ on(changeEvent([ "props", @@ -15785,14 +16358,13 @@ function loadBaseGear(profile, attacks = false) { * Base gear for profile */ on("clicked:profequip-btn", function() { - getAttrs(["profil"], function(values) { + getAttrs(["profil"], async function(values) { const [ profile ] = strval(values.profil).split("/"); if (profile === "") return; - ask(`Ajouter l'équipement de base pour ${profile} ?`, function(answer) { - if (answer === 2) + const { selected } = await askAsync(`Ajouter l'équipement de base du profil (${profile}) ?`); + if (selected === 2) loadBaseGear(profile); - }); }); }); @@ -16027,6 +16599,7 @@ on("clicked:def_config-btn", function() { "cfg_pvmax_auto", "cfg_pts_capas", "cfg_cible", + "cfg_optact", "voies_456", "voies_789", "use_skills", @@ -16073,6 +16646,8 @@ on("clicked:def_config-btn", function() { "tkmarker_cond", "chatsetattr", "$tresor_monnaie", + "pnj_tactique_tip", + "pnj_notes_tip", ]; getAttrs([ "character_name", "sheet_type", ...allConfigs ], function(values) { const sheetBase = strval(values.sheet_type) === SWData.SHEET_TYPE.PC ? "PJBase" : "PNJBase"; @@ -16113,7 +16688,7 @@ on("clicked:def_config-btn", function() { /** * On clicking the Reset button */ -on("clicked:reset_all-btn", function() { +on("clicked:reset_all-btn", async function() { const e = { triggerName: "RESET" }; updateDRMax(e); updatePMMax(e); @@ -16124,14 +16699,13 @@ on("clicked:reset_all-btn", function() { abilityUsages(e); encumbrance(e); rebuildRanks(e); - updateAbilityList(e) + updateAbilityList(e); rebuildBuffLists(e); rebuildBuffValues(e); rebuildCustom(e); - ask("Lier attaques et équipements ?", function(answer) { - if (answer === 2) - linkAttacks(); - }); + const { selected } = await askAsync("Lier attaques et équipements ?"); + if (selected === 2) + linkAttacks(); }); /** @@ -16192,7 +16766,7 @@ function rebuildCustom() { const { name, value } = parseNameValue(row); if (!name) return; - updates.set(`${name}`, replaceDevol(value)); + updates.set(name.replace(" ", "-"), replaceDevol(value)); }); if (updates.size > 0) @@ -16224,18 +16798,57 @@ nameSeq(4, "custom#").forEach(custom => { }); }); -on("change:pense_bete change:tokenmod change:token_tooltip_on", function() { - getAttrs([ "character_name", "pense_bete", "tokenmod", "token_tooltip_on" ], function(values) { +/** + * Return the list of attributes for setting reminder as token tooltip + * @returns {string[]} + */ +function toolTipAttrs() { + return [ + "tokenmod", + "token_tooltip_on", + "pense_bete", + "pnj_tactique_tip", + "pnj_tactique", + "pnj_notes_tip", + "notes" + ]; +} + +on("sheet:opened " + eventList("change", toolTipAttrs(), " "), function() { + getAttrs([ "character_name", "sheet_type", ...toolTipAttrs() ], function(values) { const tokenmod = boolval(values.tokenmod); if (!tokenmod) return; - const charName = strval(values.character_name).trim(); - const reminder = strval(values.pense_bete).trim().split("\n") - .map((r, ix) => `${ix + 1}) ${r}`).join("; "); const tokenTip = boolval(values.token_tooltip_on); - if (!tokenTip || charName === "" || reminder === "") + const charName = strval(values.character_name).trim(); + if (!tokenTip || charName === "") return; - sendChatCmd(tokenMod(strval(values.character_name), { tooltip: "'" + reminder + "'" })); + const sheetType = strval(values.sheet_type); + const npcTacticTip = boolval(values.pnj_tactique_tip); + const npcNotesTip = boolval(values.pnj_notes_tip); + if (sheetType === SWData.SHEET_TYPE.NPC) + if (!npcTacticTip && !npcNotesTip) + return; + let reminder = sheetType === SWData.SHEET_TYPE.PC + ? strval(values.pense_bete) + : ""; + if (sheetType === SWData.SHEET_TYPE.NPC) { + reminder = [ + npcTacticTip ? strval(values.pnj_tactique) : "", + npcNotesTip ? strval(values.notes) : "", + ].join("\n"); + } + reminder = reminder.split("\n") + .filter(r => strval(r).trim() !== "") + .map((r, ix) => `${ix + 1}) ${r}`) + .join(" ; "); + let tooltip = reminder; + if (tooltip !== "") + tooltip = "\"" + tooltip + "\""; + let modProps = { show_tooltip: 1, tooltip, defaulttoken: "" }; + if (sheetType === SWData.SHEET_TYPE.NPC) + modProps = { ...modProps, gm_only_tooltip: 1 }; + sendChatCmd(tokenMod(charName, modProps)); }); }); @@ -16389,7 +17002,10 @@ on("change:debug_mode", function() { * On help doc button click */ on("clicked:helpdoc-btn", function() { - sendChatMsg("[Documentation](https://stephaned68.github.io/COF2e/)", { template: "" }); + getAttrs([ "sheet_type" ], function (values) { + const page = strval(values.sheet_type).toLowerCase() + "-main"; + sendChatMsg(`[Documentation](https://stephaned68.github.io/COF2e/${page})`, { template: "" }); + }) }); /** @@ -16490,7 +17106,12 @@ on("clicked:caract_select-btn", function () { const allStats = SWData.PC.STATS.map(stat => `${shorten(stat)}`); const allTests = SWData.PC.STATS.map(stat => `${shorten(stat)}_test`); const allSupH = SWData.PC.STATS.map(stat => `${shorten(stat)}_sup`); - getAttrs([ "sheet_type", "character_name", ...allStats, ...allTests, ...allSupH ], function (values) { + getAttrs([ + "sheet_type", "character_name", + ...allStats, + ...allTests, + ...allSupH + ], function (values) { const isNPC = strval(values.sheet_type) === SWData.SHEET_TYPE.NPC; const charName = strval(values.character_name); const statSelect = SWData.PC.STATS.map((stat, ix) => { @@ -16537,45 +17158,42 @@ on("clicked:caract_menu-btn", function () { }); /** - * Display conditions menu + * Return actual list of conditions for setting + * @param {boolean} lowFantasy - low fantasy option + * @returns {object[]} */ -on("clicked:etats_menu-btn", function() { - getAttrs([ - "character_name", - "couleur_pj", - "notif_conditions", - "cfg_lowfan", - ...allConditions() - ], function (values) { - const charName = strval(values.character_name); - const whisper = intval(values.notif_conditions) === 2 ? "gm" : ""; - const lowFantasy = boolval(values.cfg_lowfan); - const chatMsg = SWData.PC.CONDITIONS +function conditionList(lowFantasy) { + return SWData.PC.CONDITIONS .filter(condition => { if (condition.name === "inconscient") return; if (condition.lowFantasy && !lowFantasy) return; return condition; - }) - .map(condition => { - const color = boolval(values[`condition_${condition.name}`]) ? [ "#dc3545" ] : [ "#198754" ]; - const button = menuButton(color, {}, { - padding: "0px 5px;", - "border-radius": "50%;", - "min-width": "10px;" - }); - return `${condition.label}=[ ](~${charName}|${condition.name}-icon${button})`; }); - sendChatMsg([ - `title=${charName}`, - "subtitle=Etats préjudiciables", - "color=" + strval(values.couleur_pj), - ...chatMsg - ], { - whisper, - template: "custom" +} + +/** + * Display conditions menu + */ +on("clicked:etats_select-btn", function() { + getAttrs([ + "character_name", + "couleur_pj", + "notif_conditions", + "cfg_lowfan", + ...allConditions() + ], async function (values) { + const charName = strval(values.character_name); + const lowFantasy = boolval(values.cfg_lowfan); + const actions = []; + const conditions = conditionList(lowFantasy).map(condition => { + actions.push(`%{${charName}|${condition.name}-icon}`); + const state = boolval(values[`condition_${condition.name}`]) ? SWData.emojis.red : SWData.emojis.green; + return state + " " + condition.label + " " + condition.emoji; }); + const { selected } = await askAsync([ "Etat ?", ...conditions ]); + sendChatCmd(actions[selected - 1]); }); }); @@ -16758,11 +17376,12 @@ on("clicked:skills_menu-btn", function () { getAttrs([ "character_name", ...skillMods ], function (values) { const charName = strval(values.character_name); const skillSelect = SWData.PC.SKILLS.map((skill, ix) => { + const emoji = skill.emoji || SWData.emojis.skills; const skillLabel = getSkillName(skill); const skillStat = Array.isArray(skill.ability) ? "" : " " + skill.ability.toUpperCase(); const skillBonus = intval(values[`${ skill.name }_bonus`]); const skillMod = skillBonus > 0 ? ` (+${skillBonus})` : ""; - return `${skillLabel}${skillStat}${skillMod},${ix + 1}`; + return `${emoji} ${skillLabel}${skillStat}${skillMod},${ix + 1}`; }); ask([ "Compétence ?", ...skillSelect ], (skill) => { const skillName = SWData.PC.SKILLS[skill -1].name; @@ -16972,7 +17591,7 @@ on("clicked:pc_actions-btn clicked:npc_actions-btn", async function(eventInfo) { .filter(id => { const type = getProp(strval(values[rptAttr(resRpt, "res-props", id)]), "type"); const onHand = numval(values[rptAttr(resRpt, "res-val", id)]); - return type.toLowerCase() === "soins" && onHand > 0; + return type.split(" ")[0].toLowerCase() === "soins" && onHand > 0; }) .map(id => { const onHand = numval(values[rptAttr(resRpt, "res-val", id)]); @@ -17061,7 +17680,16 @@ function displayCheckup() { const color = strval(values.couleur_pj); const chatMsg = []; - chatMsg.push(`PV=${ intval(values.pv) } / ${ intval(values.pv_max) }`); + const buttons = [ "fast", "full" ].map(rtype => { + const title = ({ + fast: "Récupération rapide", + full: "Récupération complète", + })[rtype]; + const button = menuButtonOutline(color, {}, { padding: "0px 5px;" }, { title }); + return `[${ SWData.emojis.recovery[rtype] }](~${charName}|recovery_${rtype}-btn${button})`; + }).join(" "); + + chatMsg.push(`PV=${ intval(values.pv) } / ${ intval(values.pv_max) } ${buttons}`); chatMsg.push(`DR=${ intval(values.dr) } / ${ intval(values.dr_max) }`); chatMsg.push(`PC=${ intval(values.pc) } / ${ intval(values.pc_max) }`); if (intval(values.pm_max) > 0) @@ -17072,8 +17700,17 @@ function displayCheckup() { chatMsg.push(`Valeur d'Ombre=${shadow.value}`); const conditions = SWData.PC.CONDITIONS.filter(condition => boolval(values[`condition_${condition.name}`])); - if (conditions.length > 0) - chatMsg.push("Etats=" + conditions.map(condition => condition.label).join(", ")); + if (conditions.length > 0) { + chatMsg.push("Etats=" + conditions.map(condition => { + const button = menuButtonOutline( + color, + {}, + { padding: "0px 5px;" }, + { title: condition.label } + ); + return `[${condition.emoji}](~${charName}|${condition.name}-icon${button})`; + }).join(" ")); + } const inHand = [ holding("princ", values), holding("autre", values) ]; chatMsg.push(`En main=${ inHand.join(", ") }`); @@ -18538,7 +19175,7 @@ function migrateSheetToV1_3(sheetType, curVer = { major: 1, minor: 2, patch: 0 } 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) }`) + migration = migrateLog(migration, "1.3.0", `${base} = ${ updates.get(base) }`); }); updates.set("migration", migration); setAttrs(Object.fromEntries(updates)); @@ -18576,6 +19213,68 @@ function migrateSheetToV1_13(sheetType, curVer = { major: 1, minor: 12, patch: 0 setAttrs(Object.fromEntries(updates)); } +/** + * Migrate to Version 1.14.0 + * @param {string} sheetType - type of sheet + * @param {{ major: number, minor: number, patch: number }} curVer - semantic version parts + */ +async function migrateSheetToV1_14(sheetType, curVer = { major: 1, minor: 13, patch: 0 }) { + if (sheetType !== SWData.SHEET_TYPE.PC) + return; + + const buffIds = await getSectionIDsAsync("buffs"); + const allBuffs = []; + buffIds.forEach(id => { + allBuffs.push(...rptAttrs("buffs", [ + "buff-on", + "buff-nom", + "buff-attrib", + "buff-value" + ], id)); + }); + + getAttrs([ + "migration", + "tactautre", + "tactname", + "tactdiv", + "tactroll", + "tactdmdiv", + ...allBuffs + ], function(values) { + let migration = strval(values.migration); + const updates = new Map(); + + // extra tactic => repeating item + const tactName = strval(values.tactname); + if (tactName !== "") { + const rowId = rptAttr("tactic", "tact-", generateRowID()); + updates.set(rowId + "on", numval(values.tactautre)); + updates.set(rowId + "name", tactName); + updates.set(rowId + "div", numval(values.tactdiv)); + updates.set(rowId + "roll", numval(values.tactroll)); + updates.set(rowId + "dmdiv", strval(values.tactdmdiv)); + } + + // DM buffs => repeating items + buffIds + .filter(id => strval(values[rptAttr("buffs", "buff-attrib", id)]) === "dm") + .forEach(id => { + const rowId = rptAttr("tactic", "tact-", generateRowID()); + updates.set(rowId + "on", numval(values[rptAttr("buffs", "buff-on", id)])); + updates.set(rowId + "name", strval(values[rptAttr("buffs", "buff-nom", id)])); + updates.set(rowId + "dmdiv", strval(values[rptAttr("buffs", "buff-value", id)])); + removeRepeatingRow("repeating_buffs_" + id); + migration = migrateLog(migration, "1.14.0", `Buff DM ${ updates.get(rowId + "name") } = Option tactique`); + }); + if (migration !== strval(values.migration)) + updates.set("migration", migration); + + if (updates.size > 0) + setAttrs(Object.fromEntries(updates)); + }); +} + /** * Apply sheet migrations * @param {string} sheetType - character sheet type @@ -18592,7 +19291,10 @@ function migrateSheet(sheetType, curVer) { if (curVer.major === 1 && curVer.minor < 13) migrateSheetToV1_13(sheetType, curVer); - + + if (curVer.major === 1 && curVer.minor < 14) + migrateSheetToV1_14(sheetType, curVer); + return; } @@ -18971,11 +19673,39 @@ on("change:pvig_max", function(eventInfo) { }); }); +on("change:pv change:temp_dm", function(eventInfo) { + if (strval(eventInfo.sourceType) !== "player") + return; + getAttrs([ "character_name", "temp_dm", "pv" ], function(values) { + const pv = intval(values.pv); + const tempDM = intval(values.temp_dm); + if (tempDM < pv) + return; + const typeDM = strval(eventInfo.triggerName); + const charName = strval(values.character_name); + const display = ({ + temp_dm: { + rsub: "DM temporaires", + text: `${charName} est assommé-e (inconscient-e) !`, + textclass: "secondary", + }, + pv: { + rsub: "DM létaux", + text: `${charName} tombe à 0 PV !`, + textclass: "fumble", + } + })[typeDM]; + sendChatMsg(rollTemplate(display)); + if (typeDM === "pv") + setAttrs({ pv: 0 }, { silent: true }); + }); +}); + /** * On clicking token adjust buttons (TokenMod) */ on("clicked:token_min-btn clicked:token_max-btn clicked:token_siz-btn clicked:token_def-btn", function(eventInfo) { - getAttrs([ "sheet_type", "character_name", "pm_max", "taille" ], function(values) { + getAttrs([ "sheet_type", "mook", "character_name", "pm_max", "taille" ], function(values) { const bt = strval(eventInfo.triggerName.split("_")[1]); const resize = strval(({ "min-btn": "0.9", @@ -18983,12 +19713,14 @@ on("clicked:token_min-btn clicked:token_max-btn clicked:token_siz-btn clicked:to "siz-btn": "?{*Taille ?}", })[bt]); const isNPC = strval(values.sheet_type) === SWData.SHEET_TYPE.NPC; + const isMook = isNPC && boolval(values.mook); let props = {}; if (resize !== "") { props = { scale: "*" + resize }; } else { const tk = SWData.settings.tokens; - Object.entries(tk[strval(values.sheet_type)]).forEach(([k, v]) => { + const type = isMook ? "MOOK" : strval(values.sheet_type) + Object.entries(tk[type]).forEach(([k, v]) => { if (v !== "pm" || intval(values.pm_max) > 0) props[k] = v; });