Compare commits
17 Commits
foundryvtt
...
foundryvtt
| Author | SHA1 | Date | |
|---|---|---|---|
| d6f2feca07 | |||
| f525b6c07a | |||
| c8119601d8 | |||
| d1da169fa3 | |||
| 07928acb48 | |||
| 9860535f5f | |||
| fc560ddee7 | |||
| 47454b30f1 | |||
| 4ee45273b3 | |||
| 94ca8cb6ea | |||
| 6af8d03d22 | |||
| 7afcadfb6f | |||
| 5801a5270f | |||
| f8908617a1 | |||
| 4b36c52bb1 | |||
| 7685409a92 | |||
| 7432c4aced |
@@ -2421,6 +2421,18 @@
|
||||
"name": "Sens aiguisé",
|
||||
"tests": "Perception (Sens)"
|
||||
},
|
||||
{
|
||||
"description": "<p>L’un de vos cinq sens principaux est particulièrement développé, vous permettant de repérer ce qui échappe à d’autres. Vous pouvez effectuer des Tests de Perception pour détecter des détails normalement imperceptibles grâce au sens associé, déterminés par le MJ. Cela peut être : voir un aigle au-delà du champ visuel de tout le monde, sentir un poison presque inodore, entendre la respiration d’une souris immobile dans un mur, percevoir une lettre effacée dans une gravure, ou déceler que deux bières d’un même brasseur ont été tirées de deux tonneaux différents.</p>",
|
||||
"id": "Acute Sense (Taste)",
|
||||
"name": "Sens aiguisé (Goût)",
|
||||
"tests": "Perception (Goût)"
|
||||
},
|
||||
{
|
||||
"description": "<p>L’un de vos cinq sens principaux est particulièrement développé, vous permettant de repérer ce qui échappe à d’autres. Vous pouvez effectuer des Tests de Perception pour détecter des détails normalement imperceptibles grâce au sens associé, déterminés par le MJ. Cela peut être : voir un aigle au-delà du champ visuel de tout le monde, sentir un poison presque inodore, entendre la respiration d’une souris immobile dans un mur, percevoir une lettre effacée dans une gravure, ou déceler que deux bières d’un même brasseur ont été tirées de deux tonneaux différents.</p>",
|
||||
"id": "Acute Sense (Sight)",
|
||||
"name": "Sens aiguisé (Vue)",
|
||||
"tests": "Perception (Vue)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Votre expérience, Talent ou entraînement vous permet de manipuler de façon plus sûre les Vents de la Magie. Vous ne subissez pas d’Incantation Imparfaite si vous obtenez un double à un Test de Focalisation réussi.</p>",
|
||||
"id": "Aethyric Attunement",
|
||||
@@ -2829,6 +2841,12 @@
|
||||
"name": "Combattant au contact",
|
||||
"tests": "Corps à corps quand vous combattez au contact ou que vous allez au contact"
|
||||
},
|
||||
{
|
||||
"description": "<p>Vous êtes entraîné à vous placer très près d’un adversaire. Vous ne subissez aucune pénalité pour vous battre contre un adversaire avec une arme plus longue que la vôtre. De plus, si vous utilisez les règles optionnelles de Combat au contact (voir page 297), gagnez un bonus de +10 pour toucher votre adversaire.</p>",
|
||||
"id": "In–fighter",
|
||||
"name": "Combattant au contact",
|
||||
"tests": "Corps à corps quand vous combattez au contact ou que vous allez au contact"
|
||||
},
|
||||
{
|
||||
"description": "<p>Vos appels exaltants peuvent renverser le cours d'une bataille. Référez-vous au tableau suivant pour voir combien de personnes vous pouvez à présent influencer avec votre Compétence @Compendium[wfrp4e-core.items.oMaJZ5cvCJeOUq9H] lors d'une guerre.</p>\n<p> </p>\n<div>\n<table title=\"\" border=\"1\" summary=\"\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td>\n<p>Talent pris</p>\n</td>\n<td>\n<p>Nombre de soldats influencés</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>1</p>\n</td>\n<td>\n<p>Normal × 5</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>2</p>\n</td>\n<td>\n<p>Normal × 10</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>3</p>\n</td>\n<td>\n<p> Normal × 20</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>4</p>\n</td>\n<td>\n<p> Normal × 50</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>5</p>\n</td>\n<td>\n<p> Normal × 100</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>6</p>\n</td>\n<td>\n<p> Normal × 200</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>7</p>\n</td>\n<td>\n<p> Normal × 500</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>8</p>\n</td>\n<td>\n<p> Normal × 1000</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>9</p>\n</td>\n<td>\n<p> Tous ceux qui peuvent entendre votre voix inspirante</p>\n</td>\n</tr>\n</tbody>\n</table>\n</div><p><strong>Exemple</strong> : <em>Le monastère de l’abbesse Brigitte van der Hoogenband est pris d’assaut par les peaux-vertes, et les choses vont mal. De fait, elle décide de renforcer l’esprit de ses soldats avec un Test de Commandement, leur octroyant +10 à tous les Tests de Psychologie. Elle obtient 3 DR à son Test de Commandement. Étant donné qu’elle possède un Bonus de Sociabilité de 6, et qu’elle peut influencer un nombre de soldats, égal à son Bonus de Sociabilité + DR en utilisant Commandement, elle renforce 9 soldats. Cependant, comme elle possède Exaltant 3, ce nombre est multiplié par 20, ce qui signifie que 180 de ses soldats sont inspirés par ses cris d’encouragement « MAINTENEZ LES RANGS ! »</em>",
|
||||
"id": "WCXnFSV4WOSmzzc4",
|
||||
@@ -5533,6 +5551,11 @@
|
||||
"id": "Art (Painting)",
|
||||
"name": "Art (Peinture)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Créez des œuvres d'art avec le moyen d'expression de votre choix.</p><p>Ne pas avoir accès aux Outils de la profession appropriés entraînera une pénalité à vos Tests. Le DR obtenu détermine la qualité de l'œuvre finale. Un Test étendu peut être nécessaire pour la réalisation d'une œuvre d'art complexe ou de grande taille. La Compétence Art est peu utilisée en combat, mais les bustes de marbre peuvent faire de formidables armes improvisées.</p><p><strong>Exemple</strong> : <em>Irina a reçu une commande pour peindre le portrait d'un noble local, dont son groupe cherche à obtenir les faveurs. Son MJ décide que cela nécessite un total de 10 DR à un Test étendu d'Art, et chaque Test représente une semaine de travail.</em></p>",
|
||||
"id": "Art (Calligraphy)",
|
||||
"name": "Art (Calligraphie)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Créez des œuvres d'art avec le moyen d'expression de votre choix.</p><p>Ne pas avoir accès aux Outils de la profession appropriés entraînera une pénalité à vos Tests. Le DR obtenu détermine la qualité de l'œuvre finale. Un Test étendu peut être nécessaire pour la réalisation d'une œuvre d'art complexe ou de grande taille. La Compétence Art est peu utilisée en combat, mais les bustes de marbre peuvent faire de formidables armes improvisées.</p><p><strong>Exemple</strong> : <em>Irina a reçu une commande pour peindre le portrait d'un noble local, dont son groupe cherche à obtenir les faveurs. Son MJ décide que cela nécessite un total de 10 DR à un Test étendu d'Art, et chaque Test représente une semaine de travail.</em></p>",
|
||||
"id": "Art (Sculpture)",
|
||||
@@ -5668,6 +5691,11 @@
|
||||
"id": "Entertain ()",
|
||||
"name": "Divertissement ()"
|
||||
},
|
||||
{
|
||||
"description": "<p>Cette Compétence vous permet de ravir les foules par la parole, peut-être en chantant, en jouant la comédie ou en faisant quelques blagues. Une utilisation réussie de la Compétence Divertissement signifie que vous avez assez amusé les spectateurs alentour pour qu'ils vous écoutent ; le DR indique à quel point vous avez réussi.</p><p>En combat, il est peu probable que Divertissement soit d'une grande utilité, bien que vous puissiez trouver une façon intéressante d'utiliser Divertissement(Interprétation) pour troubler ou leurrer vos adversaires.</p>",
|
||||
"id": "Entertain (Acting)",
|
||||
"name": "Divertissement (Interprétation)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Cette Compétence vous permet de ravir les foules par la parole, peut-être en chantant, en jouant la comédie ou en faisant quelques blagues. Une utilisation réussie de la Compétence Divertissement signifie que vous avez assez amusé les spectateurs alentour pour qu'ils vous écoutent ; le DR indique à quel point vous avez réussi.</p><p>En combat, il est peu probable que Divertissement soit d'une grande utilité, bien que vous puissiez trouver une façon intéressante d'utiliser Divertissement(Interprétation) pour troubler ou leurrer vos adversaires.</p>",
|
||||
"id": "Entertain (Comedy)",
|
||||
@@ -5683,6 +5711,11 @@
|
||||
"id": "Entertain (Storytelling)",
|
||||
"name": "Divertissement (Narration)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Cette Compétence vous permet de ravir les foules par la parole, peut-être en chantant, en jouant la comédie ou en faisant quelques blagues. Une utilisation réussie de la Compétence Divertissement signifie que vous avez assez amusé les spectateurs alentour pour qu'ils vous écoutent ; le DR indique à quel point vous avez réussi.</p><p>En combat, il est peu probable que Divertissement soit d'une grande utilité, bien que vous puissiez trouver une façon intéressante d'utiliser Divertissement(Comédie) pour troubler ou leurrer vos adversaires.</p>",
|
||||
"id": "Entertain (Speeches)",
|
||||
"name": "Divertissement (Discours)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Évaluation vous permet de déterminer la valeur d'artefacts rares, de marchandises inhabituelles et d'œuvres d'art. Tout le monde est supposé connaître la valeur relative d'objets ordinaires, mais une utilisation réussie d'Évaluation vous permet de déterminer la valeur d'objet étranges et spécifiques. Un Test d'<strong>Évaluation</strong> réussi peut aussi vous indiquer si les marchandises (ou pièces) que vous analysez sont contrefaites - ce Test est généralement opposé au DR du Test d'Art ou de Métier du faussaire. Votre MJ peut appliquer des modificateurs basés sur la rareté ou la méconnaissance de l'objet, ou sur les Compétences spécifiques de votre Personnage ou son passé.</p>",
|
||||
"id": "Evaluate",
|
||||
@@ -5733,11 +5766,21 @@
|
||||
"id": "Language (Albion)",
|
||||
"name": "Langue (Albionais)"
|
||||
},
|
||||
{
|
||||
"description": "<p>La Compétence Langue indique votre aisance avec différentes langues. Tous les Personnages sont considérés comme capables de parler le « reikspiel » – la langue de l’Empire – et leur langue maternelle (si votre Personnage en possède une différente du reikspiel), sans avoir besoin de faire de Test. Si votre partie ne se déroule pas dans l’Empire, remplacez le reikspiel par la langue locale. <p>Si vous possédez l’une de ces Compétences, vous êtes généralement capable de vous faire comprendre dans cette langue, ou de comprendre des concepts simples. Vous serez amené à faire un Test de votre Compétence de Langue quand un concept particulièrement difficile doit être transmis, ou qu’un dialecte ou vocabulaire obscur est utilisé.<p><strong>Albionnais : </strong>Parlée par le peuple d'Albion, originaire du lointain royaume insulaire brumeux et rarement rencontré.</p>",
|
||||
"id": "Language (Arabyan)",
|
||||
"name": "Langue (Arabyan)"
|
||||
},
|
||||
{
|
||||
"description": "<p>La Compétence Langue indique votre aisance avec différentes langues. Tous les Personnages sont considérés comme capables de parler le « reikspiel » – la langue de l’Empire – et leur langue maternelle (si votre Personnage en possède une différente du reikspiel), sans avoir besoin de faire de Test. Si votre partie ne se déroule pas dans l’Empire, remplacez le reikspiel par la langue locale. <p>Si vous possédez l’une de ces Compétences, vous êtes généralement capable de vous faire comprendre dans cette langue, ou de comprendre des concepts simples. Vous serez amené à faire un Test de votre Compétence de Langue quand un concept particulièrement difficile doit être transmis, ou qu’un dialecte ou vocabulaire obscur est utilisé.<p><strong>Bataille : </strong>Parlée par les guerriers du Vieux Monde. Très répandue parmi les soldats et les mercenaires, elle est utilisée pour donner rapidement des ordres pendant la bataille. Elle a soi-disant été développée par la déesse Myrmidia lorsqu'elle arpentait le Vieux Monde en tant que mortelle.</p><h4>Option : Langue (Bataille)</h4><p>La Langue (Bataille) comprend une série d'ordres et de gestes simples qui peuvent être utilisés dans le feu du combat. Un Personnage possédant Langue (Bataille) peut communiquer brièvement avec un autre Personnage pendant un combat, et sans pénalité. Ceux qui ne possèdent pas la Compétence ne peuvent pas coordonner rapidement leurs attaques ou discuter stratégie une fois le combat commencé.</p>",
|
||||
"id": "Language (Battle Tongue)",
|
||||
"name": "Langue (Bataille)"
|
||||
},
|
||||
{
|
||||
"description": "<p>La Compétence Langue indique votre aisance avec différentes langues. Tous les Personnages sont considérés comme capables de parler le « reikspiel » – la langue de l’Empire – et leur langue maternelle (si votre Personnage en possède une différente du reikspiel), sans avoir besoin de faire de Test. Si votre partie ne se déroule pas dans l’Empire, remplacez le reikspiel par la langue locale. <p>Si vous possédez l’une de ces Compétences, vous êtes généralement capable de vous faire comprendre dans cette langue, ou de comprendre des concepts simples. Vous serez amené à faire un Test de votre Compétence de Langue quand un concept particulièrement difficile doit être transmis, ou qu’un dialecte ou vocabulaire obscur est utilisé.<p><strong>Bataille : </strong>Parlée par les guerriers du Vieux Monde. Très répandue parmi les soldats et les mercenaires, elle est utilisée pour donner rapidement des ordres pendant la bataille. Elle a soi-disant été développée par la déesse Myrmidia lorsqu'elle arpentait le Vieux Monde en tant que mortelle.</p><h4>Option : Langue (Bataille)</h4><p>La Langue (Bataille) comprend une série d'ordres et de gestes simples qui peuvent être utilisés dans le feu du combat. Un Personnage possédant Langue (Bataille) peut communiquer brièvement avec un autre Personnage pendant un combat, et sans pénalité. Ceux qui ne possèdent pas la Compétence ne peuvent pas coordonner rapidement leurs attaques ou discuter stratégie une fois le combat commencé.</p>",
|
||||
"id": "Language (Battle)",
|
||||
"name": "Langue (Bataille)"
|
||||
},
|
||||
{
|
||||
"description": "<p>La Compétence Langue indique votre aisance avec différentes langues. Tous les Personnages sont considérés comme capables de parler le « reikspiel » – la langue de l’Empire – et leur langue maternelle (si votre Personnage en possède une différente du reikspiel), sans avoir besoin de faire de Test. Si votre partie ne se déroule pas dans l’Empire, remplacez le reikspiel par la langue locale. <p>Si vous possédez l’une de ces Compétences, vous êtes généralement capable de vous faire comprendre dans cette langue, ou de comprendre des concepts simples. Vous serez amené à faire un Test de votre Compétence de Langue quand un concept particulièrement difficile doit être transmis, ou qu’un dialecte ou vocabulaire obscur est utilisé.<p><strong>Bretonnien : </strong>Parlée par le peuple de Bretonnie, souvent rencontré au Reikland. La Bretonnie est un royaume chevaleresque au sud-ouest de l'Empire.</p>",
|
||||
"id": "Language (Bretonnian)",
|
||||
@@ -5813,6 +5856,11 @@
|
||||
"id": "Language (Wastelander)",
|
||||
"name": "Langue (Wastelander)"
|
||||
},
|
||||
{
|
||||
"description": "<p>La Compétence Langue indique votre aisance avec différentes langues. Tous les Personnages sont considérés comme capables de parler le « reikspiel » – la langue de l’Empire – et leur langue maternelle (si votre Personnage en possède une différente du reikspiel), sans avoir besoin de faire de Test. Si votre partie ne se déroule pas dans l’Empire, remplacez le reikspiel par la langue locale. <p>Si vous possédez l’une de ces Compétences, vous êtes généralement capable de vous faire comprendre dans cette langue, ou de comprendre des concepts simples. Vous serez amené à faire un Test de votre Compétence de Langue quand un concept particulièrement difficile doit être transmis, ou qu’un dialecte ou vocabulaire obscur est utilisé.</p><p><strong>Wastelander : </strong>Parlée par les habitants ruraux du Wasteland, un territoire battu par les vents, se trouvant à l'ouest du Reikland. La langue officielle de sa capitale, Marienburg, est pourtant le reikspiel, car ce royaume faisait jadis partie de l'Empire.</p>",
|
||||
"id": "Language (Guilder)",
|
||||
"name": "Langue (Guilde)"
|
||||
},
|
||||
{
|
||||
"description": "<p>C’est une indication de votre capacité à diriger les autres et à vous faire respecter. Le plus souvent associée à des situations martiales, un meneur déterminé peut toutefois réagir rapidement lors d’un incendie ou autre catastrophe, et les nobles utilisent fréquemment la Compétence pour commander leurs subordonnés.<p>Un Test de Commandement réussi vous permet de donner des ordres à un nombre de cibles égal à votre Bonus de Sociabilité + DR. Si les cibles sont vos subordonnés directs – un noble commandant un serf ou un sergent dirigeant ses troupes – les ordres ne rencontrent aucune opposition. S’il n’y a pas de hiérarchie en place, ou que l’ordre est particulièrement exigeant – comme ordonner à vos soldats de charger la tête d’une hydre – le Test est opposé au Calme de vos cibles.<p>En combat, vous pouvez utiliser Commandement pour encourager vos subalternes. Un Test de Commandement réussi confère un bonus de +10 à tous les Tests de Psychologie jusqu’à la fin du prochain Round (voir page 190).<p>Par ailleurs, Commandement peut être utilisé pour transférer un Avantage à des alliés capables de vous entendre ; après un Test de Commandement réussi, vous pouvez transférer un Avantage à un allié de votre choix, et +1 Avantage par DR obtenu, qui peut de nouveau être alloué aux alliés de votre choix à portée de voix.<p><strong>Exemple :</strong><em>Lord Ludwig von Schemp a regardé ses deux gardes du corps discuter avec des voyous pendant trois Rounds, utilisant sa Compétence Intuition pour cumuler 3 Avantages. Sentant que la situation ne va nulle part, il lance un ordre péremptoire pour attaquer le chef de file ; réussissant son Test avec 5 DR, il donne 2 Avantages à l’un de ses gardes du corps et 1 Avantage à l’autre, espérant que cela mette rapidement fin à l’affaire.</em><blockquote><h4>C'est qui le chef ?</h4>Les MJ doivent être conscients que le fait que des Joueurs donnent des ordres à d’autres Joueurs peut causer des tensions au sein du groupe. Si vous avez des Joueurs avec des classes sociales ou des rangs militaires différents, faites en sorte de discuter de la façon dont cela se répercutera sur la dynamique du groupe pour garantir un jeu fluide et agréable pour tout le monde.</blockquote></p>",
|
||||
"id": "Leadership",
|
||||
@@ -5828,11 +5876,51 @@
|
||||
"id": "Lore ()",
|
||||
"name": "Savoir ()"
|
||||
},
|
||||
{
|
||||
"description": "<p>Posséder une Compétence Savoir signifie que vous avez officiellement étudié, ou avez appris d’une façon ou d’une autre, une branche de connaissances avancées. Vous possédez des données approfondies dans une spécialisation et n’avez donc pas besoin d’effectuer de Test pour que le MJ vous donne les éléments d’information utiles. Si vous recherchez des informations spécifiques moins connues, vous aurez besoin d’effectuer un Test de Savoir, modifié selon la nature obscure de l’information, où les DR indiqueront de combien de détails vous vous souvenez.<p>En combat, un Test de Savoir réussi peut éventuellement vous accorder +1 Avantage (avec l’accord de votre MJ). Par exemple, Savoir (Géologie) peut vous donner un Avantage si vous combattez dans une caverne, ou Savoir (Ingénierie) peut vous aider si vous combattez un ennemi armé d’un appareil mécanique complexe. Vous pouvez continuer à renforcer votre Avantage lors des tours ultérieurs, à condition que les circonstances le permettent (déterminé par le MJ) et que vous ne soyez pas interrompu ; de cette façon, vous pouvez gagner un maximum d’Avantages, égal à votre Bonus d’Intelligence.<p><strong>Spécialisations: </strong>Géologie, Héraldique, Histoire, Ingénierie, Loi, Magick, Métallurgie, Science, Théologie.</p>",
|
||||
"id": "Lore (Skaven)",
|
||||
"name": "Savoir (Skaven)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Posséder une Compétence Savoir signifie que vous avez officiellement étudié, ou avez appris d’une façon ou d’une autre, une branche de connaissances avancées. Vous possédez des données approfondies dans une spécialisation et n’avez donc pas besoin d’effectuer de Test pour que le MJ vous donne les éléments d’information utiles. Si vous recherchez des informations spécifiques moins connues, vous aurez besoin d’effectuer un Test de Savoir, modifié selon la nature obscure de l’information, où les DR indiqueront de combien de détails vous vous souvenez.<p>En combat, un Test de Savoir réussi peut éventuellement vous accorder +1 Avantage (avec l’accord de votre MJ). Par exemple, Savoir (Géologie) peut vous donner un Avantage si vous combattez dans une caverne, ou Savoir (Ingénierie) peut vous aider si vous combattez un ennemi armé d’un appareil mécanique complexe. Vous pouvez continuer à renforcer votre Avantage lors des tours ultérieurs, à condition que les circonstances le permettent (déterminé par le MJ) et que vous ne soyez pas interrompu ; de cette façon, vous pouvez gagner un maximum d’Avantages, égal à votre Bonus d’Intelligence.</p>",
|
||||
"id": "Lore (Engineering)",
|
||||
"name": "Savoir (Ingénierie)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Posséder une Compétence Savoir signifie que vous avez officiellement étudié, ou avez appris d’une façon ou d’une autre, une branche de connaissances avancées. Vous possédez des données approfondies dans une spécialisation et n’avez donc pas besoin d’effectuer de Test pour que le MJ vous donne les éléments d’information utiles. Si vous recherchez des informations spécifiques moins connues, vous aurez besoin d’effectuer un Test de Savoir, modifié selon la nature obscure de l’information, où les DR indiqueront de combien de détails vous vous souvenez.<p>En combat, un Test de Savoir réussi peut éventuellement vous accorder +1 Avantage (avec l’accord de votre MJ). Par exemple, Savoir (Géologie) peut vous donner un Avantage si vous combattez dans une caverne, ou Savoir (Ingénierie) peut vous aider si vous combattez un ennemi armé d’un appareil mécanique complexe. Vous pouvez continuer à renforcer votre Avantage lors des tours ultérieurs, à condition que les circonstances le permettent (déterminé par le MJ) et que vous ne soyez pas interrompu ; de cette façon, vous pouvez gagner un maximum d’Avantages, égal à votre Bonus d’Intelligence.</p>",
|
||||
"id": "Lore (Bestiaria)",
|
||||
"name": "Savoir (Bestiaire)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Posséder une Compétence Savoir signifie que vous avez officiellement étudié, ou avez appris d’une façon ou d’une autre, une branche de connaissances avancées. Vous possédez des données approfondies dans une spécialisation et n’avez donc pas besoin d’effectuer de Test pour que le MJ vous donne les éléments d’information utiles. Si vous recherchez des informations spécifiques moins connues, vous aurez besoin d’effectuer un Test de Savoir, modifié selon la nature obscure de l’information, où les DR indiqueront de combien de détails vous vous souvenez.<p>En combat, un Test de Savoir réussi peut éventuellement vous accorder +1 Avantage (avec l’accord de votre MJ). Par exemple, Savoir (Géologie) peut vous donner un Avantage si vous combattez dans une caverne, ou Savoir (Ingénierie) peut vous aider si vous combattez un ennemi armé d’un appareil mécanique complexe. Vous pouvez continuer à renforcer votre Avantage lors des tours ultérieurs, à condition que les circonstances le permettent (déterminé par le MJ) et que vous ne soyez pas interrompu ; de cette façon, vous pouvez gagner un maximum d’Avantages, égal à votre Bonus d’Intelligence.</p>",
|
||||
"id": "Lore (Chaos)",
|
||||
"name": "Savoir (Chaos)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Posséder une Compétence Savoir signifie que vous avez officiellement étudié, ou avez appris d’une façon ou d’une autre, une branche de connaissances avancées. Vous possédez des données approfondies dans une spécialisation et n’avez donc pas besoin d’effectuer de Test pour que le MJ vous donne les éléments d’information utiles. Si vous recherchez des informations spécifiques moins connues, vous aurez besoin d’effectuer un Test de Savoir, modifié selon la nature obscure de l’information, où les DR indiqueront de combien de détails vous vous souvenez.<p>En combat, un Test de Savoir réussi peut éventuellement vous accorder +1 Avantage (avec l’accord de votre MJ). Par exemple, Savoir (Géologie) peut vous donner un Avantage si vous combattez dans une caverne, ou Savoir (Ingénierie) peut vous aider si vous combattez un ennemi armé d’un appareil mécanique complexe. Vous pouvez continuer à renforcer votre Avantage lors des tours ultérieurs, à condition que les circonstances le permettent (déterminé par le MJ) et que vous ne soyez pas interrompu ; de cette façon, vous pouvez gagner un maximum d’Avantages, égal à votre Bonus d’Intelligence.</p>",
|
||||
"id": "Lore (Daemonology)",
|
||||
"name": "Savoir (Démonologie)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Posséder une Compétence Savoir signifie que vous avez officiellement étudié, ou avez appris d’une façon ou d’une autre, une branche de connaissances avancées. Vous possédez des données approfondies dans une spécialisation et n’avez donc pas besoin d’effectuer de Test pour que le MJ vous donne les éléments d’information utiles. Si vous recherchez des informations spécifiques moins connues, vous aurez besoin d’effectuer un Test de Savoir, modifié selon la nature obscure de l’information, où les DR indiqueront de combien de détails vous vous souvenez.<p>En combat, un Test de Savoir réussi peut éventuellement vous accorder +1 Avantage (avec l’accord de votre MJ). Par exemple, Savoir (Géologie) peut vous donner un Avantage si vous combattez dans une caverne, ou Savoir (Ingénierie) peut vous aider si vous combattez un ennemi armé d’un appareil mécanique complexe. Vous pouvez continuer à renforcer votre Avantage lors des tours ultérieurs, à condition que les circonstances le permettent (déterminé par le MJ) et que vous ne soyez pas interrompu ; de cette façon, vous pouvez gagner un maximum d’Avantages, égal à votre Bonus d’Intelligence.</p>",
|
||||
"id": "Lore (Dark Magic)",
|
||||
"name": "Savoir (Magie Noire)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Posséder une Compétence Savoir signifie que vous avez officiellement étudié, ou avez appris d’une façon ou d’une autre, une branche de connaissances avancées. Vous possédez des données approfondies dans une spécialisation et n’avez donc pas besoin d’effectuer de Test pour que le MJ vous donne les éléments d’information utiles. Si vous recherchez des informations spécifiques moins connues, vous aurez besoin d’effectuer un Test de Savoir, modifié selon la nature obscure de l’information, où les DR indiqueront de combien de détails vous vous souvenez.<p>En combat, un Test de Savoir réussi peut éventuellement vous accorder +1 Avantage (avec l’accord de votre MJ). Par exemple, Savoir (Géologie) peut vous donner un Avantage si vous combattez dans une caverne, ou Savoir (Ingénierie) peut vous aider si vous combattez un ennemi armé d’un appareil mécanique complexe. Vous pouvez continuer à renforcer votre Avantage lors des tours ultérieurs, à condition que les circonstances le permettent (déterminé par le MJ) et que vous ne soyez pas interrompu ; de cette façon, vous pouvez gagner un maximum d’Avantages, égal à votre Bonus d’Intelligence.</p>",
|
||||
"id": "Lore (Tzeentch)",
|
||||
"name": "Savoir (Tzeentch)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Posséder une Compétence Savoir signifie que vous avez officiellement étudié, ou avez appris d’une façon ou d’une autre, une branche de connaissances avancées. Vous possédez des données approfondies dans une spécialisation et n’avez donc pas besoin d’effectuer de Test pour que le MJ vous donne les éléments d’information utiles. Si vous recherchez des informations spécifiques moins connues, vous aurez besoin d’effectuer un Test de Savoir, modifié selon la nature obscure de l’information, où les DR indiqueront de combien de détails vous vous souvenez.<p>En combat, un Test de Savoir réussi peut éventuellement vous accorder +1 Avantage (avec l’accord de votre MJ). Par exemple, Savoir (Géologie) peut vous donner un Avantage si vous combattez dans une caverne, ou Savoir (Ingénierie) peut vous aider si vous combattez un ennemi armé d’un appareil mécanique complexe. Vous pouvez continuer à renforcer votre Avantage lors des tours ultérieurs, à condition que les circonstances le permettent (déterminé par le MJ) et que vous ne soyez pas interrompu ; de cette façon, vous pouvez gagner un maximum d’Avantages, égal à votre Bonus d’Intelligence.</p>",
|
||||
"id": "Lore (Slaanesh)",
|
||||
"name": "Savoir (Slaanesh)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Posséder une Compétence Savoir signifie que vous avez officiellement étudié, ou avez appris d’une façon ou d’une autre, une branche de connaissances avancées. Vous possédez des données approfondies dans une spécialisation et n’avez donc pas besoin d’effectuer de Test pour que le MJ vous donne les éléments d’information utiles. Si vous recherchez des informations spécifiques moins connues, vous aurez besoin d’effectuer un Test de Savoir, modifié selon la nature obscure de l’information, où les DR indiqueront de combien de détails vous vous souvenez.<p>En combat, un Test de Savoir réussi peut éventuellement vous accorder +1 Avantage (avec l’accord de votre MJ). Par exemple, Savoir (Géologie) peut vous donner un Avantage si vous combattez dans une caverne, ou Savoir (Ingénierie) peut vous aider si vous combattez un ennemi armé d’un appareil mécanique complexe. Vous pouvez continuer à renforcer votre Avantage lors des tours ultérieurs, à condition que les circonstances le permettent (déterminé par le MJ) et que vous ne soyez pas interrompu ; de cette façon, vous pouvez gagner un maximum d’Avantages, égal à votre Bonus d’Intelligence.</p>",
|
||||
"id": "Lore (Necromancy)",
|
||||
"name": "Savoir (Necromancie)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Posséder une Compétence Savoir signifie que vous avez officiellement étudié, ou avez appris d’une façon ou d’une autre, une branche de connaissances avancées. Vous possédez des données approfondies dans une spécialisation et n’avez donc pas besoin d’effectuer de Test pour que le MJ vous donne les éléments d’information utiles. Si vous recherchez des informations spécifiques moins connues, vous aurez besoin d’effectuer un Test de Savoir, modifié selon la nature obscure de l’information, où les DR indiqueront de combien de détails vous vous souvenez.<p>En combat, un Test de Savoir réussi peut éventuellement vous accorder +1 Avantage (avec l’accord de votre MJ). Par exemple, Savoir (Géologie) peut vous donner un Avantage si vous combattez dans une caverne, ou Savoir (Ingénierie) peut vous aider si vous combattez un ennemi armé d’un appareil mécanique complexe. Vous pouvez continuer à renforcer votre Avantage lors des tours ultérieurs, à condition que les circonstances le permettent (déterminé par le MJ) et que vous ne soyez pas interrompu ; de cette façon, vous pouvez gagner un maximum d’Avantages, égal à votre Bonus d’Intelligence.</p>",
|
||||
"id": "Lore (Geology)",
|
||||
@@ -5863,16 +5951,51 @@
|
||||
"id": "Lore (Metallurgy)",
|
||||
"name": "Savoir (Métallurgie)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Posséder une Compétence Savoir signifie que vous avez officiellement étudié, ou avez appris d’une façon ou d’une autre, une branche de connaissances avancées. Vous possédez des données approfondies dans une spécialisation et n’avez donc pas besoin d’effectuer de Test pour que le MJ vous donne les éléments d’information utiles. Si vous recherchez des informations spécifiques moins connues, vous aurez besoin d’effectuer un Test de Savoir, modifié selon la nature obscure de l’information, où les DR indiqueront de combien de détails vous vous souvenez.<p>En combat, un Test de Savoir réussi peut éventuellement vous accorder +1 Avantage (avec l’accord de votre MJ). Par exemple, Savoir (Géologie) peut vous donner un Avantage si vous combattez dans une caverne, ou Savoir (Ingénierie) peut vous aider si vous combattez un ennemi armé d’un appareil mécanique complexe. Vous pouvez continuer à renforcer votre Avantage lors des tours ultérieurs, à condition que les circonstances le permettent (déterminé par le MJ) et que vous ne soyez pas interrompu ; de cette façon, vous pouvez gagner un maximum d’Avantages, égal à votre Bonus d’Intelligence.</p>",
|
||||
"id": "Lore (Politics)",
|
||||
"name": "Savoir (Politique)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Posséder une Compétence Savoir signifie que vous avez officiellement étudié, ou avez appris d’une façon ou d’une autre, une branche de connaissances avancées. Vous possédez des données approfondies dans une spécialisation et n’avez donc pas besoin d’effectuer de Test pour que le MJ vous donne les éléments d’information utiles. Si vous recherchez des informations spécifiques moins connues, vous aurez besoin d’effectuer un Test de Savoir, modifié selon la nature obscure de l’information, où les DR indiqueront de combien de détails vous vous souvenez.<p>En combat, un Test de Savoir réussi peut éventuellement vous accorder +1 Avantage (avec l’accord de votre MJ). Par exemple, Savoir (Géologie) peut vous donner un Avantage si vous combattez dans une caverne, ou Savoir (Ingénierie) peut vous aider si vous combattez un ennemi armé d’un appareil mécanique complexe. Vous pouvez continuer à renforcer votre Avantage lors des tours ultérieurs, à condition que les circonstances le permettent (déterminé par le MJ) et que vous ne soyez pas interrompu ; de cette façon, vous pouvez gagner un maximum d’Avantages, égal à votre Bonus d’Intelligence.</p>",
|
||||
"id": "Lore (Science)",
|
||||
"name": "Savoir (Science)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Posséder une Compétence Savoir signifie que vous avez officiellement étudié, ou avez appris d’une façon ou d’une autre, une branche de connaissances avancées. Vous possédez des données approfondies dans une spécialisation et n’avez donc pas besoin d’effectuer de Test pour que le MJ vous donne les éléments d’information utiles. Si vous recherchez des informations spécifiques moins connues, vous aurez besoin d’effectuer un Test de Savoir, modifié selon la nature obscure de l’information, où les DR indiqueront de combien de détails vous vous souvenez.<p>En combat, un Test de Savoir réussi peut éventuellement vous accorder +1 Avantage (avec l’accord de votre MJ). Par exemple, Savoir (Géologie) peut vous donner un Avantage si vous combattez dans une caverne, ou Savoir (Ingénierie) peut vous aider si vous combattez un ennemi armé d’un appareil mécanique complexe. Vous pouvez continuer à renforcer votre Avantage lors des tours ultérieurs, à condition que les circonstances le permettent (déterminé par le MJ) et que vous ne soyez pas interrompu ; de cette façon, vous pouvez gagner un maximum d’Avantages, égal à votre Bonus d’Intelligence.</p>",
|
||||
"id": "Lore (Engineer)",
|
||||
"name": "Savoir (Ingénieur)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Posséder une Compétence Savoir signifie que vous avez officiellement étudié, ou avez appris d’une façon ou d’une autre, une branche de connaissances avancées. Vous possédez des données approfondies dans une spécialisation et n’avez donc pas besoin d’effectuer de Test pour que le MJ vous donne les éléments d’information utiles. Si vous recherchez des informations spécifiques moins connues, vous aurez besoin d’effectuer un Test de Savoir, modifié selon la nature obscure de l’information, où les DR indiqueront de combien de détails vous vous souvenez.<p>En combat, un Test de Savoir réussi peut éventuellement vous accorder +1 Avantage (avec l’accord de votre MJ). Par exemple, Savoir (Géologie) peut vous donner un Avantage si vous combattez dans une caverne, ou Savoir (Ingénierie) peut vous aider si vous combattez un ennemi armé d’un appareil mécanique complexe. Vous pouvez continuer à renforcer votre Avantage lors des tours ultérieurs, à condition que les circonstances le permettent (déterminé par le MJ) et que vous ne soyez pas interrompu ; de cette façon, vous pouvez gagner un maximum d’Avantages, égal à votre Bonus d’Intelligence.</p>",
|
||||
"id": "Lore (Metallurgy)",
|
||||
"name": "Savoir (Metalurgie)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Posséder une Compétence Savoir signifie que vous avez officiellement étudié, ou avez appris d’une façon ou d’une autre, une branche de connaissances avancées. Vous possédez des données approfondies dans une spécialisation et n’avez donc pas besoin d’effectuer de Test pour que le MJ vous donne les éléments d’information utiles. Si vous recherchez des informations spécifiques moins connues, vous aurez besoin d’effectuer un Test de Savoir, modifié selon la nature obscure de l’information, où les DR indiqueront de combien de détails vous vous souvenez.<p>En combat, un Test de Savoir réussi peut éventuellement vous accorder +1 Avantage (avec l’accord de votre MJ). Par exemple, Savoir (Géologie) peut vous donner un Avantage si vous combattez dans une caverne, ou Savoir (Ingénierie) peut vous aider si vous combattez un ennemi armé d’un appareil mécanique complexe. Vous pouvez continuer à renforcer votre Avantage lors des tours ultérieurs, à condition que les circonstances le permettent (déterminé par le MJ) et que vous ne soyez pas interrompu ; de cette façon, vous pouvez gagner un maximum d’Avantages, égal à votre Bonus d’Intelligence.</p>",
|
||||
"id": "Lore (Theology)",
|
||||
"name": "Savoir (Théologie)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Posséder une Compétence Savoir signifie que vous avez officiellement étudié, ou avez appris d’une façon ou d’une autre, une branche de connaissances avancées. Vous possédez des données approfondies dans une spécialisation et n’avez donc pas besoin d’effectuer de Test pour que le MJ vous donne les éléments d’information utiles. Si vous recherchez des informations spécifiques moins connues, vous aurez besoin d’effectuer un Test de Savoir, modifié selon la nature obscure de l’information, où les DR indiqueront de combien de détails vous vous souvenez.<p>En combat, un Test de Savoir réussi peut éventuellement vous accorder +1 Avantage (avec l’accord de votre MJ). Par exemple, Savoir (Géologie) peut vous donner un Avantage si vous combattez dans une caverne, ou Savoir (Ingénierie) peut vous aider si vous combattez un ennemi armé d’un appareil mécanique complexe. Vous pouvez continuer à renforcer votre Avantage lors des tours ultérieurs, à condition que les circonstances le permettent (déterminé par le MJ) et que vous ne soyez pas interrompu ; de cette façon, vous pouvez gagner un maximum d’Avantages, égal à votre Bonus d’Intelligence.</p>",
|
||||
"id": "Lore (Torture)",
|
||||
"name": "Savoir (Torture)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Posséder une Compétence Savoir signifie que vous avez officiellement étudié, ou avez appris d’une façon ou d’une autre, une branche de connaissances avancées. Vous possédez des données approfondies dans une spécialisation et n’avez donc pas besoin d’effectuer de Test pour que le MJ vous donne les éléments d’information utiles. Si vous recherchez des informations spécifiques moins connues, vous aurez besoin d’effectuer un Test de Savoir, modifié selon la nature obscure de l’information, où les DR indiqueront de combien de détails vous vous souvenez.<p>En combat, un Test de Savoir réussi peut éventuellement vous accorder +1 Avantage (avec l’accord de votre MJ). Par exemple, Savoir (Géologie) peut vous donner un Avantage si vous combattez dans une caverne, ou Savoir (Ingénierie) peut vous aider si vous combattez un ennemi armé d’un appareil mécanique complexe. Vous pouvez continuer à renforcer votre Avantage lors des tours ultérieurs, à condition que les circonstances le permettent (déterminé par le MJ) et que vous ne soyez pas interrompu ; de cette façon, vous pouvez gagner un maximum d’Avantages, égal à votre Bonus d’Intelligence.</p>",
|
||||
"id": "Lore (Warfare)",
|
||||
"name": "Savoir (Guerre)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Posséder une Compétence Savoir signifie que vous avez officiellement étudié, ou avez appris d’une façon ou d’une autre, une branche de connaissances avancées. Vous possédez des données approfondies dans une spécialisation et n’avez donc pas besoin d’effectuer de Test pour que le MJ vous donne les éléments d’information utiles. Si vous recherchez des informations spécifiques moins connues, vous aurez besoin d’effectuer un Test de Savoir, modifié selon la nature obscure de l’information, où les DR indiqueront de combien de détails vous vous souvenez.<p>En combat, un Test de Savoir réussi peut éventuellement vous accorder +1 Avantage (avec l’accord de votre MJ). Par exemple, Savoir (Géologie) peut vous donner un Avantage si vous combattez dans une caverne, ou Savoir (Ingénierie) peut vous aider si vous combattez un ennemi armé d’un appareil mécanique complexe. Vous pouvez continuer à renforcer votre Avantage lors des tours ultérieurs, à condition que les circonstances le permettent (déterminé par le MJ) et que vous ne soyez pas interrompu ; de cette façon, vous pouvez gagner un maximum d’Avantages, égal à votre Bonus d’Intelligence.</p>",
|
||||
"id": "Lore (Tilea)",
|
||||
"name": "Savoir (Tilée)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Posséder une Compétence Savoir signifie que vous avez officiellement étudié, ou avez appris d’une façon ou d’une autre, une branche de connaissances avancées. Vous possédez des données approfondies dans une spécialisation et n’avez donc pas besoin d’effectuer de Test pour que le MJ vous donne les éléments d’information utiles. Si vous recherchez des informations spécifiques moins connues, vous aurez besoin d’effectuer un Test de Savoir, modifié selon la nature obscure de l’information, où les DR indiqueront de combien de détails vous vous souvenez.<p>En combat, un Test de Savoir réussi peut éventuellement vous accorder +1 Avantage (avec l’accord de votre MJ). Par exemple, Savoir (Géologie) peut vous donner un Avantage si vous combattez dans une caverne, ou Savoir (Ingénierie) peut vous aider si vous combattez un ennemi armé d’un appareil mécanique complexe. Vous pouvez continuer à renforcer votre Avantage lors des tours ultérieurs, à condition que les circonstances le permettent (déterminé par le MJ) et que vous ne soyez pas interrompu ; de cette façon, vous pouvez gagner un maximum d’Avantages, égal à votre Bonus d’Intelligence.</p>",
|
||||
"id": "Lore (Law)",
|
||||
"name": "Savoir (Loi)"
|
||||
},
|
||||
{
|
||||
"description": "<p>La Compétence Corps à corps représente l'entraînement spécifique que vous avez reçu avec un seul type d'armes de combat rapproché. Chaque Spécialisation de Corps à corps indique un entraînement à une classe d'arme spécifique. Si vous n'avez pas la Spécialisation appropriée pour une arme que vous souhaitez utiliser, référez-vous au <strong>Chapitre 11 : Guide de l'équipement </strong>pour consulter les statistiques de l'arme et savoir quelles pénalités vous subirez. Voir <strong>Chapitre 5 : Règles </strong>pour plus d'informations sur les combats et l'utilisation de la Compétence Corps à corps.<p><strong>Exemples de Spécialisations : </strong>Arme d'hast, Arme à deux mains, Bagarre, Base, Cavalerie, Escrime, Fléau, Parade.</p>",
|
||||
"id": "Melee",
|
||||
@@ -6173,6 +6296,11 @@
|
||||
"id": "Secret Signs (Grey Order)",
|
||||
"name": "Signes secrets (Ordre Gris)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Vous avez été formé à utiliser des marques clandestines intelligibles pour les membres d’un groupe sélectionné. Les raisons pour lesquelles quelqu’un peut vouloir délivrer un message secret sont nombreuses : les vagabonds peuvent indiquer quels propriétaires sont susceptibles d’offrir la charité, les voleurs peuvent vouloir indiquer des failles, ou des cibles potentielles, alors que les guetteurs peuvent vouloir alerter les autres sur l’emplacement proche d’un monstre dangereux.<p>Cette Compétence ne nécessite généralement pas de Test – vous pouvez déchiffrer n’importe quels signes appropriés que vous pouvez voir si vous possédez cette Compétence. Mais si les signes ont été modifiés, effacés, ou si vous êtes pressé par le temps, un Test sera probablement nécessaire. La plupart des messages sont très simples, et ne comptent pas plus de trois mots.<p><strong>Ordre gris : </strong>…les mystérieux Sorciers gris utilisent un ensemble complexe de signes, dont beaucoup ne sont perceptibles que par les sorciers. Ils marquent la région pour leurs Magisters errants, indiquant les maisons sûres, les endroits dangereux, les régions où ils ne sont pas les bienvenus, et bien plus encore.</p>",
|
||||
"id": "Secret Signs (Cultist)",
|
||||
"name": "Signes secrets (Cultist)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Vous avez été formé à utiliser des marques clandestines intelligibles pour les membres d’un groupe sélectionné. Les raisons pour lesquelles quelqu’un peut vouloir délivrer un message secret sont nombreuses : les vagabonds peuvent indiquer quels propriétaires sont susceptibles d’offrir la charité, les voleurs peuvent vouloir indiquer des failles, ou des cibles potentielles, alors que les guetteurs peuvent vouloir alerter les autres sur l’emplacement proche d’un monstre dangereux.<p>Cette Compétence ne nécessite généralement pas de Test – vous pouvez déchiffrer n’importe quels signes appropriés que vous pouvez voir si vous possédez cette Compétence. Mais si les signes ont été modifiés, effacés, ou si vous êtes pressé par le temps, un Test sera probablement nécessaire. La plupart des messages sont très simples, et ne comptent pas plus de trois mots.<p><strong>Guilde : </strong>… les guildes. Beaucoup de grandes guildes, de celle des maçons aux débardeurs, utilisent un ensemble de marques et de signes pour identifier ce qui est important pour leurs membres.</p>",
|
||||
"id": "Secret Signs (Guilder)",
|
||||
@@ -6288,6 +6416,16 @@
|
||||
"id": "Trade (Smith)",
|
||||
"name": "Métier (Forgeron)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Beaucoup de gens au Reikland embrassent une profession ; même les aventuriers ont souvent une Carrière plus fiable ou plus respectable sur laquelle compter entre deux épisodes palpitants.<p></p>La Compétence Métier représente votre capacité à créer quelque chose ou à fournir un service, ainsi que votre connaissance du savoir essentiel entourant votre profession.</p><p>Posséder la Compétence est suffisant pour effectuer automatiquement les tâches associées à votre profession, en partant du principe que vous possédez les ressources et des outils adaptés. Vous avez besoin d'effectuer un Test de Métier uniquement si vous cherchez à créer quelque chose rapidement, que les conditions sont défavorables ou que vous cherchez à inventer ou créer un objet de grande qualité.</p><p>Les Tests de Métier de ce type sont souvent des Tests étendus, dont le DR et le temps nécessaires dépendent de la portée ou de l'ampleur de ce qui est produit ; un repas rapide avec Métier (Cuisinier) pour impressionner un seigneur local prendra bien moins de temps que la construction d'un navire de guerre avec Métier (Charpentier de marine).</p><p>Vous pouvez aussi effectuer un Test de Métier comme une Compétence Savoir pour déterminer une information importante concernant la profession en question. Dans de telles circonstances, le MJ peut préférer utiliser l'Int plutôt que la Dex comme Caractéristique de base, bien que cela soit souvent ignoré pour simplifer.</p><p>Bien que beaucoup de Compétences Métier aient peu d'utilité en combat, il y a autant de Compétences Métier que de professions, et certaines peuvent être utilisées en fonction des circonstances. Par exemple, réussir un Test de Métier (Apothicaire) peut être utile si vous combattez dans une échoppe d'Apothicaire, car vous identifiez des substances âcres à jeter sur vos ennemis.</p><p>La Compétence Métier est aussi utilisée pour effectuer une Activité <em>Artisanat</em> (voir page 196).</p>",
|
||||
"id": "Trade (Engineer)",
|
||||
"name": "Métier (Ingénieur)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Beaucoup de gens au Reikland embrassent une profession ; même les aventuriers ont souvent une Carrière plus fiable ou plus respectable sur laquelle compter entre deux épisodes palpitants.<p></p>La Compétence Métier représente votre capacité à créer quelque chose ou à fournir un service, ainsi que votre connaissance du savoir essentiel entourant votre profession.</p><p>Posséder la Compétence est suffisant pour effectuer automatiquement les tâches associées à votre profession, en partant du principe que vous possédez les ressources et des outils adaptés. Vous avez besoin d'effectuer un Test de Métier uniquement si vous cherchez à créer quelque chose rapidement, que les conditions sont défavorables ou que vous cherchez à inventer ou créer un objet de grande qualité.</p><p>Les Tests de Métier de ce type sont souvent des Tests étendus, dont le DR et le temps nécessaires dépendent de la portée ou de l'ampleur de ce qui est produit ; un repas rapide avec Métier (Cuisinier) pour impressionner un seigneur local prendra bien moins de temps que la construction d'un navire de guerre avec Métier (Charpentier de marine).</p><p>Vous pouvez aussi effectuer un Test de Métier comme une Compétence Savoir pour déterminer une information importante concernant la profession en question. Dans de telles circonstances, le MJ peut préférer utiliser l'Int plutôt que la Dex comme Caractéristique de base, bien que cela soit souvent ignoré pour simplifer.</p><p>Bien que beaucoup de Compétences Métier aient peu d'utilité en combat, il y a autant de Compétences Métier que de professions, et certaines peuvent être utilisées en fonction des circonstances. Par exemple, réussir un Test de Métier (Apothicaire) peut être utile si vous combattez dans une échoppe d'Apothicaire, car vous identifiez des substances âcres à jeter sur vos ennemis.</p><p>La Compétence Métier est aussi utilisée pour effectuer une Activité <em>Artisanat</em> (voir page 196).</p>",
|
||||
"id": "Trade (Farrier)",
|
||||
"name": "Métier (Maréchal-ferrant)"
|
||||
},
|
||||
{
|
||||
"description": "<p>Beaucoup de gens au Reikland embrassent une profession ; même les aventuriers ont souvent une Carrière plus fiable ou plus respectable sur laquelle compter entre deux épisodes palpitants. </p><p>La Compétence Métier représente votre capacité à créer quelque chose ou à fournir un service, ainsi que votre connaissance du savoir essentiel entourant votre profession.</p><p>Posséder la Compétence est suffisant pour effectuer automatiquement les tâches associées à votre profession, en partant du principe que vous possédez les ressources et des outils adaptés. Vous avez besoin d'effectuer un Test de Métier uniquement si vous cherchez à créer quelque chose rapidement, que les conditions sont défavorables ou que vous cherchez à inventer ou créer un objet de grande qualité.</p><p>Les Tests de Métier de ce type sont souvent des Tests étendus, dont le DR et le temps nécessaires dépendent de la portée ou de l'ampleur de ce qui est produit ; un repas rapide avec Métier (Cuisinier) pour impressionner un seigneur local prendra bien moins de temps que la construction d'un navire de guerre avec Métier (Charpentier de marine).</p><p>Vous pouvez aussi effectuer un Test de Métier comme une Compétence Savoir pour déterminer une information importante concernant la profession en question. Dans de telles circonstances, le MJ peut préférer utiliser l'Int plutôt que la Dex comme Caractéristique de base, bien que cela soit souvent ignoré pour simplifer.</p><p>Bien que beaucoup de Compétences Métier aient peu d'utilité en combat, il y a autant de Compétences Métier que de professions, et certaines peuvent être utilisées en fonction des circonstances. Par exemple, réussir un Test de Métier (Apothicaire) peut être utile si vous combattez dans une échoppe d'Apothicaire, car vous identifiez des substances âcres à jeter sur vos ennemis.</p><p>La Compétence Métier est aussi utilisée pour effectuer une Activité Artisanat (voir page 196).</p>",
|
||||
"id": "Trade (Tanner)",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"label": "Acteurs (Le Rat Cornu",
|
||||
"label": "Acteurs (Le Rat Cornu)",
|
||||
"entries": {
|
||||
"Blanca Emrich": {
|
||||
"name": "Blanca Emrich",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
46
fr.json
46
fr.json
@@ -314,6 +314,10 @@
|
||||
"SHEET.NPCSheetNoTheme":"Fiche de PNJ (sans thème)",
|
||||
"SHEET.VehicleSheet":"Fiche de véhicule",
|
||||
"SHEET.VehicleSheetNoTheme":"Fiche de véhicule (sans thème)",
|
||||
"SHEET.Append":"Ajouter après",
|
||||
"SHEET.Prepend":"Ajouter avant",
|
||||
"SHEET.SkillName":"Nom de la compétence",
|
||||
"SHEET.TalentName":"Nom du Talent",
|
||||
|
||||
"SHEETS.Actor.vehicle":"Fiche de Véhicule",
|
||||
"SHEETS.Item.ammunition":"Fiche de Munitions",
|
||||
@@ -429,6 +433,7 @@
|
||||
"ITEM.Roles":"Roles",
|
||||
"ITEM.VitalRoles":"Roles vitaux",
|
||||
"ITEM.LearningXP":"XP d'Apprentissage",
|
||||
"ITEM.Custom":"Personnalisé",
|
||||
|
||||
"TOKEN.MOVEMENT.Status.immobile":"Immobile",
|
||||
"TOKEN.MOVEMENT.Status.restricted":"Restreint",
|
||||
@@ -645,6 +650,7 @@
|
||||
"Hide": "Cacher",
|
||||
"Targets": "Cibles",
|
||||
"Melee": "Corps à corps",
|
||||
"Swim":" Natation",
|
||||
"Cost": "Prix",
|
||||
"Length": "Longueur",
|
||||
"Cargo": "Cargaison",
|
||||
@@ -737,7 +743,6 @@
|
||||
"Stat Block Parser":"Parser de fiches de PNJ",
|
||||
"Submit":"Soumettre",
|
||||
"SuccessLevels":"DR",
|
||||
"Swim":"Nager",
|
||||
"TBRed":"BE",
|
||||
"TookDamage":"Encaisse {damage} dommages",
|
||||
"ToughnessDesc":"Endurance",
|
||||
@@ -753,9 +758,25 @@
|
||||
"WFRP4e Homebrew":"WFRP4e Homebrew",
|
||||
"WFRP4e House Rules":"WFRP4e Règles Maison",
|
||||
"WarnUnlinkedMount":"Une monture non liée associée à un Acteur lié n'est pas recommandé.",
|
||||
"Wood Elf":"Elfe des Bois",
|
||||
"Wood Elf":"Elfe Sylvain",
|
||||
"XP":"XP",
|
||||
"comma separated":"éparé par des virgules",
|
||||
"Overrides" : "Surcharges",
|
||||
"Casting Skill" : "Comp. d'Incantation",
|
||||
"Channelling Skill" : "Comp. de Focalisation",
|
||||
"Ritual" : "Rituel",
|
||||
"Random Vortex" : "Vortex aléatoire",
|
||||
"Area of Effect" : "Zone d'Effet",
|
||||
"Dice": "Dé",
|
||||
"Extra Overcast Options": "Options Spéciales de Surincantation",
|
||||
"Force Advancement": "Surcharge d'Avancement",
|
||||
"Attack Type": "Types d'Attaque",
|
||||
"Trapping Type": "Type d'Equipement",
|
||||
"War Machine": "Machine de Guerre",
|
||||
"XP Cost Modifier": "Modificateur de Coût en XP",
|
||||
"Add SL": "DR additionnels",
|
||||
"Prompt Location": "Demander la localisation",
|
||||
"Dice Roll": "Lancer de Dé",
|
||||
|
||||
"TABLE.Column":"Colonne de la Table",
|
||||
"TABLE.Key":"Clé de la Table",
|
||||
@@ -1004,6 +1025,7 @@
|
||||
"DIALOG.LinkCareerContent":"Relier {new} avec {old}? Les compétences de {old} seront ajoutées à {new}, tout en préservant toutes les spécialisations effectuées avec la carrière précédente.",
|
||||
"DIALOG.ChoosePassenger":"Choisissez un passager",
|
||||
"DIALOG.PostQuantityContent":"Combien de fois cet item peut être récupéré? Laissez vide pour illimité.",
|
||||
"DIALOG.ChooseArmour":"Choisissez une armure à endommager",
|
||||
|
||||
"CHAT.CareerChoose" : "Choisissez votre carrière",
|
||||
"CHAT.DamageError" : "Erreur de calcul des dégâts:",
|
||||
@@ -1168,6 +1190,8 @@
|
||||
"CHAT.Vital":"Vital",
|
||||
"CHAT.DiseaseRollError":"Une erreur s'est produite lors du jet d'incubation ou de durée de la maladie.",
|
||||
"CHAT.ExpReceivedNoReason":"Vous avez reçu <b>{amount}</b> points d'expérience",
|
||||
"CHAT.CriticalDeflection":"Déviation de Critique",
|
||||
"CHAT.DamageToArmour":"1 Dommages appliqués à {item} ({type})",
|
||||
|
||||
"Error.SpeciesSkills" : "Impossible d'ajouter des compétences pour les races",
|
||||
"Error.SpeciesTalents" : "Impossible d'ajouter des talents pour les races",
|
||||
@@ -1481,6 +1505,19 @@
|
||||
"WFRP4E.SymptomTreatment.Delirium" : "Certaines autorités traitent le délire comme faisant partie de la fièvre et prescrivent les mêmes remèdes. Ces derniers coûtent de quelques Sous de Cuivre à quelques Pistoles, et 10% d'entre eux sont efficaces.<br><br>Avec le bon médicament, un <b> Test de Guérison Intermédiaire (+0)</b> réussi fait cesser les hallucinations pendant <a class = 'chat-roll'><i class='fas fa-dice'></i> 1d10</a> heures.<br><br>Il est également courant de calmer les patients délirants avec des drogues tranquilisantes, comme la Fleur de Lune ou même de la Belladone, pour garder le patient Inconscient jusqu'à ce que la condition soit passée, en l'envoyant dans un sommeil agité jusqu'à ce qu'il récupère ou meure.",
|
||||
"WFRP4E.SymptomTreatment.Swelling" : "La plupart des traitements consistent à plonger la partie affectée, ou parfois tout le corps, dans un bain d'eau glacée pour réduire la chaleur qui accompagne les gonflements. Un <b>Test de Guérison Etendu Difficile (-20)</b> requérant +3 DR réduit le gonflement pendant <b><a class = 'chat-roll'>2d10</a></b> heures. Chaque Test prend une heure. Le patient subit une condition Fatigué +1 pour chaque test effectué au cours du processus.<br><br>Certains médecins saignent plutôt le patient avec une lame ou des sangsues. Un <b>Test de Guérison Etendu</b> réussi nécessitant +4 SR et des Outils (Médecin) réduisent le gonflement pendant (<a class = 'chat-roll'>1d10</a> + le Bonus d'Endurance du patient) heures. Chaque Test a une Difficulté <b>Impossible (-50)</b> et nécessite 30mn.",
|
||||
|
||||
"WFRP4E.Symptom.Blight": "Toxine",
|
||||
"WFRP4E.Symptom.Buboes": "Bubons",
|
||||
"WFRP4E.Symptom.Convulsions": "Convulsions",
|
||||
"WFRP4E.Symptom.CoughsSneezes": "Toux et éternuements",
|
||||
"WFRP4E.Symptom.Fever": "Fièvre",
|
||||
"WFRP4E.Symptom.Flux": "Intoxication Alimentaire",
|
||||
"WFRP4E.Symptom.Gangrene": "Gangrène",
|
||||
"WFRP4E.Symptom.Lingering": "Persistant",
|
||||
"WFRP4E.Symptom.Malaise": "Malaise",
|
||||
"WFRP4E.Symptom.Nausea": "Nausée",
|
||||
"WFRP4E.Symptom.Pox": "Démangeaisons",
|
||||
"WFRP4E.Symptom.Wounded": "Blessé",
|
||||
|
||||
"CONDITION.Apply" : "Appliquer {condition}",
|
||||
"CONDITION.ApplyError" : "Vous ne pouvez pas appliquer d'états à cet acteur",
|
||||
|
||||
@@ -2667,6 +2704,10 @@
|
||||
"major":"Majeur",
|
||||
"minor":"Mineur",
|
||||
"moderate":"Modéré",
|
||||
"DurationPlaceholder":"Texte concernant a durée",
|
||||
"ErrorArmourDamagePermission":"Vous n'avez pas les droits pour endommager l'armure de cet acteur.",
|
||||
"IncubationPlaceholder":"Texte concernant l'incubation",
|
||||
"Required Trappings":"Equipement requis",
|
||||
|
||||
"WH":{
|
||||
"TransferType":{
|
||||
@@ -2687,5 +2728,6 @@
|
||||
"SHEET.ExperienceLog":"Journal d'Expérince",
|
||||
"SHEET.Attacker":"Attaquant",
|
||||
"SHEET.Randomize":"Aléatoire",
|
||||
"SHEET.RequiredTrappingsError":"Impossible de lancer les revenus sans avoir tous les équipements requis dans cette carrière !",
|
||||
"Sheet.RollIncome":"Revenu"
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
}
|
||||
],
|
||||
"url": "https://www.uberwald.me/gitea/public/foundryvtt-wh4-lang-fr-fr",
|
||||
"version": "9.0.2",
|
||||
"version": "9.3.4",
|
||||
"esmodules": [
|
||||
"wh4_fr.js",
|
||||
"modules/babele-register.js",
|
||||
"modules/addon-register.js",
|
||||
"modules/import-stat-2.js",
|
||||
@@ -119,7 +120,7 @@
|
||||
}
|
||||
],
|
||||
"manifest": "https://www.uberwald.me/gitea/public/foundryvtt-wh4-lang-fr-fr/raw/v10/module.json",
|
||||
"download": "https://www.uberwald.me/gitea/public/foundryvtt-wh4-lang-fr-fr/archive/foundryvtt-wh4-lang-fr-9-0-2.zip",
|
||||
"download": "https://www.uberwald.me/gitea/public/foundryvtt-wh4-lang-fr-fr/archive/foundryvtt-wh4-lang-fr-9-3-4.zip",
|
||||
"id": "wh4-fr-translation",
|
||||
"compatibility": {
|
||||
"minimum": "13",
|
||||
|
||||
@@ -82,39 +82,8 @@ const _patch_up_in_arms = () => {
|
||||
}
|
||||
|
||||
/************************************************************************************/
|
||||
/* Manages /auberge command */
|
||||
const _manage_inn_roll = async (content, msg) => {
|
||||
// Split input into arguments
|
||||
let command = content.split(" ").map(function (item) {
|
||||
return item.trim();
|
||||
})
|
||||
|
||||
console.log("COMMANDES", command);
|
||||
if (command[0] == "/auberge" && command[1]) {
|
||||
msg["type"] = 0;
|
||||
msg["rollMode"] = "gmroll";
|
||||
let compendium = game.packs.get('wh4-fr-translation.plats-dauberges')
|
||||
game.packs.get(compendium);
|
||||
let rollList = await compendium.getDocuments()
|
||||
for (const element of rollList) {
|
||||
let rollTab = element;
|
||||
console.log("Got compendium...", rollList, rollTab.name);
|
||||
if (rollTab.name.toLowerCase().includes(command[1].toLowerCase())) {
|
||||
let my_rollTable;
|
||||
await compendium.getDocument(rollTab._id).then(mytab => my_rollTable = mytab);
|
||||
my_rollTable.draw({ rollMode: "gmroll" });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (content.includes("/auberge")) {
|
||||
msg["type"] = 0;
|
||||
msg["rollMode"] = "gmroll";
|
||||
msg["content"] = "Syntaxe : /auberge MOT_CLE, avec MOT_CLE parmi:<br>BoissonsBase, BoissonsFortes, Desserts, PlatsCommuns, PlatsExcellents, PlatsMaritimes, PlatsMédiocres, PlatsQualité, PlatsRivières<br>Des raccourcis sont possibles avec une partie du nom : /auberge Base (correspond à BoissonBase) ou /auberge Mari (correspond à PlatsMaritimes), etc."
|
||||
ChatMessage.create(msg);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/* Module /auberge géré par modules/inn/inn-init.js */
|
||||
// L'ancienne implémentation a été migrée vers le module inn pour cohérence avec /voyage
|
||||
|
||||
/************************************************************************************/
|
||||
let __eis_tables = {
|
||||
@@ -262,17 +231,13 @@ const __check_fix_wrong_modules = (chatFlag, patchFinished) => {
|
||||
}
|
||||
} else if (game.user.isGM && patchFinished) {
|
||||
ChatMessage.create({
|
||||
content: "<div>Les modules WFRP4E ont été <strong>patchés avec succés</strong>. Vous pouvez y aller et que <strong>Shallya vous garde !</strong></div><div>Derniers changements : Support WFRP4E v8.3.X</div></ul>",
|
||||
content: "<div>Les modules WFRP4E ont été <strong>patchés avec succés</strong>. Vous pouvez y aller et que <strong>Shallya vous garde !</strong><div><div>Changements v9.3.4 : <ul><li>Ajout de la commande /voyage !</li><li>Améliorations de la commande /auberge</li><li>Très grosses mise à jour des scripts d'Effets et de leur traduction</li></ul></div>",
|
||||
user: game.user.id,
|
||||
whisper: ChatMessage.getWhisperRecipients("GM")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const __history = [
|
||||
"Nouveautés 9.0.0: <ul><li>Support Foundry v13 et diverses petites corrections !</li></ul>"
|
||||
]
|
||||
|
||||
/************************************************************************************/
|
||||
const convertColumnToMulti = (table) => {
|
||||
let columns = table.columns;
|
||||
@@ -320,6 +285,7 @@ const __add_actors_translation = () => {
|
||||
const lang = game.settings.get('core', 'language');
|
||||
if (lang == "fr") {
|
||||
for (let metadata of game.packs) {
|
||||
console.log("Checking pack", metadata.collection, metadata.documentName, metadata.metadata.id, game.babele.isTranslated(metadata));
|
||||
if (!game.babele.isTranslated(metadata) &&
|
||||
metadata.collection != "wfrp4e-core.actors" &&
|
||||
metadata.collection != "wfrp4e-core.bestiary" &&
|
||||
@@ -365,14 +331,8 @@ const __add_actors_translation = () => {
|
||||
|
||||
|
||||
/************************************************************************************/
|
||||
/* Hook for specific command */
|
||||
Hooks.on("chatMessage", (html, content, msg) => {
|
||||
|
||||
if (content.toLowerCase().includes('auberge')) {
|
||||
_manage_inn_roll(content, msg);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
/* Hook for specific command - Module /auberge migré vers modules/inn/ */
|
||||
// La commande /auberge est désormais gérée par le module inn-init.js
|
||||
|
||||
/************************************************************************************/
|
||||
/* Additionnal hooks ready */
|
||||
|
||||
@@ -420,8 +420,8 @@ Hooks.once('init', () => {
|
||||
}
|
||||
let validCompendiums = game.wfrp4e.tags.getPacksWithTag("trait")
|
||||
for (let compData of validCompendiums) {
|
||||
let trait_fr = game.babele.translate(compData.metadata.id, { name: name_en }, true)
|
||||
if (trait_fr?.system) {
|
||||
let trait_fr = game.babele.translate(compData.metadata.id, { name: name_en, system:{description:{value: trait_en.system.description.value}} }, true)
|
||||
if (trait_fr?.name && trait_fr?.name != name_en) {
|
||||
trait_fr.name = trait_fr.name || trait_en.name
|
||||
trait_en.name = nbt + trait_fr.name + special;
|
||||
trait_en.system.description.value = trait_fr.system.description.value;
|
||||
@@ -441,8 +441,9 @@ Hooks.once('init', () => {
|
||||
}
|
||||
let validCompendiums = game.wfrp4e.tags.getPacksWithTag("skill")
|
||||
for (let compData of validCompendiums) {
|
||||
let trait_fr = game.babele.translate(compData.metadata.id, { name: name_en }, true)
|
||||
if (trait_fr?.system) {
|
||||
let trait_fr = game.babele.translate(compData.metadata.id, { name: name_en, system:{description:{value: trait_en.system.description.value}} }, true)
|
||||
//console.log(">>>>> Skill FR ?", trait_fr, name_en, special)
|
||||
if (trait_fr?.name && trait_fr?.name != name_en) {
|
||||
trait_fr.name = trait_fr.name || name_en
|
||||
trait_en.name = trait_fr.name + special;
|
||||
trait_en.system.description.value = trait_fr.system.description.value;
|
||||
@@ -452,8 +453,8 @@ Hooks.once('init', () => {
|
||||
} else if (trait_en.type == "prayer") {
|
||||
let validCompendiums = game.wfrp4e.tags.getPacksWithTag("prayer")
|
||||
for (let compData of validCompendiums) {
|
||||
let trait_fr = game.babele.translate(compData.metadata.id, { name: name_en }, true)
|
||||
if (trait_fr?.system) {
|
||||
let trait_fr = game.babele.translate(compData.metadata.id, { name: name_en, system:{description:{value: trait_en.system.description.value}} }, true)
|
||||
if (trait_fr?.name && trait_fr?.name != name_en) {
|
||||
WFRP4FrTranslation.parseSpellContent(trait_en)
|
||||
trait_fr.name = trait_fr.name || name_en
|
||||
trait_en.name = trait_fr.name + special;
|
||||
@@ -466,8 +467,8 @@ Hooks.once('init', () => {
|
||||
} else if (trait_en.type == "spell") {
|
||||
let validCompendiums = game.wfrp4e.tags.getPacksWithTag("spell")
|
||||
for (let compData of validCompendiums) {
|
||||
let trait_fr = game.babele.translate(compData.metadata.id, { name: name_en }, true)
|
||||
if (trait_fr?.system) {
|
||||
let trait_fr = game.babele.translate(compData.metadata.id, { name: name_en, system:{description:{value: trait_en.system.description.value}} }, true)
|
||||
if (trait_fr?.name && trait_fr?.name != name_en) {
|
||||
WFRP4FrTranslation.parseSpellContent(trait_en)
|
||||
trait_fr.name = trait_fr.name || name_en
|
||||
trait_en.name = trait_fr.name + special;
|
||||
@@ -492,12 +493,16 @@ Hooks.once('init', () => {
|
||||
if (name_en === "Inspiring") {
|
||||
name_en = "WCXnFSV4WOSmzzc4"
|
||||
}
|
||||
let trait_fr = game.babele.translate(compData.metadata.id, { name: name_en }, true)
|
||||
if (trait_fr?.system) {
|
||||
trait_en.name = name_en // Reset the name to the original one
|
||||
let trait_fr = game.babele.translate(compData.metadata.id, trait_en, true)
|
||||
if (trait_fr?.name && trait_fr?.name != name_en) {
|
||||
trait_fr.name = trait_fr.name || name_en // Security since babele v10
|
||||
//console.log(">>>>> Talent ?", trait_fr, name_en, special, trait_fr.name);
|
||||
if (trait_fr.name && (trait_fr.name == "Sprinter" || trait_fr.name != name_en)) { // Talent translated!
|
||||
trait_en.name = trait_fr.name.trim() + special
|
||||
if (trait_fr.system?.tests?.value) { // Why ???
|
||||
trait_en.system.tests.value = trait_fr.system.tests.value;
|
||||
}
|
||||
if (trait_fr.system?.description?.value) { // Why ???
|
||||
trait_en.system.description.value = trait_fr.system.description.value;
|
||||
}
|
||||
@@ -508,8 +513,8 @@ Hooks.once('init', () => {
|
||||
} else if (trait_en.type == "career") {
|
||||
let validCompendiums = game.wfrp4e.tags.getPacksWithTag("career")
|
||||
for (let compData of validCompendiums) {
|
||||
let career_fr = game.babele.translate(compData.metadata.id, { name: name_en }, true);
|
||||
if (career_fr?.system) {
|
||||
let career_fr = game.babele.translate(compData.metadata.id, trait_en, true);
|
||||
if (career_fr?.name && career_fr?.name != name_en) {
|
||||
trait_en.name = career_fr.name || trait_en.name
|
||||
// DEBG: console.log(">>>>> Career ?", career_fr.name );
|
||||
trait_en.system = foundry.utils.duplicate(career_fr.system);
|
||||
@@ -520,7 +525,7 @@ Hooks.once('init', () => {
|
||||
let validCompendiums = game.wfrp4e.tags.getPacksWithTag("vehicleRole")
|
||||
for (let compData of validCompendiums) {
|
||||
let role_fr = game.babele.translate(compData.metadata.id, trait_en, true);
|
||||
if (role_fr?.system) {
|
||||
if (role_fr?.name && role_fr?.name != name_en) {
|
||||
trait_en.name = role_fr.name || trait_en.name
|
||||
// DEBG: console.log(">>>>> Role ?", role_fr.name );
|
||||
trait_en.system = foundry.utils.duplicate(role_fr.system);
|
||||
@@ -530,8 +535,8 @@ Hooks.once('init', () => {
|
||||
} else if (trait_en.type == "trapping" || trait_en.type == "weapon" || trait_en.type == "armour" || trait_en.type == "container" || trait_en.type == "money") {
|
||||
let validCompendiums = game.wfrp4e.tags.getPacksWithTag(["trapping"], ["weapon", "armour", "container", "money"])
|
||||
for (let compData of validCompendiums) {
|
||||
let trapping_fr = game.babele.translate(compData.metadata.id, { name: name_en }, true);
|
||||
if (trapping_fr?.system) {
|
||||
let trapping_fr = game.babele.translate(compData.metadata.id, { name: name_en, system:{description:{value: trait_en.system.description.value}} }, true);
|
||||
if (trapping_fr?.name && trapping_fr?.name != name_en) {
|
||||
trait_en.name = trapping_fr.name || trait_en.name
|
||||
if (trapping_fr.system?.description?.value) {
|
||||
trait_en.system.description.value = trapping_fr.system.description.value
|
||||
|
||||
@@ -2,51 +2,72 @@
|
||||
export class WH4FRPatchConfig {
|
||||
|
||||
/************************************************************************************/
|
||||
static translateSkillList( skillList) {
|
||||
static translateSkillList(skillList) {
|
||||
|
||||
if (!skillList || skillList.length == 0) {
|
||||
return skillList;
|
||||
}
|
||||
let compendiumName = 'wfrp4e-core.items'
|
||||
|
||||
let newList = [];
|
||||
for( let compName of skillList) {
|
||||
for (let compName of skillList) {
|
||||
if (!compName) {
|
||||
newList.push(compName);
|
||||
continue;
|
||||
}
|
||||
if (!isNaN(compName)) { // If numeric, keep as is (for skill levels)
|
||||
newList.push(compName);
|
||||
continue;
|
||||
}
|
||||
// Trim compName
|
||||
compName = compName.trim();
|
||||
let special = "";
|
||||
let newName = compName;
|
||||
if ( compName.includes("(") && compName.includes(")") ) { // Then process specific skills name with (xxxx) inside
|
||||
let re = /(.*) +\((.*)\)/i;
|
||||
let res = re.exec( compName );
|
||||
let baseName = compName
|
||||
if (compName.includes("(") && compName.includes(")")) { // Then process specific skills name with (xxxx) inside
|
||||
let re = /(.*) +\((.*)\)/i;
|
||||
let res = re.exec(compName);
|
||||
compName = res[1].trim(); // Get the root skill name
|
||||
special = " (" + game.i18n.localize( res[2].trim() ) + ")"; // And the special keyword
|
||||
special = " (" + game.i18n.localize(res[2].trim()) + ")"; // And the special keyword
|
||||
}
|
||||
let compNameFR = game.babele.translate( compendiumName, { name: compName }, true );
|
||||
let compNameFR = game.babele.translate(compendiumName, { name: compName }, true);
|
||||
if (compNameFR.name != compName) { // Translation OK
|
||||
newName = compNameFR.name + special;
|
||||
}
|
||||
// DEBUG console.log("Translating skill ", compName, baseName, " to ", newName, special);
|
||||
if (!newName || newName == "" || newName === undefined || newName === "undefined") { // If no translation, keep the original name
|
||||
newName = baseName; // If no translation, keep the original name
|
||||
}
|
||||
newList.push(newName);
|
||||
}
|
||||
return newList;
|
||||
}
|
||||
|
||||
/************************************************************************************/
|
||||
static translateTalentList( talentList) {
|
||||
static translateTalentList(talentList) {
|
||||
|
||||
if (!talentList || talentList.length == 0) {
|
||||
return talentList;
|
||||
}
|
||||
let compendiumName = 'wfrp4e-core.items'
|
||||
|
||||
let newList = [];
|
||||
for( let talentLine of talentList) {
|
||||
for (let talentLine of talentList) {
|
||||
let special = "";
|
||||
let newName = talentLine;
|
||||
if ( isNaN(talentLine) ) {
|
||||
if (isNaN(talentLine)) {
|
||||
let subList = talentLine.split(',');
|
||||
let newSubList = [];
|
||||
for (let talentName of subList ) {
|
||||
for (let talentName of subList) {
|
||||
talentName = talentName.trim();
|
||||
let newName2 = talentName;
|
||||
if ( talentName.includes("(") && talentName.includes(")") ) { // Then process specific skills name with (xxxx) inside
|
||||
let re = /(.*) +\((.*)\)/i;
|
||||
let res = re.exec( talentName );
|
||||
if (talentName.includes("(") && talentName.includes(")")) { // Then process specific skills name with (xxxx) inside
|
||||
let re = /(.*) +\((.*)\)/i;
|
||||
let res = re.exec(talentName);
|
||||
talentName = res[1].trim(); // Get the root skill name
|
||||
special = " (" + game.i18n.localize( res[2].trim() ) + ")"; // And the special keyword
|
||||
special = " (" + game.i18n.localize(res[2].trim()) + ")"; // And the special keyword
|
||||
}
|
||||
let talentNameFR = game.babele.translate( compendiumName, { name: talentName }, true );
|
||||
let talentNameFR = game.babele.translate(compendiumName, { name: talentName }, true);
|
||||
if (talentNameFR.name != talentName) { // Translation OK
|
||||
newName2 = talentNameFR.name + special;
|
||||
}
|
||||
@@ -60,15 +81,18 @@ export class WH4FRPatchConfig {
|
||||
}
|
||||
|
||||
/************************************************************************************/
|
||||
static patch_subspecies( ) {
|
||||
for ( let speciesName in game.wfrp4e.config.subspecies) {
|
||||
static patch_subspecies() {
|
||||
if (!game.wfrp4e?.config?.subspecies) {
|
||||
return
|
||||
}
|
||||
for (let speciesName in game.wfrp4e.config.subspecies) {
|
||||
let subspeciesList = game.wfrp4e.config.subspecies[speciesName];
|
||||
for ( let subspeciesName in subspeciesList) {
|
||||
for (let subspeciesName in subspeciesList) {
|
||||
let subspecies = subspeciesList[subspeciesName];
|
||||
if ( subspecies.skills) {
|
||||
if (subspecies.skills) {
|
||||
subspecies.skills = this.translateSkillList(subspecies.skills);
|
||||
}
|
||||
if ( subspecies.talents) {
|
||||
if (subspecies.talents) {
|
||||
subspecies.talents = this.translateTalentList(subspecies.talents);
|
||||
}
|
||||
}
|
||||
@@ -76,20 +100,26 @@ export class WH4FRPatchConfig {
|
||||
}
|
||||
|
||||
/************************************************************************************/
|
||||
static patch_species_skills( ) {
|
||||
static patch_species_skills() {
|
||||
if (!game.wfrp4e?.config?.speciesSkills) {
|
||||
return
|
||||
}
|
||||
console.log("Patching species skills....");
|
||||
for (let speciesName in game.wfrp4e.config.speciesSkills) {
|
||||
let speciesComp = game.wfrp4e.config.speciesSkills[speciesName];
|
||||
console.log("SpeciesName", speciesName, speciesComp);
|
||||
game.wfrp4e.config.speciesSkills[speciesName] = this.translateSkillList( speciesComp )
|
||||
game.wfrp4e.config.speciesSkills[speciesName] = this.translateSkillList(speciesComp)
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************/
|
||||
static patch_species_talents( ) {
|
||||
static patch_species_talents() {
|
||||
if (!game.wfrp4e?.config?.speciesTalents) {
|
||||
return
|
||||
}
|
||||
for (let speciesName in game.wfrp4e.config.speciesTalents) {
|
||||
let speciesTalents = game.wfrp4e.config.speciesTalents[speciesName];
|
||||
game.wfrp4e.config.speciesTalents[speciesName] = this.translateTalentList( speciesTalents);
|
||||
game.wfrp4e.config.speciesTalents[speciesName] = this.translateTalentList(speciesTalents);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,16 +127,16 @@ export class WH4FRPatchConfig {
|
||||
static patch_career() {
|
||||
let compendiumName = 'wfrp4e-core.items'
|
||||
|
||||
if ( game.wfrp4e.tables.career) {
|
||||
for( let row of game.wfrp4e.tables.career.rows) {
|
||||
for ( let key in row) {
|
||||
if (game.wfrp4e?.tables?.career) {
|
||||
for (let row of game.wfrp4e.tables.career.rows) {
|
||||
for (let key in row) {
|
||||
if (key != "range") {
|
||||
if ( row[key].name == 'Slayer' ) {
|
||||
if (row[key].name == 'Slayer') {
|
||||
row[key].name = "Tueur Nains";
|
||||
} else if ( row[key].name == 'Duelist' ) {
|
||||
} else if (row[key].name == 'Duelist') {
|
||||
row[key].name = "Duelliste";
|
||||
} else {
|
||||
let career_fr = game.babele.translate( compendiumName, {name: row[key].name}, true );
|
||||
let career_fr = game.babele.translate(compendiumName, { name: row[key].name }, true);
|
||||
row[key].name = career_fr.name;
|
||||
}
|
||||
}
|
||||
@@ -115,6 +145,21 @@ export class WH4FRPatchConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************/
|
||||
static fixSpeciesTable() {
|
||||
|
||||
let speciesTable = game.wfrp4e?.tables?.findTable("species");
|
||||
if (!speciesTable?.results) {
|
||||
return
|
||||
}
|
||||
let newResults = foundry.utils.duplicate(speciesTable.results);
|
||||
for (let result of newResults) {
|
||||
result.name = game.i18n.localize(result.name);
|
||||
}
|
||||
speciesTable.update({ results: newResults })
|
||||
console.log("Species table patched to use 'Humain' instead of 'Human'", speciesTable);
|
||||
}
|
||||
|
||||
/************************************************************************************/
|
||||
static perform_patch() {
|
||||
|
||||
@@ -127,7 +172,10 @@ export class WH4FRPatchConfig {
|
||||
}
|
||||
|
||||
// Detect and patch as necessary
|
||||
if (game.wfrp4e.config?.talentBonuses ) {
|
||||
if (game.wfrp4e.config?.talentBonuses) {
|
||||
|
||||
this.fixSpeciesTable() // Force 'name' field replacement
|
||||
|
||||
game.wfrp4e.config.qualityDescriptions["distract"] = game.i18n.localize("WFRP4E.Properties.Distract"); // Patch missing quality
|
||||
|
||||
game.wfrp4e.config.talentBonuses = {
|
||||
@@ -166,22 +214,22 @@ export class WH4FRPatchConfig {
|
||||
|
||||
if (game.wfrp4e.config.characteristicsBonus) {
|
||||
game.wfrp4e.config.characteristicsBonus =
|
||||
{
|
||||
"ws": "Bonus de Capacité de Combat",
|
||||
"bs": "Bonus de Capacité de Tir",
|
||||
"s": "Bonus de Force",
|
||||
"t": "Bonus d'Endurance",
|
||||
"i": "Bonus d'Initiative",
|
||||
"ag": "Bonus d'Agilité",
|
||||
"dex": "Bonus de Dexterité",
|
||||
"int": "Bonus d'Intelligence",
|
||||
"wp": "Bonus de Force Mentale",
|
||||
"fel": "Bonus de Sociabilité"
|
||||
{
|
||||
"ws": "Bonus de Capacité de Combat",
|
||||
"bs": "Bonus de Capacité de Tir",
|
||||
"s": "Bonus de Force",
|
||||
"t": "Bonus d'Endurance",
|
||||
"i": "Bonus d'Initiative",
|
||||
"ag": "Bonus d'Agilité",
|
||||
"dex": "Bonus de Dexterité",
|
||||
"int": "Bonus d'Intelligence",
|
||||
"wp": "Bonus de Force Mentale",
|
||||
"fel": "Bonus de Sociabilité"
|
||||
}
|
||||
}
|
||||
|
||||
if (game.wfrp4e.config.classTrappings) {
|
||||
for(const c of Object.keys(game.wfrp4e.config.classTrappings)) {
|
||||
for (const c of Object.keys(game.wfrp4e.config.classTrappings)) {
|
||||
game.wfrp4e.config.classTrappings[game.i18n.localize(c)] = game.wfrp4e.config.classTrappings[c];
|
||||
}
|
||||
}
|
||||
|
||||
410
modules/inn/InnRoller.js
Normal file
410
modules/inn/InnRoller.js
Normal file
@@ -0,0 +1,410 @@
|
||||
/**
|
||||
* InnRoller
|
||||
* Classe de gestion des jets de tables d'auberge pour WFRP4e
|
||||
* Module de traduction française
|
||||
*/
|
||||
|
||||
export default class InnRoller {
|
||||
static tableNames = {
|
||||
'boissonsbase': 'BoissonsBase',
|
||||
'boissonsfortes': 'BoissonsFortes',
|
||||
'desserts': 'Desserts',
|
||||
'platscommuns': 'PlatsCommuns',
|
||||
'platsexcellents': 'PlatsExcellents',
|
||||
'platsmaritimes': 'PlatsMaritimes',
|
||||
'platsmediocres': 'PlatsMédiocres',
|
||||
'platsqualite': 'PlatsQualité',
|
||||
'platsrivieres': 'PlatsRivières'
|
||||
};
|
||||
|
||||
static displayNames = {
|
||||
'BoissonsBase': 'Boissons de Base',
|
||||
'BoissonsFortes': 'Boissons Fortes',
|
||||
'Desserts': 'Desserts',
|
||||
'PlatsCommuns': 'Plats Communs',
|
||||
'PlatsExcellents': 'Plats Excellents',
|
||||
'PlatsMaritimes': 'Plats Maritimes',
|
||||
'PlatsMédiocres': 'Plats Médiocres',
|
||||
'PlatsQualité': 'Plats de Qualité',
|
||||
'PlatsRivières': 'Plats de Rivières'
|
||||
};
|
||||
|
||||
/**
|
||||
* Obtient le nom d'affichage formaté pour une table
|
||||
* @param {String} tableName
|
||||
* @returns {String}
|
||||
*/
|
||||
static getDisplayName(tableName) {
|
||||
return this.displayNames[tableName] || tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise le nom d'une table (enlève accents, espaces, met en minuscules)
|
||||
* @param {String} name
|
||||
* @returns {String}
|
||||
*/
|
||||
static normalizeTableName(name) {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/\s+/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve la table correspondant au mot-clé
|
||||
* @param {String} keyword
|
||||
* @returns {String|null}
|
||||
*/
|
||||
static findTableByKeyword(keyword) {
|
||||
if (!keyword) return null;
|
||||
|
||||
const normalized = this.normalizeTableName(keyword);
|
||||
|
||||
// Recherche exacte
|
||||
if (this.tableNames[normalized]) {
|
||||
return this.tableNames[normalized];
|
||||
}
|
||||
|
||||
// Recherche partielle
|
||||
for (let [key, value] of Object.entries(this.tableNames)) {
|
||||
if (key.includes(normalized) || normalized.includes(key)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lance un jet sur une table d'auberge
|
||||
* @param {String} keyword Mot-clé pour identifier la table
|
||||
*/
|
||||
static async rollInnTable(keyword) {
|
||||
console.log(`InnRoller: rollInnTable appelé avec keyword="${keyword}"`);
|
||||
|
||||
// Si pas de keyword, afficher l'aide
|
||||
if (!keyword) {
|
||||
this.displayHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
// Rechercher la table
|
||||
const tableName = this.findTableByKeyword(keyword);
|
||||
|
||||
if (!tableName) {
|
||||
this.displayHelp();
|
||||
ui.notifications.warn(`Table d'auberge introuvable pour le mot-clé: "${keyword}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`InnRoller: Table trouvée: ${tableName}`);
|
||||
|
||||
// Charger le compendium
|
||||
const compendium = game.packs.get('wh4-fr-translation.plats-dauberges');
|
||||
|
||||
if (!compendium) {
|
||||
ui.notifications.error("Compendium 'plats-dauberges' introuvable");
|
||||
console.error("InnRoller: Compendium wh4-fr-translation.plats-dauberges non trouvé");
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer les tables
|
||||
const tables = await compendium.getDocuments();
|
||||
|
||||
// Trouver la table correspondante
|
||||
const rollTable = tables.find(t => t.name === tableName);
|
||||
|
||||
if (!rollTable) {
|
||||
ui.notifications.error(`Table "${tableName}" non trouvée dans le compendium`);
|
||||
console.error(`InnRoller: Table ${tableName} non trouvée`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`InnRoller: Jet sur la table ${rollTable.name}`);
|
||||
|
||||
// Effectuer le jet sans affichage automatique
|
||||
try {
|
||||
const roll = await rollTable.draw({ displayChat: false });
|
||||
console.log(`InnRoller: Jet effectué avec succès`, roll);
|
||||
|
||||
// Créer un message personnalisé
|
||||
await this.displayRollResult(rollTable.name, roll);
|
||||
} catch (error) {
|
||||
console.error("InnRoller: Erreur lors du jet:", error);
|
||||
ui.notifications.error("Erreur lors du jet sur la table d'auberge");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche le résultat d'un jet de table avec un style personnalisé
|
||||
* @param {String} tableName Nom de la table
|
||||
* @param {Object} rollResult Résultat du jet
|
||||
*/
|
||||
static async displayRollResult(tableName, rollResult) {
|
||||
// Déterminer l'icône en fonction du type de table
|
||||
let icon = "fa-utensils";
|
||||
let category = "Plat";
|
||||
|
||||
if (tableName.toLowerCase().includes('boisson')) {
|
||||
icon = "fa-wine-glass";
|
||||
category = "Boisson";
|
||||
} else if (tableName.toLowerCase().includes('dessert')) {
|
||||
icon = "fa-birthday-cake";
|
||||
category = "Dessert";
|
||||
}
|
||||
|
||||
// Extraire les informations du résultat
|
||||
const resultText = rollResult.results[0]?.text || "Résultat inconnu";
|
||||
const rollFormula = rollResult.roll?.formula || "1d100";
|
||||
const rollTotal = rollResult.roll?.total || 0;
|
||||
|
||||
// Construire le message HTML simplifié
|
||||
let message = `<div class="wfrp4e-inn-result">`;
|
||||
message += `<div class="message-header">`;
|
||||
message += `<i class="fas ${icon}"></i> `;
|
||||
message += `<span class="flavor-text">${category}: ${tableName}</span>`;
|
||||
message += `</div>`;
|
||||
message += `<div class="inn-dish-name">${resultText}</div>`;
|
||||
message += `<div class="inn-roll-info"><i class="fas fa-dice"></i> ${rollFormula} = ${rollTotal}</div>`;
|
||||
message += `</div>`;
|
||||
|
||||
// Créer le message dans le chat
|
||||
await ChatMessage.create({
|
||||
content: message,
|
||||
speaker: ChatMessage.getSpeaker(),
|
||||
whisper: ChatMessage.getWhisperRecipients("GM")
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche l'aide pour la commande /auberge avec liste cliquable
|
||||
*/
|
||||
static displayHelp() {
|
||||
let message = `<div class="wfrp4e-inn-help">`;
|
||||
message += `<h3><i class="fas fa-utensils"></i> Aide pour /auberge</h3>`;
|
||||
message += `<p><strong>Usage:</strong> <code>/auberge [mot_clé]</code></p>`;
|
||||
|
||||
// Bouton Menu
|
||||
message += `<div style="margin: 0.8em 0;">`;
|
||||
message += `<a class="action-link inn-menu-quick-btn" data-action="clickAubergeMenu" data-quality="menu">`;
|
||||
message += `<i class="fas fa-book-open"></i> Générer un menu complet`;
|
||||
message += `</a>`;
|
||||
message += `</div>`;
|
||||
|
||||
message += `<hr>`;
|
||||
|
||||
// Section avec liste cliquable
|
||||
message += `<h4><i class="fas fa-list"></i> Tables disponibles</h4>`;
|
||||
message += `<div class="wfrp4e-inn-table-grid">`;
|
||||
|
||||
const sortedTables = Object.values(this.tableNames).sort();
|
||||
for (let tableName of sortedTables) {
|
||||
const normalized = this.normalizeTableName(tableName);
|
||||
const displayName = this.getDisplayName(tableName);
|
||||
message += `<a class="action-link inn-table-btn" data-action="clickAuberge" data-table="${normalized}">`;
|
||||
message += `<i class="fas fa-dice"></i> ${displayName}`;
|
||||
message += `</a>`;
|
||||
}
|
||||
|
||||
message += `</div>`;
|
||||
message += `<hr>`;
|
||||
message += `<p style="font-size: 0.9em; margin-top: 0.5em;"><em>Vous pouvez aussi taper <code>/auberge [mot_clé]</code> directement (ex: <code>/auberge base</code>)</em></p>`;
|
||||
message += `</div>`;
|
||||
|
||||
ChatMessage.create({
|
||||
content: message,
|
||||
whisper: ChatMessage.getWhisperRecipients("GM")
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste toutes les tables disponibles
|
||||
*/
|
||||
static listTables() {
|
||||
let message = `<div class="wfrp4e-inn-list">`;
|
||||
message += `<h3><i class="fas fa-list"></i> Tables d'auberge disponibles</h3>`;
|
||||
message += `<div class="wfrp4e-inn-table-grid">`;
|
||||
|
||||
const sortedTables = Object.values(this.tableNames).sort();
|
||||
for (let tableName of sortedTables) {
|
||||
const normalized = this.normalizeTableName(tableName);
|
||||
const displayName = this.getDisplayName(tableName);
|
||||
message += `<a class="action-link inn-table-btn" data-action="clickAuberge" data-table="${normalized}">`;
|
||||
message += `<i class="fas fa-dice"></i> ${displayName}`;
|
||||
message += `</a>`;
|
||||
}
|
||||
|
||||
message += `</div>`;
|
||||
message += `</div>`;
|
||||
|
||||
ChatMessage.create({
|
||||
content: message,
|
||||
whisper: ChatMessage.getWhisperRecipients("GM")
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche le choix de qualité pour générer un menu complet
|
||||
*/
|
||||
static displayMenuChoice() {
|
||||
let message = `<div class="wfrp4e-inn-menu-choice">`;
|
||||
message += `<h3><i class="fas fa-book-open"></i> Menu de l'auberge</h3>`;
|
||||
message += `<p>Choisissez la qualité du menu :</p>`;
|
||||
message += `<div class="inn-menu-buttons">`;
|
||||
|
||||
message += `<a class="action-link inn-menu-btn" data-action="clickAubergeMenu" data-quality="mediocre">`;
|
||||
message += `<i class="fas fa-drumstick-bite"></i> Menu Médiocre`;
|
||||
message += `<br><span class="inn-menu-desc">Plat médiocre + Boisson de base</span>`;
|
||||
message += `</a>`;
|
||||
|
||||
message += `<a class="action-link inn-menu-btn" data-action="clickAubergeMenu" data-quality="commun">`;
|
||||
message += `<i class="fas fa-utensils"></i> Menu Commun`;
|
||||
message += `<br><span class="inn-menu-desc">Plat commun + Boisson de base + Dessert</span>`;
|
||||
message += `</a>`;
|
||||
|
||||
message += `<a class="action-link inn-menu-btn" data-action="clickAubergeMenu" data-quality="qualite">`;
|
||||
message += `<i class="fas fa-crown"></i> Menu de Qualité`;
|
||||
message += `<br><span class="inn-menu-desc">Plat de qualité + Boisson forte + Dessert</span>`;
|
||||
message += `</a>`;
|
||||
|
||||
message += `<a class="action-link inn-menu-btn" data-action="clickAubergeMenu" data-quality="fluvial">`;
|
||||
message += `<i class="fas fa-fish"></i> Menu Fluvial`;
|
||||
message += `<br><span class="inn-menu-desc">Plat de rivière + Boisson de base + Dessert</span>`;
|
||||
message += `</a>`;
|
||||
|
||||
message += `<a class="action-link inn-menu-btn" data-action="clickAubergeMenu" data-quality="maritime">`;
|
||||
message += `<i class="fas fa-anchor"></i> Menu Maritime`;
|
||||
message += `<br><span class="inn-menu-desc">Plat maritime + Boisson forte + Dessert</span>`;
|
||||
message += `</a>`;
|
||||
|
||||
message += `<a class="action-link inn-menu-btn" data-action="clickAubergeMenu" data-quality="excellent">`;
|
||||
message += `<i class="fas fa-gem"></i> Menu Excellent`;
|
||||
message += `<br><span class="inn-menu-desc">Plat excellent + Boisson forte + Dessert</span>`;
|
||||
message += `</a>`;
|
||||
|
||||
message += `</div>`;
|
||||
message += `</div>`;
|
||||
|
||||
ChatMessage.create({
|
||||
content: message,
|
||||
whisper: ChatMessage.getWhisperRecipients("GM")
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un menu complet selon la qualité choisie
|
||||
* @param {String} quality - 'mediocre', 'commun', ou 'qualite'
|
||||
*/
|
||||
static async generateMenu(quality) {
|
||||
console.log(`InnRoller: generateMenu appelé avec quality="${quality}"`);
|
||||
|
||||
let tables = [];
|
||||
let menuName = "";
|
||||
|
||||
// Définir les tables à tirer selon la qualité
|
||||
switch(quality) {
|
||||
case 'mediocre':
|
||||
menuName = "Menu Médiocre";
|
||||
tables = ['PlatsMédiocres', 'BoissonsBase'];
|
||||
break;
|
||||
case 'commun':
|
||||
menuName = "Menu Commun";
|
||||
tables = ['PlatsCommuns', 'BoissonsBase', 'Desserts'];
|
||||
break;
|
||||
case 'qualite':
|
||||
menuName = "Menu de Qualité";
|
||||
tables = ['PlatsQualité', 'BoissonsFortes', 'Desserts'];
|
||||
break;
|
||||
case 'fluvial':
|
||||
menuName = "Menu Fluvial";
|
||||
tables = ['PlatsRivières', 'BoissonsBase', 'Desserts'];
|
||||
break;
|
||||
case 'maritime':
|
||||
menuName = "Menu Maritime";
|
||||
tables = ['PlatsMaritimes', 'BoissonsFortes', 'Desserts'];
|
||||
break;
|
||||
case 'excellent':
|
||||
menuName = "Menu Excellent";
|
||||
tables = ['PlatsExcellents', 'BoissonsFortes', 'Desserts'];
|
||||
break;
|
||||
default:
|
||||
ui.notifications.error(`Qualité de menu inconnue: ${quality}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Charger le compendium
|
||||
const compendium = game.packs.get('wh4-fr-translation.plats-dauberges');
|
||||
if (!compendium) {
|
||||
ui.notifications.error("Compendium 'plats-dauberges' introuvable");
|
||||
return;
|
||||
}
|
||||
|
||||
const allTables = await compendium.getDocuments();
|
||||
let results = [];
|
||||
|
||||
// Effectuer les jets sur chaque table
|
||||
for (let tableName of tables) {
|
||||
const rollTable = allTables.find(t => t.name === tableName);
|
||||
if (rollTable) {
|
||||
try {
|
||||
const roll = await rollTable.draw({ displayChat: false });
|
||||
const resultText = roll.results[0]?.text || "Résultat inconnu";
|
||||
results.push({
|
||||
category: this.getCategoryName(tableName),
|
||||
name: resultText,
|
||||
tableName: tableName
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`InnRoller: Erreur lors du jet sur ${tableName}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Afficher le menu complet
|
||||
this.displayMenuResult(menuName, results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient le nom de catégorie pour une table
|
||||
* @param {String} tableName
|
||||
* @returns {String}
|
||||
*/
|
||||
static getCategoryName(tableName) {
|
||||
if (tableName.includes('Boisson')) return 'Boisson';
|
||||
if (tableName.includes('Dessert')) return 'Dessert';
|
||||
if (tableName.includes('Plat')) return 'Plat';
|
||||
return 'Item';
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche le résultat d'un menu complet
|
||||
* @param {String} menuName
|
||||
* @param {Array} results
|
||||
*/
|
||||
static async displayMenuResult(menuName, results) {
|
||||
let message = `<div class="wfrp4e-inn-menu-result">`;
|
||||
message += `<div class="message-header">`;
|
||||
message += `<i class="fas fa-book-open"></i> `;
|
||||
message += `<span class="flavor-text">${menuName}</span>`;
|
||||
message += `</div>`;
|
||||
|
||||
message += `<div class="inn-menu-items">`;
|
||||
for (let result of results) {
|
||||
message += `<div class="inn-menu-item">`;
|
||||
let icon = result.category === 'Boisson' ? 'fa-wine-glass' :
|
||||
result.category === 'Dessert' ? 'fa-birthday-cake' : 'fa-utensils';
|
||||
message += `<i class="fas ${icon}"></i> `;
|
||||
message += `<strong>${result.category}:</strong> ${result.name}`;
|
||||
message += `</div>`;
|
||||
}
|
||||
message += `</div>`;
|
||||
message += `</div>`;
|
||||
|
||||
await ChatMessage.create({
|
||||
content: message,
|
||||
speaker: ChatMessage.getSpeaker(),
|
||||
whisper: ChatMessage.getWhisperRecipients("GM")
|
||||
});
|
||||
}
|
||||
}
|
||||
148
modules/inn/README.md
Normal file
148
modules/inn/README.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Module Inn (Auberge)
|
||||
|
||||
Module de gestion des jets sur les tables d'auberge pour WFRP4e - Traduction française.
|
||||
|
||||
## Utilisation
|
||||
|
||||
### Commande `/auberge`
|
||||
|
||||
La commande `/auberge` permet d'effectuer des jets sur les tables d'auberge (plats et boissons).
|
||||
|
||||
**Syntaxe :**
|
||||
```
|
||||
/auberge [mot_clé]
|
||||
```
|
||||
|
||||
### Exemples
|
||||
|
||||
- `/auberge` - Affiche l'aide avec toutes les tables disponibles **et cliquables**
|
||||
- `/auberge help` ou `/auberge aide` - Affiche l'aide avec liste cliquable
|
||||
- `/auberge list` ou `/auberge liste` - Liste toutes les tables avec liens cliquables
|
||||
- `/auberge menu` - **Génère un menu complet** (6 types disponibles)
|
||||
- `/auberge base` - Lance un jet sur la table "BoissonsBase"
|
||||
- `/auberge fortes` - Lance un jet sur la table "BoissonsFortes"
|
||||
- `/auberge mari` - Lance un jet sur la table "PlatsMaritimes"
|
||||
|
||||
> **Note :** Quand vous tapez `/auberge` sans argument, une liste cliquable s'affiche dans le chat. Vous pouvez cliquer directement sur une table pour effectuer un jet.
|
||||
|
||||
### Génération de menus complets
|
||||
|
||||
La commande `/auberge menu` permet de générer automatiquement un menu complet :
|
||||
|
||||
1. Tapez `/auberge menu` dans le chat
|
||||
2. Cliquez sur le type de menu souhaité :
|
||||
- **Menu Médiocre** 🍗 : Plat médiocre + Boisson de base
|
||||
- **Menu Commun** 🍽️ : Plat commun + Boisson de base + Dessert
|
||||
- **Menu de Qualité** 👑 : Plat de qualité + Boisson forte + Dessert
|
||||
- **Menu Fluvial** 🐟 : Plat de rivière + Boisson de base + Dessert
|
||||
- **Menu Maritime** ⚓ : Plat maritime + Boisson forte + Dessert
|
||||
- **Menu Excellent** 💎 : Plat excellent + Boisson forte + Dessert
|
||||
3. Le menu complet est généré automatiquement avec un jet sur chaque table concernée
|
||||
|
||||
### Tables disponibles
|
||||
|
||||
- **BoissonsBase** (`boissonsbase`, `base`)
|
||||
- **BoissonsFortes** (`boissonsfortes`, `fortes`)
|
||||
- **Desserts** (`desserts`)
|
||||
- **PlatsCommuns** (`platscommuns`, `communs`)
|
||||
- **PlatsExcellents** (`platsexcellents`, `excellents`)
|
||||
- **PlatsMaritimes** (`platsmaritimes`, `maritimes`, `mari`)
|
||||
- **PlatsMédiocres** (`platsmediocres`, `mediocres`)
|
||||
- **PlatsQualité** (`platsqualite`, `qualite`)
|
||||
- **PlatsRivières** (`platsrivieres`, `rivieres`)
|
||||
|
||||
### Raccourcis
|
||||
|
||||
Le système accepte des raccourcis et ignore les accents :
|
||||
- `mari` → PlatsMaritimes
|
||||
- `qualité` ou `qualite` → PlatsQualité
|
||||
- `médiocres` ou `mediocres` → PlatsMédiocres
|
||||
|
||||
## Architecture
|
||||
|
||||
Le module suit la même architecture que le module TravelV2 (commande `/voyage`) :
|
||||
|
||||
```
|
||||
modules/inn/
|
||||
├── inn-init.js # Initialisation et enregistrement de la commande
|
||||
└── InnRoller.js # Logique métier des jets de tables
|
||||
```
|
||||
|
||||
### Affichage personnalisé
|
||||
|
||||
Les résultats des jets sont affichés avec un **rendu visuel personnalisé** :
|
||||
- 🍷 Icône adaptée au type (boisson, plat, dessert)
|
||||
- 🎨 Carte stylisée avec dégradés et bordures
|
||||
- 🎲 Affichage du jet de dés (formule et total)
|
||||
- 📋 Nom de la table et du plat mis en valeur
|
||||
- 🎉 Message de conclusion thématique
|
||||
|
||||
Le système détecte automatiquement le type de plat/boisson et adapte l'icône :
|
||||
- **Boissons** : 🍷 Verre de vin
|
||||
- **Desserts** : 🎂 Gâteau
|
||||
- **Plats** : 🍴 Couverts
|
||||
|
||||
### Fichiers principaux
|
||||
|
||||
- **inn-init.js** :
|
||||
- Enregistre la commande `/auberge` via `game.wfrp4e.commands`
|
||||
- Gère les hooks pour les clics sur les liens de tables
|
||||
- Expose `game.wfrp4e.inn` pour accès programmatique
|
||||
|
||||
- **InnRoller.js** :
|
||||
- Gestion des jets sur les tables d'auberge
|
||||
- Normalisation des noms de tables
|
||||
- Recherche par mots-clés
|
||||
- Affichage de l'aide et de la liste des tables
|
||||
|
||||
## Intégration
|
||||
|
||||
Le module est initialisé dans `wh4_fr.js` :
|
||||
|
||||
```javascript
|
||||
import { initInn } from './modules/inn/inn-init.js';
|
||||
|
||||
Hooks.once("init", function() {
|
||||
initInn();
|
||||
});
|
||||
```
|
||||
|
||||
## Dépendances
|
||||
|
||||
- Compendium : `wh4-fr-translation.plats-dauberges`
|
||||
- Système WFRP4e avec support de `game.wfrp4e.commands`
|
||||
|
||||
## Permissions
|
||||
|
||||
La commande `/auberge` est réservée au MJ (GM).
|
||||
|
||||
## API Programmatique
|
||||
|
||||
```javascript
|
||||
// Afficher l'aide
|
||||
game.wfrp4e.inn.displayHelp();
|
||||
|
||||
// Lister les tables
|
||||
game.wfrp4e.inn.listTables();
|
||||
|
||||
// Effectuer un jet
|
||||
game.wfrp4e.inn.rollInnTable('base');
|
||||
|
||||
// Trouver une table par mot-clé
|
||||
const tableName = game.wfrp4e.inn.findTableByKeyword('mari');
|
||||
```
|
||||
|
||||
## Migration depuis l'ancien système
|
||||
|
||||
L'ancienne implémentation dans `addon-register.js` (`_manage_inn_roll`) a été remplacée par ce module pour :
|
||||
- Cohérence avec le module TravelV2
|
||||
- Meilleure maintenabilité
|
||||
- Support des commandes WFRP4e natives
|
||||
- Interface utilisateur améliorée
|
||||
|
||||
## Styles CSS
|
||||
|
||||
Les styles sont définis dans `patch-styles.css` avec les classes :
|
||||
- `.wfrp4e-inn-help` - Aide de la commande
|
||||
- `.wfrp4e-inn-list` - Liste des tables
|
||||
- `.wfrp4e-inn-table-list` - Liste avec liens cliquables
|
||||
93
modules/inn/inn-init.js
Normal file
93
modules/inn/inn-init.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import InnRoller from './InnRoller.js';
|
||||
|
||||
/**
|
||||
* Initialisation du module Inn (Auberge)
|
||||
*/
|
||||
export function initInn() {
|
||||
console.log("Inn: Initialisation du module d'auberge");
|
||||
|
||||
// Hook pour initialiser au démarrage
|
||||
Hooks.once('ready', async () => {
|
||||
console.log("Inn: Module d'auberge prêt");
|
||||
|
||||
// Exposer la classe globalement
|
||||
game.wfrp4e = game.wfrp4e || {};
|
||||
game.wfrp4e.inn = InnRoller;
|
||||
|
||||
console.log("Inn: Classe accessible via game.wfrp4e.inn");
|
||||
|
||||
// Enregistrer la commande dans le système WFRP4e si disponible
|
||||
if (game.wfrp4e?.commands) {
|
||||
console.log("Inn: Enregistrement de la commande /auberge");
|
||||
game.wfrp4e.commands.add({
|
||||
auberge: {
|
||||
description: "Jets sur les tables d'auberge (FR)",
|
||||
args: ["table"],
|
||||
defaultArg: "table",
|
||||
callback: (table) => {
|
||||
// Vérifier que l'utilisateur est GM
|
||||
if (!game.user.isGM) {
|
||||
ui.notifications.warn("La commande /auberge est réservée au MJ.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Inn: Commande /auberge exécutée avec table="${table}"`);
|
||||
|
||||
// Convertir null en undefined
|
||||
table = table || undefined;
|
||||
|
||||
// Si pas de table spécifiée, afficher l'aide
|
||||
if (!table) {
|
||||
InnRoller.displayHelp();
|
||||
} else if (table === 'help' || table === 'aide') {
|
||||
InnRoller.displayHelp();
|
||||
} else if (table === 'list' || table === 'liste') {
|
||||
InnRoller.listTables();
|
||||
} else if (table === 'menu') {
|
||||
InnRoller.displayMenuChoice();
|
||||
} else {
|
||||
InnRoller.rollInnTable(table);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log("Inn: Commande /auberge enregistrée avec succès");
|
||||
} else {
|
||||
console.warn("Inn: game.wfrp4e.commands non disponible");
|
||||
}
|
||||
});
|
||||
|
||||
// Hook pour gérer les clics sur les liens de tables d'auberge
|
||||
Hooks.on('renderChatMessage', (message, html, data) => {
|
||||
// Ajouter un listener pour les clics sur les liens d'auberge
|
||||
html.find('a[data-action="clickAuberge"]').click((event) => {
|
||||
event.preventDefault();
|
||||
const tableKey = $(event.currentTarget).data('table');
|
||||
console.log(`Inn: Clic sur la table ${tableKey}`);
|
||||
|
||||
if (game.user.isGM) {
|
||||
InnRoller.rollInnTable(tableKey);
|
||||
} else {
|
||||
ui.notifications.warn("Seul le MJ peut utiliser les tables d'auberge.");
|
||||
}
|
||||
});
|
||||
|
||||
// Ajouter un listener pour les clics sur les boutons de menu
|
||||
html.find('a[data-action="clickAubergeMenu"]').click((event) => {
|
||||
event.preventDefault();
|
||||
const quality = $(event.currentTarget).data('quality');
|
||||
console.log(`Inn: Clic sur menu de qualité "${quality}"`);
|
||||
|
||||
if (game.user.isGM) {
|
||||
// Si quality === "menu", afficher le choix, sinon générer directement
|
||||
if (quality === 'menu') {
|
||||
InnRoller.displayMenuChoice();
|
||||
} else {
|
||||
InnRoller.generateMenu(quality);
|
||||
}
|
||||
} else {
|
||||
ui.notifications.warn("Seul le MJ peut utiliser les tables d'auberge.");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
339
modules/travelv2/TravelDistanceV2.js
Normal file
339
modules/travelv2/TravelDistanceV2.js
Normal file
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* TravelDistanceV2
|
||||
* Classe de gestion du calcul des distances de voyage pour WFRP4e
|
||||
* Version adaptée pour le module de traduction française
|
||||
*/
|
||||
import { PathFinder } from './pathfinding.js';
|
||||
|
||||
export default class TravelDistanceV2 {
|
||||
static roadGraph = null; // Graphe pour les routes terrestres uniquement
|
||||
static waterGraph = null; // Graphe pour les voies fluviales et maritimes
|
||||
static mixedGraph = null; // Graphe combinant tous les modes de transport
|
||||
|
||||
/**
|
||||
* Charge les données de voyage depuis le fichier JSON
|
||||
*/
|
||||
static async loadTravelData() {
|
||||
try {
|
||||
console.log("TravelV2: Début du chargement des données...");
|
||||
const response = await fetch('modules/wh4-fr-translation/modules/travelv2/travel_data.json');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
this.travel_data = await response.json();
|
||||
console.log(`TravelV2: ${this.travel_data.length} routes chargées avec succès`);
|
||||
|
||||
// Construire les 3 graphes pour le pathfinding
|
||||
this.roadGraph = PathFinder.buildGraph(this.travel_data, 'road');
|
||||
this.waterGraph = PathFinder.buildGraph(this.travel_data, 'water');
|
||||
this.mixedGraph = PathFinder.buildGraph(this.travel_data, 'mixed');
|
||||
|
||||
console.log(`TravelV2: Graphe routier: ${Object.keys(this.roadGraph).length} villes`);
|
||||
console.log(`TravelV2: Graphe fluvial/maritime: ${Object.keys(this.waterGraph).length} villes`);
|
||||
console.log(`TravelV2: Graphe mixte: ${Object.keys(this.mixedGraph).length} villes`);
|
||||
|
||||
ui.notifications.info(`TravelV2: ${this.travel_data.length} routes de voyage chargées`);
|
||||
} catch (error) {
|
||||
console.error("TravelV2: Erreur lors du chargement des données de voyage:", error);
|
||||
ui.notifications.error("Erreur lors du chargement des données de voyage. Vérifiez la console.");
|
||||
this.travel_data = []; // Initialiser avec un tableau vide pour éviter les erreurs
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne une chaîne lisible pour le niveau de danger
|
||||
* @param {String} dangerLevel
|
||||
* @returns {String}
|
||||
*/
|
||||
static dangerToString(dangerLevel) {
|
||||
if (dangerLevel == "") return "Très bas";
|
||||
if (dangerLevel == '!') return "Bas";
|
||||
if (dangerLevel == '!!') return "Moyen";
|
||||
if (dangerLevel == '!!!') return "Élevé";
|
||||
return "Très élevé";
|
||||
}
|
||||
|
||||
/**
|
||||
* Arrondit la durée à une valeur entière ou .5
|
||||
* @param {Number} duration
|
||||
* @returns {Number}
|
||||
*/
|
||||
static roundDuration(duration) {
|
||||
let trunc = Math.trunc(duration);
|
||||
let frac = duration - trunc;
|
||||
let adjust = 0;
|
||||
if (frac > 0.75) adjust = 1;
|
||||
else if (frac >= 0.25) adjust = 0.5;
|
||||
return trunc + adjust;
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche les distances de voyage entre deux villes ou liste les destinations
|
||||
* @param {String} fromTown Ville de départ
|
||||
* @param {String} toTown Ville d'arrivée (optionnel)
|
||||
*/
|
||||
static displayTravelDistance(fromTown, toTown) {
|
||||
// Vérifier que les données sont chargées
|
||||
if (!this.travel_data || this.travel_data.length === 0) {
|
||||
ui.notifications.error("Les données de voyage ne sont pas encore chargées. Veuillez patienter...");
|
||||
console.error("TravelV2: travel_data n'est pas chargé");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`TravelV2: displayTravelDistance appelé avec fromTown="${fromTown}", toTown="${toTown}"`);
|
||||
console.log(`TravelV2: fromTown type: ${typeof fromTown}, valeur falsy: ${!fromTown}`);
|
||||
console.log(`TravelV2: toTown type: ${typeof toTown}, valeur falsy: ${!toTown}`);
|
||||
console.log(`TravelV2: ${this.travel_data.length} routes disponibles`);
|
||||
|
||||
let message = "";
|
||||
|
||||
console.log("TravelV2: Vérification des conditions...");
|
||||
console.log(`TravelV2: toTown ? ${!!toTown}`);
|
||||
console.log(`TravelV2: fromTown == 'help' ? ${fromTown == 'help'}`);
|
||||
console.log(`TravelV2: fromTown ? ${!!fromTown}`);
|
||||
console.log(`TravelV2: else (pas de fromTown) ? ${!fromTown && !toTown}`);
|
||||
|
||||
if (toTown) {
|
||||
console.log("TravelV2: Branche: Affichage des détails entre deux villes");
|
||||
// Afficher les détails de voyage entre deux villes spécifiques
|
||||
const originalFrom = fromTown;
|
||||
const originalTo = toTown;
|
||||
fromTown = fromTown.toLowerCase();
|
||||
toTown = toTown.toLowerCase();
|
||||
|
||||
// Chercher d'abord une route directe
|
||||
let directRoute = null;
|
||||
for (const travel of this.travel_data) {
|
||||
if (travel.from.toLowerCase() == fromTown && travel.to.toLowerCase() == toTown) {
|
||||
directRoute = travel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (directRoute) {
|
||||
// Route directe trouvée - affichage classique avec toutes les options
|
||||
message += this.formatDirectRoute(directRoute);
|
||||
} else {
|
||||
// Pas de route directe - calculer les 3 types de trajets
|
||||
console.log("TravelV2: Pas de route directe, calcul des itinéraires...");
|
||||
|
||||
const mixedPath = PathFinder.dijkstra(this.mixedGraph, originalFrom, originalTo, 'days');
|
||||
const roadPath = PathFinder.dijkstra(this.roadGraph, originalFrom, originalTo, 'days');
|
||||
const waterPath = PathFinder.dijkstra(this.waterGraph, originalFrom, originalTo, 'days');
|
||||
|
||||
if (!mixedPath && !roadPath && !waterPath) {
|
||||
message += `<p><strong>Aucun chemin trouvé entre ${originalFrom} et ${originalTo}</strong></p>`;
|
||||
message += `<p>Il n'existe pas de route reliant ces deux villes dans les données disponibles.</p>`;
|
||||
} else {
|
||||
message += `<div class="voyage-main-title">De ${originalFrom} à ${originalTo}</div>`;
|
||||
message += `<p><em>Différentes options de voyage disponibles :</em></p><hr class="voyage-separator">`;
|
||||
|
||||
// Option 1 : Trajet mixte optimal (le plus rapide)
|
||||
if (mixedPath) {
|
||||
message += this.formatMultiStepRoute(mixedPath, originalFrom, originalTo, 'mixed');
|
||||
message += `<hr class="voyage-separator">`;
|
||||
}
|
||||
|
||||
// Option 2 : Trajet 100% terrestre
|
||||
if (roadPath) {
|
||||
message += this.formatMultiStepRoute(roadPath, originalFrom, originalTo, 'road');
|
||||
message += `<hr class="voyage-separator">`;
|
||||
}
|
||||
|
||||
// Option 3 : Trajet 100% eau (fleuve/mer)
|
||||
if (waterPath) {
|
||||
message += this.formatMultiStepRoute(waterPath, originalFrom, originalTo, 'water');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (fromTown && fromTown == "help") {
|
||||
console.log("TravelV2: Branche: Affichage de l'aide");
|
||||
// Afficher l'aide
|
||||
message += `<p><strong>Aide pour /voyage</strong><br>`;
|
||||
message += `Usage: <code>/voyage [ville_départ] [ville_arrivée]</code><br><br>`;
|
||||
message += `Exemples:<br>`;
|
||||
message += `<code>/voyage</code> - Liste toutes les villes de départ<br>`;
|
||||
message += `<code>/voyage Altdorf</code> - Liste les destinations depuis Altdorf<br>`;
|
||||
message += `<code>/voyage Altdorf Nuln</code> - Affiche les détails de voyage entre Altdorf et Nuln`;
|
||||
message += `</p>`;
|
||||
} else if (fromTown) {
|
||||
console.log("TravelV2: Branche: Liste des destinations depuis une ville");
|
||||
// Lister toutes les destinations possibles depuis une ville (avec pathfinding)
|
||||
const normalizedFrom = PathFinder.findCityInGraph(this.roadGraph, fromTown);
|
||||
|
||||
if (normalizedFrom) {
|
||||
message += `<div class="voyage-destinations-title">Destinations depuis ${normalizedFrom}</div>`;
|
||||
message += `<p><em>Toutes les villes accessibles (routes directes et itinéraires calculés)</em></p>`;
|
||||
|
||||
// Récupérer toutes les villes du graphe sauf la ville de départ
|
||||
const allCities = Object.keys(this.roadGraph)
|
||||
.filter(city => city.toLowerCase() !== normalizedFrom.toLowerCase())
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
// Afficher toutes les destinations
|
||||
for (const city of allCities) {
|
||||
message += `<p><a class="action-link" data-action="clickVoyage" data-from="${normalizedFrom}" data-to="${city}"><i class="fas fa-map-marked-alt"></i> ${city}</a></p>`;
|
||||
}
|
||||
} else {
|
||||
message += `<p><strong>Ville inconnue: ${fromTown}</strong></p>`;
|
||||
message += `<p>Cette ville n'existe pas dans la base de données.</p>`;
|
||||
}
|
||||
} else {
|
||||
console.log("TravelV2: Branche: Liste de toutes les villes de départ");
|
||||
// Lister toutes les villes de départ
|
||||
message += `<div class="voyage-destinations-title">Sélectionnez une ville de départ</div>`;
|
||||
let uniqTown = {};
|
||||
|
||||
for (const travel of this.travel_data) {
|
||||
if (uniqTown[travel.from] == undefined) {
|
||||
uniqTown[travel.from] = 1;
|
||||
message += `<p><a class="action-link" data-action="clickVoyage" data-from="${travel.from}"><i class="fas fa-list"></i> ${travel.from}</a></p>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`TravelV2: Message généré (longueur: ${message.length})`);
|
||||
|
||||
if (message.length === 0) {
|
||||
console.warn("TravelV2: Aucune donnée trouvée pour les critères fournis");
|
||||
ui.notifications.warn("Aucune route trouvée pour ces critères");
|
||||
return;
|
||||
}
|
||||
|
||||
ChatMessage.create({
|
||||
content: message,
|
||||
whisper: [game.user.id], // Afficher uniquement pour le GM qui a lancé la commande
|
||||
speaker: { alias: "Outil de voyage" }
|
||||
});
|
||||
|
||||
console.log("TravelV2: ChatMessage créé avec succès");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère le clic sur un lien de voyage
|
||||
* @param {Event} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static handleTravelClick(event, target) {
|
||||
let fromTown = target.dataset.from;
|
||||
let toTown = target.dataset.to;
|
||||
TravelDistanceV2.displayTravelDistance(fromTown, toTown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate l'affichage d'une route directe
|
||||
* @param {Object} travel - Données de la route
|
||||
* @returns {String} HTML formaté
|
||||
*/
|
||||
static formatDirectRoute(travel) {
|
||||
let message = `<p><strong>De ${travel.from} à ${travel.to}</strong> (Route directe)`;
|
||||
|
||||
if (travel.road_distance != "") {
|
||||
let road_horse_heavy_days = this.roundDuration(travel.road_days * 0.8);
|
||||
let road_horse_fast_days = this.roundDuration(travel.road_days * 0.65);
|
||||
let road_feet_days = this.roundDuration(travel.road_days * 1.25);
|
||||
let road_danger_string = this.dangerToString(travel.road_danger);
|
||||
let road_danger_feet_string = this.dangerToString(travel.road_danger + "!");
|
||||
|
||||
message += `<br><br><strong>Par route:</strong>`;
|
||||
message += `<br>Distance: ${travel.road_distance} km`;
|
||||
message += `<br>Durée (chariot): ${travel.road_days} jours - Danger: ${road_danger_string}`;
|
||||
message += `<br>Durée (cheval de charge): ${road_horse_heavy_days} jours - Danger: ${road_danger_string}`;
|
||||
message += `<br>Durée (cheval rapide): ${road_horse_fast_days} jours - Danger: ${road_danger_string}`;
|
||||
message += `<br>Durée (à pied): ${road_feet_days} jours - Danger: ${road_danger_feet_string}`;
|
||||
}
|
||||
|
||||
if (travel.river_distance != "") {
|
||||
let river_danger_string = this.dangerToString(travel.river_danger);
|
||||
message += `<br><br><strong>Par rivière:</strong>`;
|
||||
message += `<br>Distance: ${travel.river_distance} km`;
|
||||
message += `<br>Durée: ${travel.river_days} jours - Danger: ${river_danger_string}`;
|
||||
}
|
||||
|
||||
if (travel.sea_distance != "") {
|
||||
let sea_danger_string = this.dangerToString(travel.sea_danger);
|
||||
message += `<br><br><strong>Par mer:</strong>`;
|
||||
message += `<br>Distance: ${travel.sea_distance} km`;
|
||||
message += `<br>Durée: ${travel.sea_days} jours - Danger: ${sea_danger_string}`;
|
||||
}
|
||||
|
||||
message += "</p>";
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate l'affichage d'une route multi-étapes
|
||||
* @param {Object} pathResult - Résultat du pathfinding
|
||||
* @param {String} fromCity - Ville de départ
|
||||
* @param {String} toCity - Ville d'arrivée
|
||||
* @param {String} routeType - Type de route ('mixed', 'road', 'water')
|
||||
* @returns {String} HTML formaté
|
||||
*/
|
||||
static formatMultiStepRoute(pathResult, fromCity, toCity, routeType = 'road') {
|
||||
// Déterminer le titre selon le type
|
||||
let routeTitle = "";
|
||||
|
||||
if (routeType === 'mixed') {
|
||||
routeTitle = "🌟 Itinéraire optimal (tous modes de transport)";
|
||||
} else if (routeType === 'road') {
|
||||
routeTitle = "🛤️ Itinéraire 100% terrestre";
|
||||
} else if (routeType === 'water') {
|
||||
routeTitle = "⛵ Itinéraire 100% fluvial/maritime";
|
||||
}
|
||||
|
||||
let message = `<div class="voyage-route-title">${routeTitle}</div>`;
|
||||
message += `<p><strong>${fromCity} → ${toCity}</strong> (${pathResult.steps} étape${pathResult.steps > 1 ? 's' : ''})`;
|
||||
|
||||
// Résumé du voyage
|
||||
const totalDistance = Math.round(pathResult.totalDistance);
|
||||
const totalDays = Math.round(pathResult.totalDays);
|
||||
const danger_string = this.dangerToString(pathResult.maxDanger);
|
||||
|
||||
// Pour les routes terrestres, afficher les variantes de durée
|
||||
const includesRoad = !pathResult.modesUsed || pathResult.modesUsed.includes('road');
|
||||
|
||||
message += `<br><br><strong>📊 Résumé du voyage:</strong>`;
|
||||
message += `<br>Distance totale: ${totalDistance} km`;
|
||||
|
||||
if (includesRoad) {
|
||||
const road_horse_heavy_days = this.roundDuration(pathResult.totalDays * 0.8);
|
||||
const road_horse_fast_days = this.roundDuration(pathResult.totalDays * 0.65);
|
||||
const road_feet_days = this.roundDuration(pathResult.totalDays * 1.25);
|
||||
const danger_feet_string = this.dangerToString(pathResult.maxDanger + "!");
|
||||
|
||||
message += `<br>Durée (chariot): ${totalDays} jours - Danger max: ${danger_string}`;
|
||||
message += `<br>Durée (cheval de charge): ${road_horse_heavy_days} jours - Danger max: ${danger_string}`;
|
||||
message += `<br>Durée (cheval rapide): ${road_horse_fast_days} jours - Danger max: ${danger_string}`;
|
||||
message += `<br>Durée (à pied): ${road_feet_days} jours - Danger max: ${danger_feet_string}`;
|
||||
} else {
|
||||
message += `<br>Durée totale: ${totalDays} jours - Danger max: ${danger_string}`;
|
||||
}
|
||||
|
||||
// Détails des étapes avec mode de transport
|
||||
message += `<br><br><strong>🗺️ Itinéraire détaillé:</strong>`;
|
||||
for (let i = 0; i < pathResult.path.length; i++) {
|
||||
const step = pathResult.path[i];
|
||||
const stepNum = i + 1;
|
||||
const stepDanger = this.dangerToString(step.danger);
|
||||
|
||||
// Icône selon le mode de transport
|
||||
let modeIcon = "🛤️";
|
||||
let modeName = "route";
|
||||
if (step.mode === 'river') {
|
||||
modeIcon = "🚣";
|
||||
modeName = "fleuve";
|
||||
} else if (step.mode === 'sea') {
|
||||
modeIcon = "⛵";
|
||||
modeName = "mer";
|
||||
}
|
||||
|
||||
message += `<br><br><em>Étape ${stepNum}:</em> ${step.from} → ${step.to}`;
|
||||
message += `<br> ${modeIcon} Par ${modeName}: ${Math.round(step.distance)} km, ${step.days} jour${step.days > 1 ? 's' : ''} - Danger: ${stepDanger}`;
|
||||
}
|
||||
|
||||
message += "</p>";
|
||||
return message;
|
||||
}
|
||||
}
|
||||
30
modules/travelv2/debug-display.js
Normal file
30
modules/travelv2/debug-display.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Script de débogage pour vérifier l'affichage du message
|
||||
*/
|
||||
|
||||
// Vérifier que les données sont chargées
|
||||
console.log("=== DEBUG DISPLAY ===");
|
||||
console.log("travel_data:", game.wfrp4e.travelv2.travel_data);
|
||||
console.log("Nombre de routes:", game.wfrp4e.travelv2.travel_data?.length);
|
||||
|
||||
// Générer le message manuellement comme le fait la fonction
|
||||
let message = "";
|
||||
message += `<h3>Sélectionnez une ville de départ</h3>`;
|
||||
let uniqTown = {};
|
||||
|
||||
for (var travel of game.wfrp4e.travelv2.travel_data) {
|
||||
if (uniqTown[travel.from] == undefined) {
|
||||
uniqTown[travel.from] = 1;
|
||||
message += `<p><a class="action-link" data-action="clickTravel2" data-from="${travel.from}"><i class="fas fa-list"></i> ${travel.from}</a></p>`;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Message généré:", message);
|
||||
console.log("Longueur du message:", message.length);
|
||||
console.log("Nombre de villes uniques:", Object.keys(uniqTown).length);
|
||||
|
||||
// Tester ChatMessage.create
|
||||
ChatMessage.create({
|
||||
content: message,
|
||||
whisper: game.user.isGM ? [] : [game.user.id]
|
||||
});
|
||||
69
modules/travelv2/diagnostic.js
Normal file
69
modules/travelv2/diagnostic.js
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Script de diagnostic pour TravelV2
|
||||
*
|
||||
* Copiez-collez ce code dans la console de Foundry VTT (F12)
|
||||
* pour diagnostiquer les problèmes de chargement
|
||||
*/
|
||||
|
||||
console.log("=== Diagnostic TravelV2 ===\n");
|
||||
|
||||
// Test 1: Vérifier si la classe existe
|
||||
console.log("1. Classe TravelDistanceV2 existe ?");
|
||||
if (typeof TravelDistanceV2 !== 'undefined') {
|
||||
console.log(" ✓ OUI - La classe est disponible");
|
||||
} else {
|
||||
console.log(" ✗ NON - La classe n'est pas chargée !");
|
||||
console.log(" → Vérifiez que le module est activé et rechargez (F5)");
|
||||
}
|
||||
|
||||
// Test 2: Vérifier les données
|
||||
console.log("\n2. Données chargées ?");
|
||||
if (typeof TravelDistanceV2 !== 'undefined' && TravelDistanceV2.travel_data) {
|
||||
console.log(` ✓ OUI - ${TravelDistanceV2.travel_data.length} routes chargées`);
|
||||
console.log(` → Première route: ${TravelDistanceV2.travel_data[0]?.from} → ${TravelDistanceV2.travel_data[0]?.to}`);
|
||||
} else {
|
||||
console.log(" ✗ NON - Les données ne sont pas chargées");
|
||||
console.log(" → Essayez de les charger manuellement:");
|
||||
console.log(" → await TravelDistanceV2.loadTravelData()");
|
||||
}
|
||||
|
||||
// Test 3: Tester le chargement manuel
|
||||
console.log("\n3. Test de chargement manuel:");
|
||||
console.log(" Exécutez: await TravelDistanceV2.loadTravelData()");
|
||||
console.log(" Puis vérifiez avec: TravelDistanceV2.travel_data.length");
|
||||
|
||||
// Test 4: Tester le chemin du fichier
|
||||
console.log("\n4. Vérification du chemin du fichier:");
|
||||
const path = 'modules/foundryvtt-wh4-lang-fr-fr/modules/travelv2/travel_data.json';
|
||||
console.log(` Chemin: ${path}`);
|
||||
console.log(" Test de fetch...");
|
||||
fetch(path)
|
||||
.then(response => {
|
||||
console.log(` ✓ Fichier accessible - Status: ${response.status}`);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log(` ✓ JSON valide - ${data.length} routes trouvées`);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(` ✗ Erreur: ${error.message}`);
|
||||
console.log(" → Vérifiez que le fichier travel_data.json existe bien");
|
||||
});
|
||||
|
||||
// Test 5: Afficher l'état du module
|
||||
console.log("\n5. État du module:");
|
||||
console.log(` game.modules = ${game.modules ? 'Disponible' : 'Non disponible'}`);
|
||||
const frModule = game.modules.get('foundryvtt-wh4-lang-fr-fr');
|
||||
if (frModule) {
|
||||
console.log(` ✓ Module trouvé: ${frModule.title}`);
|
||||
console.log(` → Actif: ${frModule.active}`);
|
||||
} else {
|
||||
console.log(" ✗ Module 'foundryvtt-wh4-lang-fr-fr' non trouvé");
|
||||
}
|
||||
|
||||
console.log("\n=== Fin du diagnostic ===");
|
||||
console.log("\nCommandes utiles:");
|
||||
console.log("• Charger les données: await TravelDistanceV2.loadTravelData()");
|
||||
console.log("• Vérifier les données: TravelDistanceV2.travel_data");
|
||||
console.log("• Tester l'affichage: TravelDistanceV2.displayTravelDistance()");
|
||||
console.log("• Tester avec ville: TravelDistanceV2.displayTravelDistance('Altdorf')");
|
||||
8
modules/travelv2/index.js
Normal file
8
modules/travelv2/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Index du module TravelV2
|
||||
*
|
||||
* Ce fichier exporte tous les composants du module TravelV2
|
||||
*/
|
||||
|
||||
export { default as TravelDistanceV2 } from './TravelDistanceV2.js';
|
||||
export { initTravelV2 } from './travelv2-init.js';
|
||||
218
modules/travelv2/pathfinding.js
Normal file
218
modules/travelv2/pathfinding.js
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* Algorithmes de calcul de plus court chemin pour les voyages
|
||||
*/
|
||||
|
||||
/**
|
||||
* Classe pour le calcul de plus court chemin (algorithme de Dijkstra)
|
||||
*/
|
||||
export class PathFinder {
|
||||
/**
|
||||
* Construit un graphe à partir des données de voyage
|
||||
* @param {Array} travelData - Données de voyage
|
||||
* @param {String|Array} modes - Mode(s) de transport ('road', 'river', 'sea', ['road', 'river', 'sea'], 'water')
|
||||
* @returns {Object} Graphe avec adjacence et poids
|
||||
*/
|
||||
static buildGraph(travelData, modes = 'road') {
|
||||
const graph = {};
|
||||
|
||||
// Normaliser modes en tableau
|
||||
let modeList = [];
|
||||
if (modes === 'water') {
|
||||
modeList = ['river', 'sea'];
|
||||
} else if (modes === 'mixed') {
|
||||
modeList = ['road', 'river', 'sea'];
|
||||
} else if (Array.isArray(modes)) {
|
||||
modeList = modes;
|
||||
} else {
|
||||
modeList = [modes];
|
||||
}
|
||||
|
||||
for (const route of travelData) {
|
||||
const from = route.from;
|
||||
const to = route.to;
|
||||
|
||||
// Initialiser les nœuds
|
||||
if (!graph[from]) {
|
||||
graph[from] = [];
|
||||
}
|
||||
if (!graph[to]) {
|
||||
graph[to] = [];
|
||||
}
|
||||
|
||||
// Pour chaque mode de transport disponible
|
||||
for (const mode of modeList) {
|
||||
const distanceKey = `${mode}_distance`;
|
||||
const daysKey = `${mode}_days`;
|
||||
const distance = route[distanceKey];
|
||||
const days = route[daysKey];
|
||||
|
||||
// Ignorer les routes sans ce mode de transport
|
||||
if (!distance || distance === "" || !days || days === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ajouter les arêtes (bidirectionnelles)
|
||||
graph[from].push({
|
||||
destination: to,
|
||||
distance: parseFloat(distance),
|
||||
days: parseFloat(days),
|
||||
danger: route[`${mode}_danger`] || "",
|
||||
mode: mode
|
||||
});
|
||||
|
||||
graph[to].push({
|
||||
destination: from,
|
||||
distance: parseFloat(distance),
|
||||
days: parseFloat(days),
|
||||
danger: route[`${mode}_danger`] || "",
|
||||
mode: mode
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithme de Dijkstra pour trouver le plus court chemin
|
||||
* @param {Object} graph - Graphe d'adjacence
|
||||
* @param {String} start - Ville de départ
|
||||
* @param {String} end - Ville d'arrivée
|
||||
* @param {String} metric - Métrique à minimiser ('distance' ou 'days')
|
||||
* @returns {Object|null} Chemin trouvé avec détails ou null si pas de chemin
|
||||
*/
|
||||
static dijkstra(graph, start, end, metric = 'days') {
|
||||
// Normaliser les noms de villes
|
||||
const normalizedStart = this.findCityInGraph(graph, start);
|
||||
const normalizedEnd = this.findCityInGraph(graph, end);
|
||||
|
||||
if (!normalizedStart || !normalizedEnd) {
|
||||
console.warn(`PathFinder: Ville non trouvée - start: ${start}, end: ${end}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Initialisation
|
||||
const distances = {};
|
||||
const previous = {};
|
||||
const visited = new Set();
|
||||
const queue = [];
|
||||
|
||||
// Initialiser toutes les distances à l'infini
|
||||
for (const node in graph) {
|
||||
distances[node] = Infinity;
|
||||
previous[node] = null;
|
||||
}
|
||||
|
||||
distances[normalizedStart] = 0;
|
||||
queue.push({ node: normalizedStart, distance: 0 });
|
||||
|
||||
while (queue.length > 0) {
|
||||
// Trouver le nœud avec la plus petite distance
|
||||
queue.sort((a, b) => a.distance - b.distance);
|
||||
const { node: current } = queue.shift();
|
||||
|
||||
if (visited.has(current)) continue;
|
||||
visited.add(current);
|
||||
|
||||
// Si on a atteint la destination
|
||||
if (current === normalizedEnd) {
|
||||
return this.reconstructPath(previous, normalizedStart, normalizedEnd, graph, metric);
|
||||
}
|
||||
|
||||
// Explorer les voisins
|
||||
const neighbors = graph[current] || [];
|
||||
for (const neighbor of neighbors) {
|
||||
if (visited.has(neighbor.destination)) continue;
|
||||
|
||||
const weight = neighbor[metric]; // 'distance' ou 'days'
|
||||
const newDistance = distances[current] + weight;
|
||||
|
||||
if (newDistance < distances[neighbor.destination]) {
|
||||
distances[neighbor.destination] = newDistance;
|
||||
previous[neighbor.destination] = {
|
||||
from: current,
|
||||
edge: neighbor
|
||||
};
|
||||
queue.push({ node: neighbor.destination, distance: newDistance });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pas de chemin trouvé
|
||||
console.warn(`PathFinder: Aucun chemin trouvé entre ${start} et ${end}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve une ville dans le graphe (insensible à la casse)
|
||||
* @param {Object} graph - Graphe
|
||||
* @param {String} cityName - Nom de la ville
|
||||
* @returns {String|null} Nom normalisé de la ville ou null
|
||||
*/
|
||||
static findCityInGraph(graph, cityName) {
|
||||
const lowerName = cityName.toLowerCase();
|
||||
for (const city in graph) {
|
||||
if (city.toLowerCase() === lowerName) {
|
||||
return city;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstruit le chemin à partir des prédécesseurs
|
||||
* @param {Object} previous - Map des prédécesseurs
|
||||
* @param {String} start - Ville de départ
|
||||
* @param {String} end - Ville d'arrivée
|
||||
* @param {Object} graph - Graphe d'adjacence
|
||||
* @param {String} metric - Métrique utilisée
|
||||
* @returns {Object} Détails du chemin
|
||||
*/
|
||||
static reconstructPath(previous, start, end, graph, metric) {
|
||||
const path = [];
|
||||
let current = end;
|
||||
let totalDistance = 0;
|
||||
let totalDays = 0;
|
||||
let maxDanger = "";
|
||||
const modesUsed = new Set();
|
||||
|
||||
// Reconstruire le chemin en remontant
|
||||
while (current !== start) {
|
||||
const prev = previous[current];
|
||||
if (!prev) break;
|
||||
|
||||
const mode = prev.edge.mode || 'road';
|
||||
modesUsed.add(mode);
|
||||
|
||||
path.unshift({
|
||||
from: prev.from,
|
||||
to: current,
|
||||
distance: prev.edge.distance,
|
||||
days: prev.edge.days,
|
||||
danger: prev.edge.danger,
|
||||
mode: mode
|
||||
});
|
||||
|
||||
totalDistance += prev.edge.distance;
|
||||
totalDays += prev.edge.days;
|
||||
|
||||
// Calculer le danger maximum
|
||||
const dangerLevel = (prev.edge.danger || "").length;
|
||||
const currentMaxLevel = maxDanger.length;
|
||||
if (dangerLevel > currentMaxLevel) {
|
||||
maxDanger = prev.edge.danger;
|
||||
}
|
||||
|
||||
current = prev.from;
|
||||
}
|
||||
|
||||
return {
|
||||
path: path,
|
||||
totalDistance: totalDistance,
|
||||
totalDays: totalDays,
|
||||
maxDanger: maxDanger,
|
||||
steps: path.length,
|
||||
modesUsed: Array.from(modesUsed)
|
||||
};
|
||||
}
|
||||
}
|
||||
53
modules/travelv2/test.js
Normal file
53
modules/travelv2/test.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Fichier de test pour le module TravelV2
|
||||
*
|
||||
* Pour tester le module dans la console de développement de Foundry VTT:
|
||||
*
|
||||
* 1. Charger le module
|
||||
* 2. Ouvrir la console (F12)
|
||||
* 3. Exécuter ces tests
|
||||
*/
|
||||
|
||||
// Test 1: Vérifier que la classe est chargée
|
||||
console.log("Test 1: Classe TravelDistanceV2 disponible?");
|
||||
console.log(typeof TravelDistanceV2 !== 'undefined' ? "✓ OK" : "✗ ÉCHEC");
|
||||
|
||||
// Test 2: Vérifier que les données sont chargées
|
||||
console.log("\nTest 2: Données de voyage chargées?");
|
||||
console.log(TravelDistanceV2.travel_data ? "✓ OK - " + TravelDistanceV2.travel_data.length + " routes trouvées" : "✗ ÉCHEC");
|
||||
|
||||
// Test 3: Tester dangerToString
|
||||
console.log("\nTest 3: Test de dangerToString");
|
||||
console.log("'' -> " + TravelDistanceV2.dangerToString(""));
|
||||
console.log("'!' -> " + TravelDistanceV2.dangerToString("!"));
|
||||
console.log("'!!' -> " + TravelDistanceV2.dangerToString("!!"));
|
||||
console.log("'!!!' -> " + TravelDistanceV2.dangerToString("!!!"));
|
||||
|
||||
// Test 4: Tester roundDuration
|
||||
console.log("\nTest 4: Test de roundDuration");
|
||||
console.log("22.1 -> " + TravelDistanceV2.roundDuration(22.1));
|
||||
console.log("22.3 -> " + TravelDistanceV2.roundDuration(22.3));
|
||||
console.log("22.5 -> " + TravelDistanceV2.roundDuration(22.5));
|
||||
console.log("22.8 -> " + TravelDistanceV2.roundDuration(22.8));
|
||||
|
||||
// Test 5: Tester displayTravelDistance avec une ville
|
||||
console.log("\nTest 5: Affichage des destinations depuis Altdorf");
|
||||
// TravelDistanceV2.displayTravelDistance("Altdorf");
|
||||
console.log("Exécutez manuellement: TravelDistanceV2.displayTravelDistance('Altdorf')");
|
||||
|
||||
// Test 6: Tester displayTravelDistance avec deux villes
|
||||
console.log("\nTest 6: Affichage du trajet Altdorf -> Nuln");
|
||||
// TravelDistanceV2.displayTravelDistance("Altdorf", "Nuln");
|
||||
console.log("Exécutez manuellement: TravelDistanceV2.displayTravelDistance('Altdorf', 'Nuln')");
|
||||
|
||||
// Test 7: Tester la commande help
|
||||
console.log("\nTest 7: Affichage de l'aide");
|
||||
// TravelDistanceV2.displayTravelDistance("help");
|
||||
console.log("Exécutez manuellement: TravelDistanceV2.displayTravelDistance('help')");
|
||||
|
||||
console.log("\n=== Tests terminés ===");
|
||||
console.log("\nPour tester la commande complète, tapez dans le chat:");
|
||||
console.log("/travel2");
|
||||
console.log("/travel2 Altdorf");
|
||||
console.log("/travel2 Altdorf Nuln");
|
||||
console.log("/travel2 help");
|
||||
2745
modules/travelv2/travel_data.json
Normal file
2745
modules/travelv2/travel_data.json
Normal file
File diff suppressed because it is too large
Load Diff
67
modules/travelv2/travelv2-init.js
Normal file
67
modules/travelv2/travelv2-init.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import TravelDistanceV2 from './TravelDistanceV2.js';
|
||||
|
||||
/**
|
||||
* Initialisation du module TravelV2
|
||||
*/
|
||||
export function initTravelV2() {
|
||||
console.log("TravelV2: Initialisation du module de voyage");
|
||||
|
||||
// Hook pour charger les données au démarrage
|
||||
Hooks.once('ready', async () => {
|
||||
console.log("TravelV2: Chargement des données de voyage");
|
||||
|
||||
// Exposer la classe globalement pour accès depuis la console
|
||||
game.wfrp4e = game.wfrp4e || {};
|
||||
game.wfrp4e.travelv2 = TravelDistanceV2;
|
||||
|
||||
await TravelDistanceV2.loadTravelData();
|
||||
|
||||
console.log("TravelV2: Classe accessible via game.wfrp4e.travelv2");
|
||||
|
||||
// Enregistrer la commande dans le système WFRP4e si disponible
|
||||
if (game.wfrp4e?.commands) {
|
||||
console.log("TravelV2: Enregistrement de la commande /voyage");
|
||||
game.wfrp4e.commands.add({
|
||||
voyage: {
|
||||
description: "Outil de calcul de distances de voyage (FR)",
|
||||
args: ["from", "to"],
|
||||
defaultArg: "from",
|
||||
callback: (from, to) => {
|
||||
// Vérifier que l'utilisateur est GM
|
||||
if (!game.user.isGM) {
|
||||
ui.notifications.warn("La commande /voyage est réservée au MJ.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`TravelV2: Commande /voyage exécutée`);
|
||||
console.log(`TravelV2: from =`, from, `(type: ${typeof from})`);
|
||||
console.log(`TravelV2: to =`, to, `(type: ${typeof to})`);
|
||||
console.log(`TravelV2: from === null ?`, from === null);
|
||||
console.log(`TravelV2: to === null ?`, to === null);
|
||||
|
||||
// Convertir null en undefined pour que la logique fonctionne
|
||||
from = from || undefined;
|
||||
to = to || undefined;
|
||||
|
||||
TravelDistanceV2.displayTravelDistance(from, to);
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log("TravelV2: Commande /voyage enregistrée avec succès");
|
||||
} else {
|
||||
console.warn("TravelV2: game.wfrp4e.commands non disponible");
|
||||
}
|
||||
});
|
||||
|
||||
// Hook pour ajouter un gestionnaire de clics sur les liens de voyage
|
||||
Hooks.on('renderChatMessage', (message, html, data) => {
|
||||
// Ajouter un listener pour les clics sur les liens de voyage
|
||||
html.find('a[data-action="clickVoyage"]').click((event) => {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget;
|
||||
TravelDistanceV2.handleTravelClick(event, target);
|
||||
});
|
||||
});
|
||||
|
||||
console.log("TravelV2: Module de voyage initialisé");
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
MANIFEST-001070
|
||||
MANIFEST-001251
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
2025/05/17-16:42:35.359585 7f52bcdfa6c0 Recovering log #1068
|
||||
2025/05/17-16:42:35.413166 7f52bcdfa6c0 Delete type=3 #1066
|
||||
2025/05/17-16:42:35.413234 7f52bcdfa6c0 Delete type=0 #1068
|
||||
2025/05/17-16:46:29.166187 7f52b67fc6c0 Level-0 table #1073: started
|
||||
2025/05/17-16:46:29.166212 7f52b67fc6c0 Level-0 table #1073: 0 bytes OK
|
||||
2025/05/17-16:46:29.202366 7f52b67fc6c0 Delete type=0 #1071
|
||||
2025/05/17-16:46:29.239848 7f52b67fc6c0 Manual compaction at level-0 from '!journal!3IgmiprzLB6Lwenc' @ 72057594037927935 : 1 .. '!journal.pages!suuYN87Al1ZZWtQQ.jhgNnhWhrkOpKs1B' @ 0 : 0; will stop at (end)
|
||||
2026/01/07-15:05:51.801144 7f93ebfff6c0 Recovering log #1249
|
||||
2026/01/07-15:05:51.812047 7f93ebfff6c0 Delete type=3 #1247
|
||||
2026/01/07-15:05:51.812099 7f93ebfff6c0 Delete type=0 #1249
|
||||
2026/01/07-15:06:45.486999 7f93e9ffb6c0 Level-0 table #1254: started
|
||||
2026/01/07-15:06:45.487020 7f93e9ffb6c0 Level-0 table #1254: 0 bytes OK
|
||||
2026/01/07-15:06:45.493208 7f93e9ffb6c0 Delete type=0 #1252
|
||||
2026/01/07-15:06:45.493509 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!3IgmiprzLB6Lwenc' @ 72057594037927935 : 1 .. '!journal.pages!suuYN87Al1ZZWtQQ.jhgNnhWhrkOpKs1B' @ 0 : 0; will stop at (end)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
2025/05/17-16:40:21.151832 7f52b7fff6c0 Recovering log #1064
|
||||
2025/05/17-16:40:21.161682 7f52b7fff6c0 Delete type=3 #1062
|
||||
2025/05/17-16:40:21.161736 7f52b7fff6c0 Delete type=0 #1064
|
||||
2025/05/17-16:40:56.011461 7f52b67fc6c0 Level-0 table #1069: started
|
||||
2025/05/17-16:40:56.011532 7f52b67fc6c0 Level-0 table #1069: 0 bytes OK
|
||||
2025/05/17-16:40:56.018207 7f52b67fc6c0 Delete type=0 #1067
|
||||
2025/05/17-16:40:56.039155 7f52b67fc6c0 Manual compaction at level-0 from '!journal!3IgmiprzLB6Lwenc' @ 72057594037927935 : 1 .. '!journal.pages!suuYN87Al1ZZWtQQ.jhgNnhWhrkOpKs1B' @ 0 : 0; will stop at (end)
|
||||
2026/01/07-14:19:22.396531 7f93ebfff6c0 Recovering log #1245
|
||||
2026/01/07-14:19:22.406238 7f93ebfff6c0 Delete type=3 #1243
|
||||
2026/01/07-14:19:22.406300 7f93ebfff6c0 Delete type=0 #1245
|
||||
2026/01/07-15:04:29.638874 7f93e9ffb6c0 Level-0 table #1250: started
|
||||
2026/01/07-15:04:29.638900 7f93e9ffb6c0 Level-0 table #1250: 0 bytes OK
|
||||
2026/01/07-15:04:29.650961 7f93e9ffb6c0 Delete type=0 #1248
|
||||
2026/01/07-15:04:29.667055 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!3IgmiprzLB6Lwenc' @ 72057594037927935 : 1 .. '!journal.pages!suuYN87Al1ZZWtQQ.jhgNnhWhrkOpKs1B' @ 0 : 0; will stop at (end)
|
||||
|
||||
Binary file not shown.
BIN
packs/aides-de-jeu-fr/MANIFEST-001251
Normal file
BIN
packs/aides-de-jeu-fr/MANIFEST-001251
Normal file
Binary file not shown.
@@ -1 +1 @@
|
||||
MANIFEST-001072
|
||||
MANIFEST-001253
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
2025/05/17-16:42:35.415628 7f52b77fe6c0 Recovering log #1070
|
||||
2025/05/17-16:42:35.464272 7f52b77fe6c0 Delete type=3 #1068
|
||||
2025/05/17-16:42:35.464341 7f52b77fe6c0 Delete type=0 #1070
|
||||
2025/05/17-16:46:29.123835 7f52b67fc6c0 Level-0 table #1075: started
|
||||
2025/05/17-16:46:29.123864 7f52b67fc6c0 Level-0 table #1075: 0 bytes OK
|
||||
2025/05/17-16:46:29.165929 7f52b67fc6c0 Delete type=0 #1073
|
||||
2025/05/17-16:46:29.239836 7f52b67fc6c0 Manual compaction at level-0 from '!folders!3uquYH73ttCdoH0I' @ 72057594037927935 : 1 .. '!items!ylFhk7mGZOnAJTUT' @ 0 : 0; will stop at (end)
|
||||
2026/01/07-15:05:51.814570 7f93ea7fc6c0 Recovering log #1251
|
||||
2026/01/07-15:05:51.824155 7f93ea7fc6c0 Delete type=3 #1249
|
||||
2026/01/07-15:05:51.824233 7f93ea7fc6c0 Delete type=0 #1251
|
||||
2026/01/07-15:06:45.493731 7f93e9ffb6c0 Level-0 table #1256: started
|
||||
2026/01/07-15:06:45.493762 7f93e9ffb6c0 Level-0 table #1256: 0 bytes OK
|
||||
2026/01/07-15:06:45.500998 7f93e9ffb6c0 Delete type=0 #1254
|
||||
2026/01/07-15:06:45.529036 7f93e9ffb6c0 Manual compaction at level-0 from '!folders!3uquYH73ttCdoH0I' @ 72057594037927935 : 1 .. '!items!ylFhk7mGZOnAJTUT' @ 0 : 0; will stop at (end)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
2025/05/17-16:40:21.164397 7f52b6ffd6c0 Recovering log #1066
|
||||
2025/05/17-16:40:21.174508 7f52b6ffd6c0 Delete type=3 #1064
|
||||
2025/05/17-16:40:21.174596 7f52b6ffd6c0 Delete type=0 #1066
|
||||
2025/05/17-16:40:56.018463 7f52b67fc6c0 Level-0 table #1071: started
|
||||
2025/05/17-16:40:56.018534 7f52b67fc6c0 Level-0 table #1071: 0 bytes OK
|
||||
2025/05/17-16:40:56.024927 7f52b67fc6c0 Delete type=0 #1069
|
||||
2025/05/17-16:40:56.039168 7f52b67fc6c0 Manual compaction at level-0 from '!folders!3uquYH73ttCdoH0I' @ 72057594037927935 : 1 .. '!items!ylFhk7mGZOnAJTUT' @ 0 : 0; will stop at (end)
|
||||
2026/01/07-14:19:22.408420 7f93ea7fc6c0 Recovering log #1247
|
||||
2026/01/07-14:19:22.419450 7f93ea7fc6c0 Delete type=3 #1245
|
||||
2026/01/07-14:19:22.419496 7f93ea7fc6c0 Delete type=0 #1247
|
||||
2026/01/07-15:04:29.689520 7f93e9ffb6c0 Level-0 table #1252: started
|
||||
2026/01/07-15:04:29.689548 7f93e9ffb6c0 Level-0 table #1252: 0 bytes OK
|
||||
2026/01/07-15:04:29.701802 7f93e9ffb6c0 Delete type=0 #1250
|
||||
2026/01/07-15:04:29.713438 7f93e9ffb6c0 Manual compaction at level-0 from '!folders!3uquYH73ttCdoH0I' @ 72057594037927935 : 1 .. '!items!ylFhk7mGZOnAJTUT' @ 0 : 0; will stop at (end)
|
||||
|
||||
Binary file not shown.
BIN
packs/antidotes-and-remedes/MANIFEST-001253
Normal file
BIN
packs/antidotes-and-remedes/MANIFEST-001253
Normal file
Binary file not shown.
@@ -1 +1 @@
|
||||
MANIFEST-001070
|
||||
MANIFEST-001251
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
2025/05/17-16:42:35.517952 7f52b77fe6c0 Recovering log #1068
|
||||
2025/05/17-16:42:35.570810 7f52b77fe6c0 Delete type=3 #1066
|
||||
2025/05/17-16:42:35.570866 7f52b77fe6c0 Delete type=0 #1068
|
||||
2025/05/17-16:46:29.300164 7f52b67fc6c0 Level-0 table #1073: started
|
||||
2025/05/17-16:46:29.300228 7f52b67fc6c0 Level-0 table #1073: 0 bytes OK
|
||||
2025/05/17-16:46:29.336878 7f52b67fc6c0 Delete type=0 #1071
|
||||
2025/05/17-16:46:29.435201 7f52b67fc6c0 Manual compaction at level-0 from '!journal!cZtNgayIw2QFhC9u' @ 72057594037927935 : 1 .. '!journal.pages!cZtNgayIw2QFhC9u.ts265H1XkisLgdow' @ 0 : 0; will stop at (end)
|
||||
2026/01/07-15:05:51.839318 7f93eaffd6c0 Recovering log #1249
|
||||
2026/01/07-15:05:51.849796 7f93eaffd6c0 Delete type=3 #1247
|
||||
2026/01/07-15:05:51.849857 7f93eaffd6c0 Delete type=0 #1249
|
||||
2026/01/07-15:06:45.501213 7f93e9ffb6c0 Level-0 table #1254: started
|
||||
2026/01/07-15:06:45.501269 7f93e9ffb6c0 Level-0 table #1254: 0 bytes OK
|
||||
2026/01/07-15:06:45.508204 7f93e9ffb6c0 Delete type=0 #1252
|
||||
2026/01/07-15:06:45.529072 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!cZtNgayIw2QFhC9u' @ 72057594037927935 : 1 .. '!journal.pages!cZtNgayIw2QFhC9u.ts265H1XkisLgdow' @ 0 : 0; will stop at (end)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
2025/05/17-16:40:21.191338 7f52b77fe6c0 Recovering log #1064
|
||||
2025/05/17-16:40:21.201359 7f52b77fe6c0 Delete type=3 #1062
|
||||
2025/05/17-16:40:21.201415 7f52b77fe6c0 Delete type=0 #1064
|
||||
2025/05/17-16:40:56.039409 7f52b67fc6c0 Level-0 table #1069: started
|
||||
2025/05/17-16:40:56.039485 7f52b67fc6c0 Level-0 table #1069: 0 bytes OK
|
||||
2025/05/17-16:40:56.045833 7f52b67fc6c0 Delete type=0 #1067
|
||||
2025/05/17-16:40:56.070370 7f52b67fc6c0 Manual compaction at level-0 from '!journal!cZtNgayIw2QFhC9u' @ 72057594037927935 : 1 .. '!journal.pages!cZtNgayIw2QFhC9u.ts265H1XkisLgdow' @ 0 : 0; will stop at (end)
|
||||
2026/01/07-14:19:22.434524 7f93ea7fc6c0 Recovering log #1245
|
||||
2026/01/07-14:19:22.444128 7f93ea7fc6c0 Delete type=3 #1243
|
||||
2026/01/07-14:19:22.444200 7f93ea7fc6c0 Delete type=0 #1245
|
||||
2026/01/07-15:04:29.701936 7f93e9ffb6c0 Level-0 table #1250: started
|
||||
2026/01/07-15:04:29.701958 7f93e9ffb6c0 Level-0 table #1250: 0 bytes OK
|
||||
2026/01/07-15:04:29.713305 7f93e9ffb6c0 Delete type=0 #1248
|
||||
2026/01/07-15:04:29.713449 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!cZtNgayIw2QFhC9u' @ 72057594037927935 : 1 .. '!journal.pages!cZtNgayIw2QFhC9u.ts265H1XkisLgdow' @ 0 : 0; will stop at (end)
|
||||
|
||||
Binary file not shown.
BIN
packs/apothicarium/MANIFEST-001251
Normal file
BIN
packs/apothicarium/MANIFEST-001251
Normal file
Binary file not shown.
0
packs/dons-de-rhya/001253.log
Normal file
0
packs/dons-de-rhya/001253.log
Normal file
@@ -1 +1 @@
|
||||
MANIFEST-001070
|
||||
MANIFEST-001251
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
2025/05/17-16:42:35.305910 7f52b6ffd6c0 Recovering log #1068
|
||||
2025/05/17-16:42:35.357121 7f52b6ffd6c0 Delete type=3 #1066
|
||||
2025/05/17-16:42:35.357176 7f52b6ffd6c0 Delete type=0 #1068
|
||||
2025/05/17-16:46:29.086777 7f52b67fc6c0 Level-0 table #1073: started
|
||||
2025/05/17-16:46:29.086806 7f52b67fc6c0 Level-0 table #1073: 0 bytes OK
|
||||
2025/05/17-16:46:29.123666 7f52b67fc6c0 Delete type=0 #1071
|
||||
2025/05/17-16:46:29.239818 7f52b67fc6c0 Manual compaction at level-0 from '!journal!50u8VAjdmovyr0hx' @ 72057594037927935 : 1 .. '!journal.pages!yzw9I0r3hCK7PJnz.sPNCYj2nR3Cp3jHd' @ 0 : 0; will stop at (end)
|
||||
2026/01/07-15:05:51.788330 7f93eaffd6c0 Recovering log #1249
|
||||
2026/01/07-15:05:51.798442 7f93eaffd6c0 Delete type=3 #1247
|
||||
2026/01/07-15:05:51.798508 7f93eaffd6c0 Delete type=0 #1249
|
||||
2026/01/07-15:06:45.480176 7f93e9ffb6c0 Level-0 table #1254: started
|
||||
2026/01/07-15:06:45.480197 7f93e9ffb6c0 Level-0 table #1254: 0 bytes OK
|
||||
2026/01/07-15:06:45.486885 7f93e9ffb6c0 Delete type=0 #1252
|
||||
2026/01/07-15:06:45.493497 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!50u8VAjdmovyr0hx' @ 72057594037927935 : 1 .. '!journal.pages!yzw9I0r3hCK7PJnz.sPNCYj2nR3Cp3jHd' @ 0 : 0; will stop at (end)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
2025/05/17-16:40:21.137852 7f52bcdfa6c0 Recovering log #1064
|
||||
2025/05/17-16:40:21.148810 7f52bcdfa6c0 Delete type=3 #1062
|
||||
2025/05/17-16:40:21.148982 7f52bcdfa6c0 Delete type=0 #1064
|
||||
2025/05/17-16:40:56.025046 7f52b67fc6c0 Level-0 table #1069: started
|
||||
2025/05/17-16:40:56.025072 7f52b67fc6c0 Level-0 table #1069: 0 bytes OK
|
||||
2025/05/17-16:40:56.031175 7f52b67fc6c0 Delete type=0 #1067
|
||||
2025/05/17-16:40:56.039176 7f52b67fc6c0 Manual compaction at level-0 from '!journal!50u8VAjdmovyr0hx' @ 72057594037927935 : 1 .. '!journal.pages!yzw9I0r3hCK7PJnz.sPNCYj2nR3Cp3jHd' @ 0 : 0; will stop at (end)
|
||||
2026/01/07-14:19:22.383713 7f93eb7fe6c0 Recovering log #1245
|
||||
2026/01/07-14:19:22.394171 7f93eb7fe6c0 Delete type=3 #1243
|
||||
2026/01/07-14:19:22.394305 7f93eb7fe6c0 Delete type=0 #1245
|
||||
2026/01/07-15:04:29.651336 7f93e9ffb6c0 Level-0 table #1250: started
|
||||
2026/01/07-15:04:29.651360 7f93e9ffb6c0 Level-0 table #1250: 0 bytes OK
|
||||
2026/01/07-15:04:29.666893 7f93e9ffb6c0 Delete type=0 #1248
|
||||
2026/01/07-15:04:29.667066 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!50u8VAjdmovyr0hx' @ 72057594037927935 : 1 .. '!journal.pages!yzw9I0r3hCK7PJnz.sPNCYj2nR3Cp3jHd' @ 0 : 0; will stop at (end)
|
||||
|
||||
Binary file not shown.
BIN
packs/dons-de-rhya/MANIFEST-001251
Normal file
BIN
packs/dons-de-rhya/MANIFEST-001251
Normal file
Binary file not shown.
0
packs/dons-de-rhya/lost/001220.log
Normal file
0
packs/dons-de-rhya/lost/001220.log
Normal file
0
packs/plats-dauberges/001253.log
Normal file
0
packs/plats-dauberges/001253.log
Normal file
@@ -1 +1 @@
|
||||
MANIFEST-001070
|
||||
MANIFEST-001251
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
2025/05/17-16:42:35.251586 7f52b77fe6c0 Recovering log #1068
|
||||
2025/05/17-16:42:35.302107 7f52b77fe6c0 Delete type=3 #1066
|
||||
2025/05/17-16:42:35.302246 7f52b77fe6c0 Delete type=0 #1068
|
||||
2025/05/17-16:46:29.043855 7f52b67fc6c0 Level-0 table #1073: started
|
||||
2025/05/17-16:46:29.043913 7f52b67fc6c0 Level-0 table #1073: 0 bytes OK
|
||||
2025/05/17-16:46:29.086354 7f52b67fc6c0 Delete type=0 #1071
|
||||
2025/05/17-16:46:29.086652 7f52b67fc6c0 Manual compaction at level-0 from '!tables!4l60Lxv8cpsyy2Cg' @ 72057594037927935 : 1 .. '!tables.results!tfaYKDZqu7kgZvRG.yvbwKursaixh2dby' @ 0 : 0; will stop at (end)
|
||||
2026/01/07-15:05:51.775857 7f93ea7fc6c0 Recovering log #1249
|
||||
2026/01/07-15:05:51.785876 7f93ea7fc6c0 Delete type=3 #1247
|
||||
2026/01/07-15:05:51.785947 7f93ea7fc6c0 Delete type=0 #1249
|
||||
2026/01/07-15:06:45.473201 7f93e9ffb6c0 Level-0 table #1254: started
|
||||
2026/01/07-15:06:45.473242 7f93e9ffb6c0 Level-0 table #1254: 0 bytes OK
|
||||
2026/01/07-15:06:45.480074 7f93e9ffb6c0 Delete type=0 #1252
|
||||
2026/01/07-15:06:45.493481 7f93e9ffb6c0 Manual compaction at level-0 from '!tables!4l60Lxv8cpsyy2Cg' @ 72057594037927935 : 1 .. '!tables.results!tfaYKDZqu7kgZvRG.yvbwKursaixh2dby' @ 0 : 0; will stop at (end)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
2025/05/17-16:40:21.124718 7f52b77fe6c0 Recovering log #1064
|
||||
2025/05/17-16:40:21.135350 7f52b77fe6c0 Delete type=3 #1062
|
||||
2025/05/17-16:40:21.135414 7f52b77fe6c0 Delete type=0 #1064
|
||||
2025/05/17-16:40:56.004725 7f52b67fc6c0 Level-0 table #1069: started
|
||||
2025/05/17-16:40:56.004769 7f52b67fc6c0 Level-0 table #1069: 0 bytes OK
|
||||
2025/05/17-16:40:56.010954 7f52b67fc6c0 Delete type=0 #1067
|
||||
2025/05/17-16:40:56.011246 7f52b67fc6c0 Manual compaction at level-0 from '!tables!4l60Lxv8cpsyy2Cg' @ 72057594037927935 : 1 .. '!tables.results!tfaYKDZqu7kgZvRG.yvbwKursaixh2dby' @ 0 : 0; will stop at (end)
|
||||
2026/01/07-14:19:22.369653 7f93ebfff6c0 Recovering log #1245
|
||||
2026/01/07-14:19:22.381041 7f93ebfff6c0 Delete type=3 #1243
|
||||
2026/01/07-14:19:22.381133 7f93ebfff6c0 Delete type=0 #1245
|
||||
2026/01/07-15:04:29.629205 7f93e9ffb6c0 Level-0 table #1250: started
|
||||
2026/01/07-15:04:29.629243 7f93e9ffb6c0 Level-0 table #1250: 0 bytes OK
|
||||
2026/01/07-15:04:29.638743 7f93e9ffb6c0 Delete type=0 #1248
|
||||
2026/01/07-15:04:29.667035 7f93e9ffb6c0 Manual compaction at level-0 from '!tables!4l60Lxv8cpsyy2Cg' @ 72057594037927935 : 1 .. '!tables.results!tfaYKDZqu7kgZvRG.yvbwKursaixh2dby' @ 0 : 0; will stop at (end)
|
||||
|
||||
Binary file not shown.
BIN
packs/plats-dauberges/MANIFEST-001251
Normal file
BIN
packs/plats-dauberges/MANIFEST-001251
Normal file
Binary file not shown.
0
packs/plats-dauberges/lost/001220.log
Normal file
0
packs/plats-dauberges/lost/001220.log
Normal file
0
packs/tables-des-traductions/000896.log
Normal file
0
packs/tables-des-traductions/000896.log
Normal file
@@ -1 +1 @@
|
||||
MANIFEST-000713
|
||||
MANIFEST-000894
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
2025/05/17-16:42:35.467966 7f52bcdfa6c0 Recovering log #711
|
||||
2025/05/17-16:42:35.514839 7f52bcdfa6c0 Delete type=3 #709
|
||||
2025/05/17-16:42:35.514892 7f52bcdfa6c0 Delete type=0 #711
|
||||
2025/05/17-16:46:29.202536 7f52b67fc6c0 Level-0 table #716: started
|
||||
2025/05/17-16:46:29.202569 7f52b67fc6c0 Level-0 table #716: 0 bytes OK
|
||||
2025/05/17-16:46:29.239543 7f52b67fc6c0 Delete type=0 #714
|
||||
2025/05/17-16:46:29.239859 7f52b67fc6c0 Manual compaction at level-0 from '!journal!056ILNNrLiPq3Gi3' @ 72057594037927935 : 1 .. '!journal.pages!yfZxl4I7XAuUF6r3.apXmOlZRmGT4GreB' @ 0 : 0; will stop at (end)
|
||||
2026/01/07-15:05:51.826614 7f93ebfff6c0 Recovering log #892
|
||||
2026/01/07-15:05:51.837158 7f93ebfff6c0 Delete type=3 #890
|
||||
2026/01/07-15:05:51.837206 7f93ebfff6c0 Delete type=0 #892
|
||||
2026/01/07-15:06:45.521984 7f93e9ffb6c0 Level-0 table #897: started
|
||||
2026/01/07-15:06:45.522031 7f93e9ffb6c0 Level-0 table #897: 0 bytes OK
|
||||
2026/01/07-15:06:45.528836 7f93e9ffb6c0 Delete type=0 #895
|
||||
2026/01/07-15:06:45.529100 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!056ILNNrLiPq3Gi3' @ 72057594037927935 : 1 .. '!journal.pages!yfZxl4I7XAuUF6r3.apXmOlZRmGT4GreB' @ 0 : 0; will stop at (end)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
2025/05/17-16:40:21.177789 7f52b7fff6c0 Recovering log #707
|
||||
2025/05/17-16:40:21.188568 7f52b7fff6c0 Delete type=3 #705
|
||||
2025/05/17-16:40:21.188669 7f52b7fff6c0 Delete type=0 #707
|
||||
2025/05/17-16:40:56.031366 7f52b67fc6c0 Level-0 table #712: started
|
||||
2025/05/17-16:40:56.031424 7f52b67fc6c0 Level-0 table #712: 0 bytes OK
|
||||
2025/05/17-16:40:56.039038 7f52b67fc6c0 Delete type=0 #710
|
||||
2025/05/17-16:40:56.039225 7f52b67fc6c0 Manual compaction at level-0 from '!journal!056ILNNrLiPq3Gi3' @ 72057594037927935 : 1 .. '!journal.pages!yfZxl4I7XAuUF6r3.apXmOlZRmGT4GreB' @ 0 : 0; will stop at (end)
|
||||
2026/01/07-14:19:22.421981 7f93eb7fe6c0 Recovering log #888
|
||||
2026/01/07-14:19:22.432249 7f93eb7fe6c0 Delete type=3 #886
|
||||
2026/01/07-14:19:22.432317 7f93eb7fe6c0 Delete type=0 #888
|
||||
2026/01/07-15:04:29.679197 7f93e9ffb6c0 Level-0 table #893: started
|
||||
2026/01/07-15:04:29.679221 7f93e9ffb6c0 Level-0 table #893: 0 bytes OK
|
||||
2026/01/07-15:04:29.689398 7f93e9ffb6c0 Delete type=0 #891
|
||||
2026/01/07-15:04:29.713429 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!056ILNNrLiPq3Gi3' @ 72057594037927935 : 1 .. '!journal.pages!yfZxl4I7XAuUF6r3.apXmOlZRmGT4GreB' @ 0 : 0; will stop at (end)
|
||||
|
||||
Binary file not shown.
BIN
packs/tables-des-traductions/MANIFEST-000894
Normal file
BIN
packs/tables-des-traductions/MANIFEST-000894
Normal file
Binary file not shown.
0
packs/tables-des-traductions/lost/000863.log
Normal file
0
packs/tables-des-traductions/lost/000863.log
Normal file
312
patch-styles.css
312
patch-styles.css
@@ -7,4 +7,314 @@
|
||||
color: darkolivegreen;
|
||||
align-self: center;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Styles pour le module de voyage TravelV2 */
|
||||
.voyage-main-title {
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.voyage-route-title {
|
||||
font-size: 1.15em;
|
||||
font-weight: bold;
|
||||
margin-top: 0.8em;
|
||||
margin-bottom: 0.3em;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.voyage-section-title {
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
|
||||
.voyage-destinations-title {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.voyage-separator {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
border: 0;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
/* Styles pour le module Inn (Auberge) */
|
||||
.wfrp4e-inn-help h3,
|
||||
.wfrp4e-inn-list h3 {
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
color: #8b4513;
|
||||
border-bottom: 2px solid #d2691e;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.wfrp4e-inn-help ul,
|
||||
.wfrp4e-inn-list ul {
|
||||
margin-left: 1.5em;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.wfrp4e-inn-help li,
|
||||
.wfrp4e-inn-table-list li {
|
||||
margin: 0.3em 0;
|
||||
}
|
||||
|
||||
.wfrp4e-inn-help hr {
|
||||
margin: 1em 0;
|
||||
border: 0;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.wfrp4e-inn-help code {
|
||||
background-color: #f5f5f5;
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
color: #d63384;
|
||||
}
|
||||
|
||||
.wfrp4e-inn-help h4 {
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
margin: 0.8em 0 0.5em 0;
|
||||
color: var(--color-warm);
|
||||
}
|
||||
|
||||
/* Grille de boutons pour les tables */
|
||||
.wfrp4e-inn-table-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 0.5em;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.inn-table-btn {
|
||||
display: block;
|
||||
padding: 0.6em 0.8em;
|
||||
background: rgba(139, 69, 19, 0.12);
|
||||
border: 2px solid rgba(139, 69, 19, 0.35);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.95em;
|
||||
color: var(--color-warm);
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.inn-table-btn:hover {
|
||||
background: rgba(139, 69, 19, 0.2);
|
||||
border-color: rgba(139, 69, 19, 0.5);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.inn-table-btn i {
|
||||
margin-right: 0.4em;
|
||||
color: var(--color-warm-3);
|
||||
}
|
||||
|
||||
/* Anciens styles - à supprimer ou garder pour compatibilité */
|
||||
.wfrp4e-inn-table-list {
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.wfrp4e-inn-table-list li {
|
||||
margin: 0.5em 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.wfrp4e-inn-table-list a.action-link {
|
||||
display: inline-block;
|
||||
padding: 0.3em 0.8em;
|
||||
background-color: #8b4513;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.wfrp4e-inn-table-list a.action-link:hover {
|
||||
background-color: #a0522d;
|
||||
}
|
||||
|
||||
.wfrp4e-inn-table-list a.action-link i {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.inn-keyword {
|
||||
font-size: 0.85em;
|
||||
color: var(--color-grey2);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Styles pour les résultats de jets d'auberge - Thème WFRP4e */
|
||||
.wfrp4e-inn-result {
|
||||
margin: 0.3em 0;
|
||||
}
|
||||
|
||||
.wfrp4e-inn-result .message-header {
|
||||
text-shadow: 0px 0px 1px #00000087;
|
||||
border: 2px solid rgba(62, 0, 0, 0.3);
|
||||
padding: 0.4em 0.6em;
|
||||
margin-bottom: 0.5em;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.wfrp4e-inn-result .flavor-text {
|
||||
color: var(--color-warm);
|
||||
font-weight: bold;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.wfrp4e-inn-result .message-header i {
|
||||
margin-right: 0.3em;
|
||||
color: var(--color-warm-3);
|
||||
}
|
||||
|
||||
.inn-dish-name {
|
||||
font-size: 1.15em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 0.6em;
|
||||
margin: 0.3em 0;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.inn-roll-info {
|
||||
text-align: center;
|
||||
font-size: 0.85em;
|
||||
color: var(--color-grey2);
|
||||
margin-top: 0.4em;
|
||||
padding: 0.3em;
|
||||
}
|
||||
|
||||
.inn-roll-info i {
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
/* Styles pour le choix de menu */
|
||||
.wfrp4e-inn-menu-choice {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.wfrp4e-inn-menu-choice h3 {
|
||||
color: var(--color-warm);
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
|
||||
.inn-menu-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.inn-menu-btn {
|
||||
display: block;
|
||||
padding: 0.6em 1em;
|
||||
background: rgba(139, 69, 19, 0.1);
|
||||
border: 2px solid rgba(139, 69, 19, 0.3);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.inn-menu-btn:hover {
|
||||
background: rgba(139, 69, 19, 0.2);
|
||||
border-color: rgba(139, 69, 19, 0.5);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.inn-menu-btn i {
|
||||
margin-right: 0.4em;
|
||||
color: var(--color-warm-3);
|
||||
}
|
||||
|
||||
.inn-menu-desc {
|
||||
display: block;
|
||||
font-size: 0.85em;
|
||||
font-weight: normal;
|
||||
color: var(--color-grey2);
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
|
||||
/* Styles pour le résultat de menu */
|
||||
.wfrp4e-inn-menu-result {
|
||||
margin: 0.3em 0;
|
||||
}
|
||||
|
||||
.wfrp4e-inn-menu-result .message-header {
|
||||
text-shadow: 0px 0px 1px #00000087;
|
||||
border: 2px solid rgba(62, 0, 0, 0.3);
|
||||
padding: 0.4em 0.6em;
|
||||
margin-bottom: 0.5em;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.wfrp4e-inn-menu-result .flavor-text {
|
||||
color: var(--color-warm);
|
||||
font-weight: bold;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.inn-menu-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3em;
|
||||
}
|
||||
|
||||
.inn-menu-item {
|
||||
padding: 0.5em;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.inn-menu-item i {
|
||||
margin-right: 0.4em;
|
||||
color: var(--color-warm-3);
|
||||
}
|
||||
|
||||
.inn-menu-item strong {
|
||||
color: var(--color-warm);
|
||||
}
|
||||
|
||||
/* Bouton rapide Menu dans l'aide */
|
||||
.inn-menu-quick-btn {
|
||||
display: block;
|
||||
padding: 0.7em 1em;
|
||||
background: rgba(139, 69, 19, 0.15);
|
||||
border: 2px solid rgba(139, 69, 19, 0.4);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 1.05em;
|
||||
color: var(--color-warm);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.inn-menu-quick-btn:hover {
|
||||
background: rgba(139, 69, 19, 0.25);
|
||||
border-color: rgba(139, 69, 19, 0.6);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.inn-menu-quick-btn i {
|
||||
margin-right: 0.5em;
|
||||
color: var(--color-warm-3);
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
return args.item?.system?.isRanged && args.data.targets[0]?.actor?.sizeNum < 3
|
||||
return args.item?.system?.isProjectiles && args.data.targets[0]?.actor?.sizeNum < 3
|
||||
@@ -2,24 +2,24 @@
|
||||
// takes 3 Poisoned Conditions that cannot be resisted at first,
|
||||
await this.actor.addCondition("poisoned", 3)
|
||||
|
||||
// recovers a number of Wounds equal to their Toughness Bonus,
|
||||
// recovers a number of Wounds equal to their Toughness Bonus,
|
||||
await this.actor.modifyWounds(this.actor.system.characteristics.t.bonus)
|
||||
|
||||
// and acquires the Regenerate Creature Trait.
|
||||
const hasRegenerate = this.actor.has("Régénération")
|
||||
if (hasRegenerate === undefined) {
|
||||
// and acquires the Régénération Creature Trait.
|
||||
const hasRégénération = this.actor.has("Régénération")
|
||||
if (hasRégénération === undefined) {
|
||||
fromUuid("Compendium.wfrp4e-core.items.SfUUdOGjdYpr3KSR").then(trait => {
|
||||
let traitItem = trait.toObject()
|
||||
this.actor.createEmbeddedDocuments("Item", [traitItem], {fromEffect: this.effect.id})
|
||||
})
|
||||
}
|
||||
|
||||
this.script.scriptMessage(`<p><strong>${this.actor.prototypeToken.name}</strong> :
|
||||
this.script.message(`<p><strong>${this.actor.prototypeToken.name}</strong> a :
|
||||
<ul>
|
||||
<li>Reçoit 3 états Empoisonnés, sans Test de Résistance possible</li>
|
||||
<li>Récupère ${this.actor.system.characteristics.t.bonus} Blessures</li>
|
||||
<li>Acuiert le Trait de Creature Régénération.</li>
|
||||
<li>Acquis 3 états Empoisonné qui ne peuvent pas être résistés au début du round.</li>
|
||||
<li>Récupération de ${this.actor.system.characteristics.t.bonus} Blessures</li>
|
||||
<li>Acquisition du Trait de Créature Régénération.</li>
|
||||
</ul>
|
||||
C'est à Ranaldde choisir si la régénératin peut guérir de l'empoisonnement.</p>
|
||||
<p>Lorsque tout les états Empoisonnés sont terminés, le Trait Régénération est perdu également.</p>`,
|
||||
{ whisper: ChatMessage.getWhisperRecipients("GM"), blind: true })
|
||||
C’est à Ranald de décider si leur régénération peut dépasser leur empoisonnement.</p>
|
||||
<p>Lorsque tous les états Empoisonné sont perdus, la Régénération l’est aussi.</p>`,
|
||||
{ whisper: ChatMessage.getWhisperRecipients("GM"), blind: true })
|
||||
|
||||
3
scripts/07tvKnPT8ICtv2us.js
Normal file
3
scripts/07tvKnPT8ICtv2us.js
Normal file
@@ -0,0 +1,3 @@
|
||||
if (args.test.result.misfire && args.test.result.roll !== 100) {
|
||||
delete args.test.result.misfire
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
return !args.options.terror && !args.extendedTest?.flags.wfrp4e?.fear
|
||||
return !args.context.terror && !args.extendedTest?.flags.wfrp4e?.fear
|
||||
@@ -1 +0,0 @@
|
||||
return !args.skill?.name.includes(game.i18n.localize("NAME.Row")) && !args.skill?.name.includes(game.i18n.localize("NAME.Sail"));
|
||||
@@ -1,4 +1,4 @@
|
||||
let spells = await warhammer.utility.findAllItems("spell", "Chargement des sorts")
|
||||
let spells = await warhammer.utility.findAllItems("spell", "Chargement des sorts", true, ["system.lore.value"])
|
||||
|
||||
let text = (await game.wfrp4e.tables.rollTable("random-caster", {hideDSN: true})).result
|
||||
|
||||
@@ -6,15 +6,15 @@ lore = Array.from(text.matchAll(/{(.+?)}/gm))[0][1]
|
||||
|
||||
if (text == "GM's Choice")
|
||||
{
|
||||
return this.script.scriptNotification(text)
|
||||
return this.script.notification(text)
|
||||
}
|
||||
|
||||
if (spellsWithLore.length > 0)
|
||||
{
|
||||
let spellsWithLore = spells.filter(i => game.wfrp4e.config.magicLores[i.system.lore.value] == lore)
|
||||
let selectedSpell = spellsWithLore[Math.floor(CONFIG.Dice.randomUniform() * spellsWithLore.length)]
|
||||
this.script.scriptNotification(selectedSpell.name);
|
||||
this.actor.createEmbeddedDocuments("Item", [selectedSpell])
|
||||
this.script.notification(selectedSpell.name);
|
||||
this.actor.createEmbeddedDocuments("Item", [(await fromUuid(selectedSpell)).toObject()])
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
4
scripts/0H2syk6qc0sCY0pj.js
Normal file
4
scripts/0H2syk6qc0sCY0pj.js
Normal file
@@ -0,0 +1,4 @@
|
||||
if ( args.actor.has(game.i18n.localize("NAME.AA"), "talent") ||
|
||||
args.actor.has(game.i18n.localize("NAME.SecondSight"), "talent") ) {
|
||||
args.modifiers.other.push({label : this.effect.name, value : 5, details : "La Cible possède Harmonisation Aethyrique ou Seconde Vue"});
|
||||
}
|
||||
1
scripts/0IAc5VFR2ogXOaGE.js
Normal file
1
scripts/0IAc5VFR2ogXOaGE.js
Normal file
@@ -0,0 +1 @@
|
||||
return args.characteristic != "int" || args.type != "characteristic"
|
||||
1
scripts/0NAD1LNcVyAZ1fC7.js
Normal file
1
scripts/0NAD1LNcVyAZ1fC7.js
Normal file
@@ -0,0 +1 @@
|
||||
return args.skill?.name !== game.i18n.localize("NAME.Cool") && args.skill?.name !== game.i18n.localize("NAME.Résistance");
|
||||
1
scripts/0PzfGjGhHQbxEwlb.js
Normal file
1
scripts/0PzfGjGhHQbxEwlb.js
Normal file
@@ -0,0 +1 @@
|
||||
this.item.update({"system.AP" : {lArm : 0, rArm : 0, lLeg : 0, rLeg: 0}});
|
||||
@@ -1,7 +1,7 @@
|
||||
let table = game.wfrp4e.tables.findTable("mutatephys");
|
||||
if (!table)
|
||||
{
|
||||
return ui.notifications.error("La table des Mutations n'a pas été trouvée. Assurez vous que la table avec la clé `mutatephys` est bien importée dans le monde.")
|
||||
return ui.notifications.error("Table de Mutation introuvable, veuillez vous assurer qu'une table avec la clé `mutatephys` est importée dans le monde.")
|
||||
}
|
||||
let result = (await table.roll()).results[0];
|
||||
let uuid = `Compendium.${result.documentCollection}.${result.documentId}`
|
||||
@@ -9,10 +9,10 @@ let item = await fromUuid(uuid);
|
||||
|
||||
if (item)
|
||||
{
|
||||
this.script.scriptNotification(`${item.name} added`)
|
||||
this.script.notification(`${item.name} added`)
|
||||
this.actor.createEmbeddedDocuments("Item", [item])
|
||||
}
|
||||
else
|
||||
else
|
||||
{
|
||||
ui.notifications.error("L'item ne peut être trouvé: " + uuid)
|
||||
ui.notifications.error("Impossible de trouver l'objet : " + uuid)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ if (location)
|
||||
|
||||
if (dropped.length)
|
||||
{
|
||||
this.script.scriptNotification(`Lache ${dropped.map(i => i.name).join(", ")}!`)
|
||||
this.script.notification(`Lache ${dropped.map(i => i.name).join(", ")}!`)
|
||||
for(let weapon of dropped)
|
||||
{
|
||||
await weapon.system.toggleEquip();
|
||||
@@ -16,6 +16,6 @@ if (location)
|
||||
|
||||
let roll = await new Roll("max(1, 1d10 - @system.characteristics.t.bonus)", this.actor).roll()
|
||||
|
||||
roll.toMessage(this.script.getChatData({flavor : `${this.effect.name} (Durée)`}));
|
||||
roll.toMessage(this.script.getChatData({flavor : `${this.effet.name} (Durée)`}));
|
||||
|
||||
this.effect.updateSource({"duration.rounds" : roll.total})
|
||||
this.effet.updateSource({"duration.rounds" : roll.total})
|
||||
@@ -1,9 +0,0 @@
|
||||
if (args.skill?.name != game.i18n.localize("NAME.Gossip"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
args.data.canReverse = true; // Kind of a kludge here, the talent Tests has a specific condition, but the description simply says "any gossip test can be reversed" so check it here instead of submission
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
if (args.applyAP && args.modifiers.ap.metal)
|
||||
if (args.applyAP && args.modifiers.ap.metal)
|
||||
{
|
||||
args.modifiers.ap.ignored += args.modifiers.ap.metal
|
||||
args.modifiers.ap.details.push("<strong>" + this.effect.name + "</strong>: Ignore le m<>tal (" + args.modifiers.ap.metal + ")");
|
||||
args.modifiers.ap.details.push("<strong>" + this.effect.name + "</strong>: Ignorer Métal (" + args.modifiers.ap.metal + ")");
|
||||
args.modifiers.ap.metal = 0
|
||||
}
|
||||
3
scripts/0amHqfjTRp5ff6Op.js
Normal file
3
scripts/0amHqfjTRp5ff6Op.js
Normal file
@@ -0,0 +1,3 @@
|
||||
let wounds = this.effect.sourceActor.system.characteristics.wp.bonus;
|
||||
this.actor.modifyWounds(wounds);
|
||||
this.script.message(`Soigne ${wounds} Blessures`);
|
||||
1
scripts/0hAbiNR1nFkpFMRz.js
Normal file
1
scripts/0hAbiNR1nFkpFMRz.js
Normal file
@@ -0,0 +1 @@
|
||||
if (!this.actor.has(game.i18n.localize("NAME.SecondSight"),"talent")) this.actor.addEffectItems("Compendium.wfrp4e-core.items.Item.OEjUvJKi0xmBwbS2", this.effect)
|
||||
5
scripts/0hgTyeaEhMBLwzUn.js
Normal file
5
scripts/0hgTyeaEhMBLwzUn.js
Normal file
@@ -0,0 +1,5 @@
|
||||
let fearCounter = this.item.effects.filter(i => i.name == this.effect.name).length;
|
||||
|
||||
fearCounter += Number(this.actor.has("Peur")?.system.specification.value) || 0
|
||||
|
||||
game.wfrp4e.utility.postPeur(fearCounter || 1, this.effect.name)
|
||||
@@ -1,4 +1,4 @@
|
||||
if (args.test.spell)
|
||||
{
|
||||
args.test.result.other.push(`<strong>${this.effect.name}</strong>: Automatic Failure`)
|
||||
args.test.result.other.push(`<strong>${this.effect.name}</strong>: Echec Automatique`)
|
||||
}
|
||||
1
scripts/0kUalAsb4OhtYbaJ.js
Normal file
1
scripts/0kUalAsb4OhtYbaJ.js
Normal file
@@ -0,0 +1 @@
|
||||
return args.item?.system?.attackType == "ranged" && !this.actor.system.canFly.effects.filter(e => e.name == "Flying")[0].disabled
|
||||
@@ -1,7 +0,0 @@
|
||||
this.actor.setupSkill(game.i18n.localize("NAME.Cool"), {skipTargets: true, appendTitle : ` - ${this.effect.name}`}).then(async test => {
|
||||
await test.roll()
|
||||
if (test.failed)
|
||||
{
|
||||
this.actor.addCondition("stunned")
|
||||
}
|
||||
})
|
||||
3
scripts/0uUxvBLJC76WIIBC.js
Normal file
3
scripts/0uUxvBLJC76WIIBC.js
Normal file
@@ -0,0 +1,3 @@
|
||||
let species = await ValueDialog.create({text : "Saisir l'espèce cible (singulier)", title : this.effect.name})
|
||||
|
||||
this.effect.updateSource({name : this.effect.setSpecifier(species)});
|
||||
7
scripts/0wR0LWpfhLFA240I.js
Normal file
7
scripts/0wR0LWpfhLFA240I.js
Normal file
@@ -0,0 +1,7 @@
|
||||
let value = await ValueDialog.create({
|
||||
title : this.script.label,
|
||||
text: "Notes de Victoire pour le Journal d'Expérience"
|
||||
});
|
||||
value
|
||||
? this.actor.system.awardExp(50, value)
|
||||
: this.actor.system.awardExp(50, this.script.label)
|
||||
4
scripts/0wmIC2MssUX6LW3N.js
Normal file
4
scripts/0wmIC2MssUX6LW3N.js
Normal file
@@ -0,0 +1,4 @@
|
||||
if (args.test.options.doomboltRolled)
|
||||
{
|
||||
args.test.result.damage += 4;
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
return args.options.terror || args.extendedTest?.flags.wfrp4e?.fear
|
||||
return args.context.terror || args.extendedTest?.flags.wfrp4e?.fear
|
||||
@@ -1,6 +1,6 @@
|
||||
let type = this.item.getFlag("wfrp4e", "breath");
|
||||
|
||||
if (["feu", "electricité", "poison"].includes(type))
|
||||
if (["fire", "electricity", "poison"].includes(type))
|
||||
{
|
||||
args.applyAP = false;
|
||||
}
|
||||
@@ -2,6 +2,6 @@ let state = !this.effect.disabled;
|
||||
this.effect.update({"disabled": state});
|
||||
|
||||
if (state)
|
||||
return ui.notifications.info("EFFECT.CreatureBackInWater", {localize: true})
|
||||
return ui.notifications.info("Effet.CreatureBackInWater", {localize: true})
|
||||
|
||||
return ui.notifications.info("EFFECT.CreatureOutOfWater", {localize: true});
|
||||
return ui.notifications.info("Effet.CreatureOutOfWater", {localize: true});
|
||||
@@ -1,31 +1,31 @@
|
||||
if (!this.item.name.includes("(") || this.item.system.tests.value.includes("Terrain"))
|
||||
if (!this.item.name.includes("(") || this.item.system.Tests.value.includes("Terrain") || this.item.system.Tests.value.toLowerCase().includes("(any)"))
|
||||
{
|
||||
let tests = this.item.system.tests.value
|
||||
let Tests = this.item.system.Tests.value
|
||||
let name = this.item.name
|
||||
|
||||
// If name already specifies, make sure tests value reflects that
|
||||
if (name.includes("("))
|
||||
// If name already specifies, make sure Tests value reflects that
|
||||
if (name.includes("(") && !name.toLowerCase().includes("(any)"))
|
||||
{
|
||||
let terrain = name.split("(")[1].split(")")[0]
|
||||
tests = tests.replace("Terrain", terrain)
|
||||
tests = tests.replace("the Terrain", terrain)
|
||||
}
|
||||
else // If no sense specified, provide dialog choice
|
||||
{
|
||||
let choice = await ItemDialog.create(ItemDialog.objectToArray({
|
||||
coastal : "Côtes",
|
||||
coastal : "Littoral",
|
||||
deserts : "Déserts",
|
||||
marshes : "Marches",
|
||||
rocky : "Rocailles",
|
||||
tundra : "Tundra",
|
||||
woodlands : "Forêts"
|
||||
}, this.item.img), 1, "Choisir le Terrain");
|
||||
marshes : "Marécages",
|
||||
rocky : "Rocailleux",
|
||||
tundra : "Toundra",
|
||||
woodlands : "Régions boisées"
|
||||
}, this.item.img), 1, "Choisissez un Terrain");
|
||||
if (choice[0])
|
||||
{
|
||||
name = `${name.split("(")[0].trim()} (${choice[0].name})`
|
||||
tests = tests.replace("Terrain", "Terrain " + choice[0].name )
|
||||
tests = tests.replace("Terrain", choice[0].name + " Terrain")
|
||||
}
|
||||
}
|
||||
|
||||
this.effect.updateSource({name})
|
||||
this.effet.updateSource({name})
|
||||
this.item.updateSource({name, "system.tests.value" : tests})
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
return !args.skill?.name.includes(game.i18n.localize("NAME.Language"));
|
||||
@@ -14,13 +14,13 @@
|
||||
|
||||
let updateObj = this.actor.toObject();
|
||||
|
||||
let talents = (await Promise.all([game.wfrp4e.tables.rollTable("talents"), game.wfrp4e.tables.rollTable("talents"), game.wfrp4e.tables.rollTable("talents")])).map(i => i.text)
|
||||
|
||||
let talents = (await Promise.tout([game.wfrp4e.tables.rollTable("talents"), game.wfrp4e.tables.rollTable("talents"), game.wfrp4e.tables.rollTable("talents")])).map(i => i.text)
|
||||
|
||||
for (let ch in characteristics)
|
||||
{
|
||||
updateObj.system.characteristics[ch].modifier += characteristics[ch];
|
||||
}
|
||||
|
||||
|
||||
for (let talent of talents)
|
||||
{
|
||||
let talentItem = await game.wfrp4e.utility.findTalent(talent)
|
||||
@@ -28,13 +28,12 @@
|
||||
{
|
||||
items.push(talentItem.toObject());
|
||||
}
|
||||
else
|
||||
else
|
||||
{
|
||||
ui.notifications.warn(`Impossible de trouver ${talent}`, {permanent : true})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
await this.actor.update(updateObj)
|
||||
this.actor.createEmbeddedDocuments("Item", items);
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ let characteristics = {
|
||||
"wp" : 10,
|
||||
"fel" : 10
|
||||
}
|
||||
let skills = ["Intimidate", "Commandement" , "Perception"]
|
||||
let skills = ["Intimidation", "Commandement", "Perception"]
|
||||
let skillAdvancements = [10, 10, 10]
|
||||
let talents = ["Combat Aware", "Drilled", "Menaçant", "Robust"]
|
||||
let trappings = ["Arme simple", "Mail Coat", "Mail Chausses"]
|
||||
let specialItems = [
|
||||
]
|
||||
let talents = ["Vigilance", "Coude-à-coude", "Menaçant", "Robuste"]
|
||||
let trappings = ["Arme simple", "Cotte de Mailles", "Chausses de Mailles"]
|
||||
let specialItems = [
|
||||
]
|
||||
let items = [];
|
||||
|
||||
let updateObj = this.actor.toObject();
|
||||
@@ -28,11 +28,11 @@ for (let ch in characteristics)
|
||||
for (let item of specialItems) {
|
||||
let newItem
|
||||
if (item.type == "weapon") {
|
||||
newItem = new ItemWfrp4e({ name: item.name, type: item.type, system: { equipped: true, damage: {value: item.damage}} })
|
||||
newItem = new ItemWFRP4e({ name: item.name, type: item.type, system: { equipped: true, damage: {value: item.damage}} })
|
||||
} else if (item.type == "trapping") {
|
||||
newItem = new ItemWfrp4e({ img: "systems/wfrp4e/icons/blank.png", name: item.name, type: item.type, system: { worn: true, trappingType: { value: item.trappingType} } } )
|
||||
newItem = new ItemWFRP4e({ img: "systems/wfrp4e/icons/blank.png", name: item.name, type: item.type, system: { worn: true, trappingType: { value: item.trappingType} } } )
|
||||
} else {
|
||||
newItem = new ItemWfrp4e({ img: "systems/wfrp4e/icons/blank.png", name: item.name, type: item.type })
|
||||
newItem = new ItemWFRP4e({ img: "systems/wfrp4e/icons/blank.png", name: item.name, type: item.type })
|
||||
}
|
||||
items.push(newItem.toObject())
|
||||
}
|
||||
@@ -44,7 +44,7 @@ for (let index = 0; index < skills.length; index++)
|
||||
skillItem = updateObj.items.find(i => i.name == skill && i.type == "skill")
|
||||
if (skillItem)
|
||||
skillItem.system.advances.value += skillAdvancements[index]
|
||||
else
|
||||
else
|
||||
{
|
||||
skillItem = await game.wfrp4e.utility.findSkill(skill)
|
||||
skillItem = skillItem.toObject();
|
||||
@@ -60,13 +60,13 @@ for (let talent of talents)
|
||||
{
|
||||
items.push(talentItem.toObject());
|
||||
}
|
||||
else
|
||||
else
|
||||
{
|
||||
ui.notifications.warn(`Impossible de trouver ${talent}`, {permanent : true})
|
||||
}
|
||||
}
|
||||
|
||||
for (let trapping of trappings)
|
||||
for (let trapping of trappings)
|
||||
{
|
||||
let trappingItem = await game.wfrp4e.utility.findItem(trapping)
|
||||
if (trappingItem)
|
||||
@@ -77,13 +77,13 @@ for (let trapping of trappings)
|
||||
|
||||
items.push(trappingItem);
|
||||
}
|
||||
else
|
||||
else
|
||||
{
|
||||
ui.notifications.warn(`Impossible de trouver ${trapping}`, {permanent : true})
|
||||
ui.notifications.warn(`Could not find ${trapping}`, {permanent : true})
|
||||
}
|
||||
}
|
||||
|
||||
updateObj.name = updateObj.name += " " + this.effect.name
|
||||
updateObj.name = updateObj.name += " " + this.effet.name
|
||||
|
||||
await this.actor.update(updateObj)
|
||||
this.actor.createEmbeddedDocuments("Item", items);
|
||||
10
scripts/1Du4e27M8WgP2iui.js
Normal file
10
scripts/1Du4e27M8WgP2iui.js
Normal file
@@ -0,0 +1,10 @@
|
||||
if (args.equipped === true && this.actor.name !== "Kurgorn Three-eyes")
|
||||
{
|
||||
this.actor.addCondition("blinded", 1, {"statuses" : ["blinded", "blind"]})
|
||||
this.script.notification(`Aveuglé en portant ${this.item.name}`);
|
||||
}
|
||||
|
||||
if (args.equipped === false && this.actor.name !== "Kurgorn Three-eyes")
|
||||
{
|
||||
this.actor.removeCondition("blinded")
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user