Compare commits

..

17 Commits

Author SHA1 Message Date
752a6701c0 Fix scripts ! 2026-01-29 11:43:28 +01:00
0e8237c233 Ajout de la commande /voyage et grosse MAJK de la commande /auberge 2026-01-07 15:55:25 +01:00
df26a699a0 Ajout de la commande /voyage et grosse MAJK de la commande /auberge 2026-01-07 15:53:44 +01:00
0a1fa37e49 Ajout de la commande /voyage et grosse MAJK de la commande /auberge 2026-01-07 15:14:42 +01:00
5b7fba4c87 Ajout de la commande /voyage et grosse MAJK de la commande /auberge 2026-01-07 15:13:54 +01:00
d710061eeb Ajout de la commande /voyage et grosse MAJK de la commande /auberge 2026-01-07 15:13:26 +01:00
d6f2feca07 Ajout de la commande /voyage et grosse MAJK de la commande /auberge 2026-01-07 15:07:05 +01:00
f525b6c07a Ajout de la commande /voyage et grosse MAJK de la commande /auberge 2026-01-07 15:04:49 +01:00
c8119601d8 Enhance robustness 2026-01-05 17:44:31 +01:00
d1da169fa3 Enhance robustness 2026-01-05 17:24:01 +01:00
07928acb48 Ajout Aux Armes 2025-11-12 22:26:13 +01:00
9860535f5f Ajotu Aux Armes 2025-11-12 22:24:58 +01:00
fc560ddee7 Align with v9.3.0 2025-10-31 18:05:03 +00:00
47454b30f1 Align with v9.3.0 2025-10-31 18:04:13 +00:00
4ee45273b3 COrrection sur Natation - again 2025-10-13 20:59:50 +02:00
94ca8cb6ea Align to v9.2.X 2025-09-23 16:44:41 +02:00
6af8d03d22 Fix natation/swim 2025-07-25 15:03:04 +02:00
2761 changed files with 24492 additions and 16526 deletions

View File

@@ -2841,6 +2841,12 @@
"name": "Combattant au contact", "name": "Combattant au contact",
"tests": "Corps à corps quand vous combattez au contact ou que vous allez 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 dun 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": "Infighter",
"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>&nbsp;</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>&nbsp;Normal × 20</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>4</p>\n</td>\n<td>\n<p>&nbsp;Normal × 50</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>5</p>\n</td>\n<td>\n<p>&nbsp;Normal × 100</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>6</p>\n</td>\n<td>\n<p>&nbsp;Normal × 200</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>7</p>\n</td>\n<td>\n<p>&nbsp;Normal × 500</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>8</p>\n</td>\n<td>\n<p>&nbsp;Normal × 1000</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>9</p>\n</td>\n<td>\n<p>&nbsp;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 labbesse Brigitte van der Hoogenband est pris dassaut par les peaux-vertes, et les choses vont mal. De fait, elle décide de renforcer lesprit 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é quelle possède un Bonus de Sociabilité de 6, et quelle 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 dencouragement « MAINTENEZ LES RANGS ! »</em>", "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>&nbsp;</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>&nbsp;Normal × 20</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>4</p>\n</td>\n<td>\n<p>&nbsp;Normal × 50</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>5</p>\n</td>\n<td>\n<p>&nbsp;Normal × 100</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>6</p>\n</td>\n<td>\n<p>&nbsp;Normal × 200</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>7</p>\n</td>\n<td>\n<p>&nbsp;Normal × 500</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>8</p>\n</td>\n<td>\n<p>&nbsp;Normal × 1000</p>\n</td>\n</tr>\n<tr>\n<td>\n<p>9</p>\n</td>\n<td>\n<p>&nbsp;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 labbesse Brigitte van der Hoogenband est pris dassaut par les peaux-vertes, et les choses vont mal. De fait, elle décide de renforcer lesprit 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é quelle possède un Bonus de Sociabilité de 6, et quelle 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 dencouragement « MAINTENEZ LES RANGS ! »</em>",
"id": "WCXnFSV4WOSmzzc4", "id": "WCXnFSV4WOSmzzc4",

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
{
"folders": [
{
"path": "."
},
{
"path": "../WFRP4e-FoundryVTT"
},
{
"path": "../WarhammerLibrary-FVTT"
}
],
"settings": {}
}

14
fr.json
View File

@@ -314,6 +314,10 @@
"SHEET.NPCSheetNoTheme":"Fiche de PNJ (sans thème)", "SHEET.NPCSheetNoTheme":"Fiche de PNJ (sans thème)",
"SHEET.VehicleSheet":"Fiche de véhicule", "SHEET.VehicleSheet":"Fiche de véhicule",
"SHEET.VehicleSheetNoTheme":"Fiche de véhicule (sans thème)", "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.Actor.vehicle":"Fiche de Véhicule",
"SHEETS.Item.ammunition":"Fiche de Munitions", "SHEETS.Item.ammunition":"Fiche de Munitions",
@@ -429,6 +433,7 @@
"ITEM.Roles":"Roles", "ITEM.Roles":"Roles",
"ITEM.VitalRoles":"Roles vitaux", "ITEM.VitalRoles":"Roles vitaux",
"ITEM.LearningXP":"XP d'Apprentissage", "ITEM.LearningXP":"XP d'Apprentissage",
"ITEM.Custom":"Personnalisé",
"TOKEN.MOVEMENT.Status.immobile":"Immobile", "TOKEN.MOVEMENT.Status.immobile":"Immobile",
"TOKEN.MOVEMENT.Status.restricted":"Restreint", "TOKEN.MOVEMENT.Status.restricted":"Restreint",
@@ -645,6 +650,7 @@
"Hide": "Cacher", "Hide": "Cacher",
"Targets": "Cibles", "Targets": "Cibles",
"Melee": "Corps à corps", "Melee": "Corps à corps",
"Swim":" Natation",
"Cost": "Prix", "Cost": "Prix",
"Length": "Longueur", "Length": "Longueur",
"Cargo": "Cargaison", "Cargo": "Cargaison",
@@ -1019,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.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.ChoosePassenger":"Choisissez un passager",
"DIALOG.PostQuantityContent":"Combien de fois cet item peut être récupéré? Laissez vide pour illimité.", "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.CareerChoose" : "Choisissez votre carrière",
"CHAT.DamageError" : "Erreur de calcul des dégâts:", "CHAT.DamageError" : "Erreur de calcul des dégâts:",
@@ -1183,6 +1190,8 @@
"CHAT.Vital":"Vital", "CHAT.Vital":"Vital",
"CHAT.DiseaseRollError":"Une erreur s'est produite lors du jet d'incubation ou de durée de la maladie.", "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.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.SpeciesSkills" : "Impossible d'ajouter des compétences pour les races",
"Error.SpeciesTalents" : "Impossible d'ajouter des talents pour les races", "Error.SpeciesTalents" : "Impossible d'ajouter des talents pour les races",
@@ -2695,6 +2704,10 @@
"major":"Majeur", "major":"Majeur",
"minor":"Mineur", "minor":"Mineur",
"moderate":"Modéré", "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":{ "WH":{
"TransferType":{ "TransferType":{
@@ -2715,5 +2728,6 @@
"SHEET.ExperienceLog":"Journal d'Expérince", "SHEET.ExperienceLog":"Journal d'Expérince",
"SHEET.Attacker":"Attaquant", "SHEET.Attacker":"Attaquant",
"SHEET.Randomize":"Aléatoire", "SHEET.Randomize":"Aléatoire",
"SHEET.RequiredTrappingsError":"Impossible de lancer les revenus sans avoir tous les équipements requis dans cette carrière !",
"Sheet.RollIncome":"Revenu" "Sheet.RollIncome":"Revenu"
} }

View File

@@ -8,8 +8,9 @@
} }
], ],
"url": "https://www.uberwald.me/gitea/public/foundryvtt-wh4-lang-fr-fr", "url": "https://www.uberwald.me/gitea/public/foundryvtt-wh4-lang-fr-fr",
"version": "9.1.4", "version": "9.3.6",
"esmodules": [ "esmodules": [
"wh4_fr.js",
"modules/babele-register.js", "modules/babele-register.js",
"modules/addon-register.js", "modules/addon-register.js",
"modules/import-stat-2.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", "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-1-4.zip", "download": "https://www.uberwald.me/gitea/public/foundryvtt-wh4-lang-fr-fr/archive/foundryvtt-wh4-lang-fr-9-3-6.zip",
"id": "wh4-fr-translation", "id": "wh4-fr-translation",
"compatibility": { "compatibility": {
"minimum": "13", "minimum": "13",

View File

@@ -82,39 +82,8 @@ const _patch_up_in_arms = () => {
} }
/************************************************************************************/ /************************************************************************************/
/* Manages /auberge command */ /* Module /auberge géré par modules/inn/inn-init.js */
const _manage_inn_roll = async (content, msg) => { // L'ancienne implémentation a été migrée vers le module inn pour cohérence avec /voyage
// 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;
}
}
/************************************************************************************/ /************************************************************************************/
let __eis_tables = { let __eis_tables = {
@@ -262,17 +231,13 @@ const __check_fix_wrong_modules = (chatFlag, patchFinished) => {
} }
} else if (game.user.isGM && patchFinished) { } else if (game.user.isGM && patchFinished) {
ChatMessage.create({ 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.5 : <ul><li>Ajout de la commande /voyage !</li><li>Améliorations de la commande /auberge</li><li>Les joueurs doivent désormais pouvoir créer leur persos</li><li>Très grosses mise à jour des scripts d'Effets et de leur traduction</li></ul></div>",
user: game.user.id, user: game.user.id,
whisper: ChatMessage.getWhisperRecipients("GM") 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) => { const convertColumnToMulti = (table) => {
let columns = table.columns; let columns = table.columns;
@@ -366,14 +331,8 @@ const __add_actors_translation = () => {
/************************************************************************************/ /************************************************************************************/
/* Hook for specific command */ /* Hook for specific command - Module /auberge migré vers modules/inn/ */
Hooks.on("chatMessage", (html, content, msg) => { // La commande /auberge est désormais gérée par le module inn-init.js
if (content.toLowerCase().includes('auberge')) {
_manage_inn_roll(content, msg);
return false;
}
});
/************************************************************************************/ /************************************************************************************/
/* Additionnal hooks ready */ /* Additionnal hooks ready */
@@ -426,7 +385,9 @@ Hooks.on('ready', () => {
"doom": "Maudit (-40)" "doom": "Maudit (-40)"
} }
game.wfrp4e.warnDialog.render(true, { focus: true, left: 20, top: 20 }); if (game.user.isGM) {
game.wfrp4e.warnDialog.render(true, { focus: true, left: 20, top: 20 });
}
//setTimeout( __check_fix_wrong_modules, 2000, true, false); //setTimeout( __check_fix_wrong_modules, 2000, true, false);
setTimeout(__check_fix_wrong_modules, 20000, true, true); setTimeout(__check_fix_wrong_modules, 20000, true, true);
setTimeout(__add_actors_translation, 21000, false, true); setTimeout(__add_actors_translation, 21000, false, true);

View File

@@ -248,16 +248,16 @@ Hooks.once('init', () => {
let translw = translItem?.name || undefined let translw = translItem?.name || undefined
if (translw && translw != s1) { if (translw && translw != s1) {
let res2 = re.exec(translw); let res2 = re.exec(translw);
transl = res2[1] + "(" + subword + ")"; transl = res2[1].trim() + " (" + subword + ")";
} else { } else {
s1 = res[1].trim() + " ( )"; s1 = res[1].trim() + " ( )";
translItem = game.babele.translate(compData.metadata.id, { name: s1, type: "skill" }, true) translItem = game.babele.translate(compData.metadata.id, { name: s1, type: "skill" }, true)
translw = translItem?.name || undefined translw = translItem?.name || undefined
if(translw) { if(translw) {
let res2 = re.exec(translw); let res2 = re.exec(translw);
transl = res2[1] + "(" + subword + ")"; transl = res2[1].trim() + " (" + subword + ")";
} else { } else {
transl = res[1] + " (" + subword + ")"; transl = res[1].trim() + " (" + subword + ")";
} }
} }
} }
@@ -366,7 +366,7 @@ Hooks.once('init', () => {
translItem = game.babele.translate(compData.metadata.id, { name: s1 }, true) translItem = game.babele.translate(compData.metadata.id, { name: s1 }, true)
let translw = translItem?.name || undefined let translw = translItem?.name || undefined
if (translw && translw != s1) { if (translw && translw != s1) {
transl = translw + " (" + subword + ")"; transl = translw.trim() + " (" + subword + ")";
} }
} }
} }
@@ -424,7 +424,9 @@ Hooks.once('init', () => {
if (trait_fr?.name && trait_fr?.name != name_en) { if (trait_fr?.name && trait_fr?.name != name_en) {
trait_fr.name = trait_fr.name || trait_en.name trait_fr.name = trait_fr.name || trait_en.name
trait_en.name = nbt + trait_fr.name + special; trait_en.name = nbt + trait_fr.name + special;
trait_en.system.description.value = trait_fr.system.description.value; if ( trait_en.system?.description?.value) {
trait_en.system.description.value = trait_fr.system.description.value;
}
if (trait_en.system?.specification && isNaN(trait_en.system.specification.value)) { // This is a string, so translate it if (trait_en.system?.specification && isNaN(trait_en.system.specification.value)) { // This is a string, so translate it
//console.log("Translating : ", trait_en.system.specification.value); //console.log("Translating : ", trait_en.system.specification.value);
trait_en.system.specification.value = game.i18n.localize(trait_en.system.specification.value.trim()); trait_en.system.specification.value = game.i18n.localize(trait_en.system.specification.value.trim());

View File

@@ -2,26 +2,40 @@
export class WH4FRPatchConfig { export class WH4FRPatchConfig {
/************************************************************************************/ /************************************************************************************/
static translateSkillList( skillList) { static translateSkillList(skillList) {
if (!skillList || skillList.length == 0) {
return skillList;
}
let compendiumName = 'wfrp4e-core.items' let compendiumName = 'wfrp4e-core.items'
let newList = []; 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 special = "";
let newName = compName; let newName = compName;
let baseName = compName let baseName = compName
if ( compName.includes("(") && compName.includes(")") ) { // Then process specific skills name with (xxxx) inside if (compName.includes("(") && compName.includes(")")) { // Then process specific skills name with (xxxx) inside
let re = /(.*) +\((.*)\)/i; let re = /(.*) +\((.*)\)/i;
let res = re.exec( compName ); let res = re.exec(compName);
compName = res[1].trim(); // Get the root skill name 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 if (compNameFR.name != compName) { // Translation OK
newName = compNameFR.name + special; newName = compNameFR.name + special;
} }
if ( !newName || newName == "" || newName == undefined ) { // If no translation, keep the original name // 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 newName = baseName; // If no translation, keep the original name
} }
newList.push(newName); newList.push(newName);
@@ -30,27 +44,30 @@ export class WH4FRPatchConfig {
} }
/************************************************************************************/ /************************************************************************************/
static translateTalentList( talentList) { static translateTalentList(talentList) {
if (!talentList || talentList.length == 0) {
return talentList;
}
let compendiumName = 'wfrp4e-core.items' let compendiumName = 'wfrp4e-core.items'
let newList = []; let newList = [];
for( let talentLine of talentList) { for (let talentLine of talentList) {
let special = ""; let special = "";
let newName = talentLine; let newName = talentLine;
if ( isNaN(talentLine) ) { if (isNaN(talentLine)) {
let subList = talentLine.split(','); let subList = talentLine.split(',');
let newSubList = []; let newSubList = [];
for (let talentName of subList ) { for (let talentName of subList) {
talentName = talentName.trim(); talentName = talentName.trim();
let newName2 = talentName; let newName2 = talentName;
if ( talentName.includes("(") && talentName.includes(")") ) { // Then process specific skills name with (xxxx) inside if (talentName.includes("(") && talentName.includes(")")) { // Then process specific skills name with (xxxx) inside
let re = /(.*) +\((.*)\)/i; let re = /(.*) +\((.*)\)/i;
let res = re.exec( talentName ); let res = re.exec(talentName);
talentName = res[1].trim(); // Get the root skill name 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 if (talentNameFR.name != talentName) { // Translation OK
newName2 = talentNameFR.name + special; newName2 = talentNameFR.name + special;
} }
@@ -64,15 +81,18 @@ export class WH4FRPatchConfig {
} }
/************************************************************************************/ /************************************************************************************/
static patch_subspecies( ) { static patch_subspecies() {
for ( let speciesName in game.wfrp4e.config.subspecies) { if (!game.wfrp4e?.config?.subspecies) {
return
}
for (let speciesName in game.wfrp4e.config.subspecies) {
let subspeciesList = game.wfrp4e.config.subspecies[speciesName]; let subspeciesList = game.wfrp4e.config.subspecies[speciesName];
for ( let subspeciesName in subspeciesList) { for (let subspeciesName in subspeciesList) {
let subspecies = subspeciesList[subspeciesName]; let subspecies = subspeciesList[subspeciesName];
if ( subspecies.skills) { if (subspecies.skills) {
subspecies.skills = this.translateSkillList(subspecies.skills); subspecies.skills = this.translateSkillList(subspecies.skills);
} }
if ( subspecies.talents) { if (subspecies.talents) {
subspecies.talents = this.translateTalentList(subspecies.talents); subspecies.talents = this.translateTalentList(subspecies.talents);
} }
} }
@@ -80,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...."); console.log("Patching species skills....");
for (let speciesName in game.wfrp4e.config.speciesSkills) { for (let speciesName in game.wfrp4e.config.speciesSkills) {
let speciesComp = game.wfrp4e.config.speciesSkills[speciesName]; let speciesComp = game.wfrp4e.config.speciesSkills[speciesName];
console.log("SpeciesName", speciesName, speciesComp); 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) { for (let speciesName in game.wfrp4e.config.speciesTalents) {
let speciesTalents = game.wfrp4e.config.speciesTalents[speciesName]; let speciesTalents = game.wfrp4e.config.speciesTalents[speciesName];
game.wfrp4e.config.speciesTalents[speciesName] = this.translateTalentList( speciesTalents); game.wfrp4e.config.speciesTalents[speciesName] = this.translateTalentList(speciesTalents);
} }
} }
@@ -101,16 +127,16 @@ export class WH4FRPatchConfig {
static patch_career() { static patch_career() {
let compendiumName = 'wfrp4e-core.items' let compendiumName = 'wfrp4e-core.items'
if ( game.wfrp4e.tables.career) { if (game.wfrp4e?.tables?.career) {
for( let row of game.wfrp4e.tables.career.rows) { for (let row of game.wfrp4e.tables.career.rows) {
for ( let key in row) { for (let key in row) {
if (key != "range") { if (key != "range") {
if ( row[key].name == 'Slayer' ) { if (row[key].name == 'Slayer') {
row[key].name = "Tueur Nains"; row[key].name = "Tueur Nains";
} else if ( row[key].name == 'Duelist' ) { } else if (row[key].name == 'Duelist') {
row[key].name = "Duelliste"; row[key].name = "Duelliste";
} else { } 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; row[key].name = career_fr.name;
} }
} }
@@ -122,12 +148,15 @@ export class WH4FRPatchConfig {
/************************************************************************************/ /************************************************************************************/
static fixSpeciesTable() { static fixSpeciesTable() {
let speciesTable = game.wfrp4e.tables.findTable("species"); let speciesTable = game.wfrp4e?.tables?.findTable("species");
if (!speciesTable?.results) {
return
}
let newResults = foundry.utils.duplicate(speciesTable.results); let newResults = foundry.utils.duplicate(speciesTable.results);
for (let result of newResults) { for (let result of newResults) {
result.name = game.i18n.localize(result.name); result.name = game.i18n.localize(result.name);
} }
speciesTable.update({results: newResults } ) speciesTable.update({ results: newResults })
console.log("Species table patched to use 'Humain' instead of 'Human'", speciesTable); console.log("Species table patched to use 'Humain' instead of 'Human'", speciesTable);
} }
@@ -143,7 +172,7 @@ export class WH4FRPatchConfig {
} }
// Detect and patch as necessary // Detect and patch as necessary
if (game.wfrp4e.config?.talentBonuses ) { if (game.wfrp4e.config?.talentBonuses) {
this.fixSpeciesTable() // Force 'name' field replacement this.fixSpeciesTable() // Force 'name' field replacement
@@ -185,22 +214,22 @@ export class WH4FRPatchConfig {
if (game.wfrp4e.config.characteristicsBonus) { if (game.wfrp4e.config.characteristicsBonus) {
game.wfrp4e.config.characteristicsBonus = game.wfrp4e.config.characteristicsBonus =
{ {
"ws": "Bonus de Capacité de Combat", "ws": "Bonus de Capacité de Combat",
"bs": "Bonus de Capacité de Tir", "bs": "Bonus de Capacité de Tir",
"s": "Bonus de Force", "s": "Bonus de Force",
"t": "Bonus d'Endurance", "t": "Bonus d'Endurance",
"i": "Bonus d'Initiative", "i": "Bonus d'Initiative",
"ag": "Bonus d'Agilité", "ag": "Bonus d'Agilité",
"dex": "Bonus de Dexterité", "dex": "Bonus de Dexterité",
"int": "Bonus d'Intelligence", "int": "Bonus d'Intelligence",
"wp": "Bonus de Force Mentale", "wp": "Bonus de Force Mentale",
"fel": "Bonus de Sociabilité" "fel": "Bonus de Sociabilité"
} }
} }
if (game.wfrp4e.config.classTrappings) { 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]; game.wfrp4e.config.classTrappings[game.i18n.localize(c)] = game.wfrp4e.config.classTrappings[c];
} }
} }

410
modules/inn/InnRoller.js Normal file
View 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
View 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
View 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

View 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;
}
}

View 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]
});

View 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')");

View 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';

View 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
View 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");

File diff suppressed because it is too large Load Diff

View 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é");
}

View File

@@ -1 +1 @@
MANIFEST-001162 MANIFEST-001279

View File

@@ -1,7 +1,7 @@
2025/07/25-10:34:43.762543 7fbfde7fc6c0 Recovering log #1160 2026/01/29-10:58:42.937989 7fac277fe6c0 Recovering log #1277
2025/07/25-10:34:43.773316 7fbfde7fc6c0 Delete type=3 #1158 2026/01/29-10:58:42.947978 7fac277fe6c0 Delete type=3 #1275
2025/07/25-10:34:43.773472 7fbfde7fc6c0 Delete type=0 #1160 2026/01/29-10:58:42.948038 7fac277fe6c0 Delete type=0 #1277
2025/07/25-10:51:25.651385 7fbd3ffff6c0 Level-0 table #1165: started 2026/01/29-11:43:15.082893 7fa9a6fef6c0 Level-0 table #1282: started
2025/07/25-10:51:25.651416 7fbd3ffff6c0 Level-0 table #1165: 0 bytes OK 2026/01/29-11:43:15.082936 7fa9a6fef6c0 Level-0 table #1282: 0 bytes OK
2025/07/25-10:51:25.658484 7fbd3ffff6c0 Delete type=0 #1163 2026/01/29-11:43:15.089469 7fa9a6fef6c0 Delete type=0 #1280
2025/07/25-10:51:25.658718 7fbd3ffff6c0 Manual compaction at level-0 from '!journal!3IgmiprzLB6Lwenc' @ 72057594037927935 : 1 .. '!journal.pages!suuYN87Al1ZZWtQQ.jhgNnhWhrkOpKs1B' @ 0 : 0; will stop at (end) 2026/01/29-11:43:15.099933 7fa9a6fef6c0 Manual compaction at level-0 from '!journal!3IgmiprzLB6Lwenc' @ 72057594037927935 : 1 .. '!journal.pages!suuYN87Al1ZZWtQQ.jhgNnhWhrkOpKs1B' @ 0 : 0; will stop at (end)

View File

@@ -1,7 +1,7 @@
2025/07/25-10:29:27.346281 7fbfddffb6c0 Recovering log #1156 2026/01/29-10:52:14.344493 7fac277fe6c0 Recovering log #1273
2025/07/25-10:29:27.356344 7fbfddffb6c0 Delete type=3 #1154 2026/01/29-10:52:14.354354 7fac277fe6c0 Delete type=3 #1271
2025/07/25-10:29:27.356416 7fbfddffb6c0 Delete type=0 #1156 2026/01/29-10:52:14.354436 7fac277fe6c0 Delete type=0 #1273
2025/07/25-10:29:34.032226 7fbd3ffff6c0 Level-0 table #1161: started 2026/01/29-10:55:44.810919 7fa9a6fef6c0 Level-0 table #1278: started
2025/07/25-10:29:34.032249 7fbd3ffff6c0 Level-0 table #1161: 0 bytes OK 2026/01/29-10:55:44.810962 7fa9a6fef6c0 Level-0 table #1278: 0 bytes OK
2025/07/25-10:29:34.038701 7fbd3ffff6c0 Delete type=0 #1159 2026/01/29-10:55:44.820672 7fa9a6fef6c0 Delete type=0 #1276
2025/07/25-10:29:34.039163 7fbd3ffff6c0 Manual compaction at level-0 from '!journal!3IgmiprzLB6Lwenc' @ 72057594037927935 : 1 .. '!journal.pages!suuYN87Al1ZZWtQQ.jhgNnhWhrkOpKs1B' @ 0 : 0; will stop at (end) 2026/01/29-10:55:44.820811 7fa9a6fef6c0 Manual compaction at level-0 from '!journal!3IgmiprzLB6Lwenc' @ 72057594037927935 : 1 .. '!journal.pages!suuYN87Al1ZZWtQQ.jhgNnhWhrkOpKs1B' @ 0 : 0; will stop at (end)

Binary file not shown.

View File

@@ -1 +1 @@
MANIFEST-001164 MANIFEST-001281

View File

@@ -1,7 +1,7 @@
2025/07/25-10:34:43.777030 7fbfddffb6c0 Recovering log #1162 2026/01/29-10:58:42.950367 7fac277fe6c0 Recovering log #1279
2025/07/25-10:34:43.787316 7fbfddffb6c0 Delete type=3 #1160 2026/01/29-10:58:42.959516 7fac277fe6c0 Delete type=3 #1277
2025/07/25-10:34:43.787393 7fbfddffb6c0 Delete type=0 #1162 2026/01/29-10:58:42.959596 7fac277fe6c0 Delete type=0 #1279
2025/07/25-10:51:25.614990 7fbd3ffff6c0 Level-0 table #1167: started 2026/01/29-11:43:14.910167 7fa9a6fef6c0 Level-0 table #1284: started
2025/07/25-10:51:25.615048 7fbd3ffff6c0 Level-0 table #1167: 0 bytes OK 2026/01/29-11:43:14.910204 7fa9a6fef6c0 Level-0 table #1284: 0 bytes OK
2025/07/25-10:51:25.621619 7fbd3ffff6c0 Delete type=0 #1165 2026/01/29-11:43:14.916676 7fa9a6fef6c0 Delete type=0 #1282
2025/07/25-10:51:25.629942 7fbd3ffff6c0 Manual compaction at level-0 from '!folders!3uquYH73ttCdoH0I' @ 72057594037927935 : 1 .. '!items!ylFhk7mGZOnAJTUT' @ 0 : 0; will stop at (end) 2026/01/29-11:43:14.924493 7fa9a6fef6c0 Manual compaction at level-0 from '!folders!3uquYH73ttCdoH0I' @ 72057594037927935 : 1 .. '!items!ylFhk7mGZOnAJTUT' @ 0 : 0; will stop at (end)

View File

@@ -1,7 +1,7 @@
2025/07/25-10:29:27.359068 7fbfde7fc6c0 Recovering log #1158 2026/01/29-10:52:14.356741 7fac27fff6c0 Recovering log #1275
2025/07/25-10:29:27.369855 7fbfde7fc6c0 Delete type=3 #1156 2026/01/29-10:52:14.366627 7fac27fff6c0 Delete type=3 #1273
2025/07/25-10:29:27.369961 7fbfde7fc6c0 Delete type=0 #1158 2026/01/29-10:52:14.366686 7fac27fff6c0 Delete type=0 #1275
2025/07/25-10:29:34.039250 7fbd3ffff6c0 Level-0 table #1163: started 2026/01/29-10:55:44.821680 7fa9a6fef6c0 Level-0 table #1280: started
2025/07/25-10:29:34.039321 7fbd3ffff6c0 Level-0 table #1163: 0 bytes OK 2026/01/29-10:55:44.821714 7fa9a6fef6c0 Level-0 table #1280: 0 bytes OK
2025/07/25-10:29:34.046345 7fbd3ffff6c0 Delete type=0 #1161 2026/01/29-10:55:44.835173 7fa9a6fef6c0 Delete type=0 #1278
2025/07/25-10:29:34.067660 7fbd3ffff6c0 Manual compaction at level-0 from '!folders!3uquYH73ttCdoH0I' @ 72057594037927935 : 1 .. '!items!ylFhk7mGZOnAJTUT' @ 0 : 0; will stop at (end) 2026/01/29-10:55:44.835325 7fa9a6fef6c0 Manual compaction at level-0 from '!folders!3uquYH73ttCdoH0I' @ 72057594037927935 : 1 .. '!items!ylFhk7mGZOnAJTUT' @ 0 : 0; will stop at (end)

Binary file not shown.

View File

@@ -1 +1 @@
MANIFEST-001162 MANIFEST-001279

View File

@@ -1,7 +1,7 @@
2025/07/25-10:34:43.804048 7fbfdeffd6c0 Recovering log #1160 2026/01/29-10:58:42.977033 7fac3cbfe6c0 Recovering log #1277
2025/07/25-10:34:43.813871 7fbfdeffd6c0 Delete type=3 #1158 2026/01/29-10:58:42.986296 7fac3cbfe6c0 Delete type=3 #1275
2025/07/25-10:34:43.813960 7fbfdeffd6c0 Delete type=0 #1160 2026/01/29-10:58:42.986342 7fac3cbfe6c0 Delete type=0 #1277
2025/07/25-10:51:25.644344 7fbd3ffff6c0 Level-0 table #1165: started 2026/01/29-11:43:14.924604 7fa9a6fef6c0 Level-0 table #1282: started
2025/07/25-10:51:25.644427 7fbd3ffff6c0 Level-0 table #1165: 0 bytes OK 2026/01/29-11:43:14.924660 7fa9a6fef6c0 Level-0 table #1282: 0 bytes OK
2025/07/25-10:51:25.651247 7fbd3ffff6c0 Delete type=0 #1163 2026/01/29-11:43:14.930959 7fa9a6fef6c0 Delete type=0 #1280
2025/07/25-10:51:25.658708 7fbd3ffff6c0 Manual compaction at level-0 from '!journal!cZtNgayIw2QFhC9u' @ 72057594037927935 : 1 .. '!journal.pages!cZtNgayIw2QFhC9u.ts265H1XkisLgdow' @ 0 : 0; will stop at (end) 2026/01/29-11:43:14.954971 7fa9a6fef6c0 Manual compaction at level-0 from '!journal!cZtNgayIw2QFhC9u' @ 72057594037927935 : 1 .. '!journal.pages!cZtNgayIw2QFhC9u.ts265H1XkisLgdow' @ 0 : 0; will stop at (end)

View File

@@ -1,7 +1,7 @@
2025/07/25-10:29:27.386763 7fbfde7fc6c0 Recovering log #1156 2026/01/29-10:52:14.382212 7fac27fff6c0 Recovering log #1273
2025/07/25-10:29:27.396435 7fbfde7fc6c0 Delete type=3 #1154 2026/01/29-10:52:14.391540 7fac27fff6c0 Delete type=3 #1271
2025/07/25-10:29:27.396509 7fbfde7fc6c0 Delete type=0 #1156 2026/01/29-10:52:14.391602 7fac27fff6c0 Delete type=0 #1273
2025/07/25-10:29:34.053947 7fbd3ffff6c0 Level-0 table #1161: started 2026/01/29-10:55:44.848192 7fa9a6fef6c0 Level-0 table #1278: started
2025/07/25-10:29:34.053983 7fbd3ffff6c0 Level-0 table #1161: 0 bytes OK 2026/01/29-10:55:44.848229 7fa9a6fef6c0 Level-0 table #1278: 0 bytes OK
2025/07/25-10:29:34.060105 7fbd3ffff6c0 Delete type=0 #1159 2026/01/29-10:55:44.858087 7fa9a6fef6c0 Delete type=0 #1276
2025/07/25-10:29:34.067683 7fbd3ffff6c0 Manual compaction at level-0 from '!journal!cZtNgayIw2QFhC9u' @ 72057594037927935 : 1 .. '!journal.pages!cZtNgayIw2QFhC9u.ts265H1XkisLgdow' @ 0 : 0; will stop at (end) 2026/01/29-10:55:44.858262 7fa9a6fef6c0 Manual compaction at level-0 from '!journal!cZtNgayIw2QFhC9u' @ 72057594037927935 : 1 .. '!journal.pages!cZtNgayIw2QFhC9u.ts265H1XkisLgdow' @ 0 : 0; will stop at (end)

Binary file not shown.

Binary file not shown.

View File

View File

@@ -1 +1 @@
MANIFEST-001162 MANIFEST-001279

View File

@@ -1,7 +1,7 @@
2025/07/25-10:34:43.747985 7fbfdd7fa6c0 Recovering log #1160 2026/01/29-10:58:42.926104 7fac277fe6c0 Recovering log #1277
2025/07/25-10:34:43.759279 7fbfdd7fa6c0 Delete type=3 #1158 2026/01/29-10:58:42.935779 7fac277fe6c0 Delete type=3 #1275
2025/07/25-10:34:43.759366 7fbfdd7fa6c0 Delete type=0 #1160 2026/01/29-10:58:42.935842 7fac277fe6c0 Delete type=0 #1277
2025/07/25-10:51:25.621866 7fbd3ffff6c0 Level-0 table #1165: started 2026/01/29-11:43:14.916846 7fa9a6fef6c0 Level-0 table #1282: started
2025/07/25-10:51:25.621939 7fbd3ffff6c0 Level-0 table #1165: 0 bytes OK 2026/01/29-11:43:14.916889 7fa9a6fef6c0 Level-0 table #1282: 0 bytes OK
2025/07/25-10:51:25.629742 7fbd3ffff6c0 Delete type=0 #1163 2026/01/29-11:43:14.924373 7fa9a6fef6c0 Delete type=0 #1280
2025/07/25-10:51:25.629955 7fbd3ffff6c0 Manual compaction at level-0 from '!journal!50u8VAjdmovyr0hx' @ 72057594037927935 : 1 .. '!journal.pages!yzw9I0r3hCK7PJnz.sPNCYj2nR3Cp3jHd' @ 0 : 0; will stop at (end) 2026/01/29-11:43:14.924501 7fa9a6fef6c0 Manual compaction at level-0 from '!journal!50u8VAjdmovyr0hx' @ 72057594037927935 : 1 .. '!journal.pages!yzw9I0r3hCK7PJnz.sPNCYj2nR3Cp3jHd' @ 0 : 0; will stop at (end)

View File

@@ -1,7 +1,7 @@
2025/07/25-10:29:27.331370 7fbfdeffd6c0 Recovering log #1156 2026/01/29-10:52:14.331605 7fac3d3ff6c0 Recovering log #1273
2025/07/25-10:29:27.342846 7fbfdeffd6c0 Delete type=3 #1154 2026/01/29-10:52:14.341908 7fac3d3ff6c0 Delete type=3 #1271
2025/07/25-10:29:27.342999 7fbfdeffd6c0 Delete type=0 #1156 2026/01/29-10:52:14.341974 7fac3d3ff6c0 Delete type=0 #1273
2025/07/25-10:29:34.025148 7fbd3ffff6c0 Level-0 table #1161: started 2026/01/29-10:55:44.799059 7fa9a6fef6c0 Level-0 table #1278: started
2025/07/25-10:29:34.025180 7fbd3ffff6c0 Level-0 table #1161: 0 bytes OK 2026/01/29-10:55:44.799092 7fa9a6fef6c0 Level-0 table #1278: 0 bytes OK
2025/07/25-10:29:34.032076 7fbd3ffff6c0 Delete type=0 #1159 2026/01/29-10:55:44.809892 7fa9a6fef6c0 Delete type=0 #1276
2025/07/25-10:29:34.039142 7fbd3ffff6c0 Manual compaction at level-0 from '!journal!50u8VAjdmovyr0hx' @ 72057594037927935 : 1 .. '!journal.pages!yzw9I0r3hCK7PJnz.sPNCYj2nR3Cp3jHd' @ 0 : 0; will stop at (end) 2026/01/29-10:55:44.810048 7fa9a6fef6c0 Manual compaction at level-0 from '!journal!50u8VAjdmovyr0hx' @ 72057594037927935 : 1 .. '!journal.pages!yzw9I0r3hCK7PJnz.sPNCYj2nR3Cp3jHd' @ 0 : 0; will stop at (end)

Binary file not shown.

Binary file not shown.

View File

View File

View File

@@ -1 +1 @@
MANIFEST-001162 MANIFEST-001279

View File

@@ -1,7 +1,7 @@
2025/07/25-10:34:43.733425 7fbfddffb6c0 Recovering log #1160 2026/01/29-10:58:42.911172 7fac3cbfe6c0 Recovering log #1277
2025/07/25-10:34:43.743802 7fbfddffb6c0 Delete type=3 #1158 2026/01/29-10:58:42.922025 7fac3cbfe6c0 Delete type=3 #1275
2025/07/25-10:34:43.743868 7fbfddffb6c0 Delete type=0 #1160 2026/01/29-10:58:42.922081 7fac3cbfe6c0 Delete type=0 #1277
2025/07/25-10:51:25.607883 7fbd3ffff6c0 Level-0 table #1165: started 2026/01/29-11:43:14.903068 7fa9a6fef6c0 Level-0 table #1282: started
2025/07/25-10:51:25.607927 7fbd3ffff6c0 Level-0 table #1165: 0 bytes OK 2026/01/29-11:43:14.903107 7fa9a6fef6c0 Level-0 table #1282: 0 bytes OK
2025/07/25-10:51:25.614720 7fbd3ffff6c0 Delete type=0 #1163 2026/01/29-11:43:14.910024 7fa9a6fef6c0 Delete type=0 #1280
2025/07/25-10:51:25.629926 7fbd3ffff6c0 Manual compaction at level-0 from '!tables!4l60Lxv8cpsyy2Cg' @ 72057594037927935 : 1 .. '!tables.results!tfaYKDZqu7kgZvRG.yvbwKursaixh2dby' @ 0 : 0; will stop at (end) 2026/01/29-11:43:14.924484 7fa9a6fef6c0 Manual compaction at level-0 from '!tables!4l60Lxv8cpsyy2Cg' @ 72057594037927935 : 1 .. '!tables.results!tfaYKDZqu7kgZvRG.yvbwKursaixh2dby' @ 0 : 0; will stop at (end)

View File

@@ -1,7 +1,7 @@
2025/07/25-10:29:27.316812 7fbfdd7fa6c0 Recovering log #1156 2026/01/29-10:52:14.318846 7fac277fe6c0 Recovering log #1273
2025/07/25-10:29:27.327498 7fbfdd7fa6c0 Delete type=3 #1154 2026/01/29-10:52:14.328479 7fac277fe6c0 Delete type=3 #1271
2025/07/25-10:29:27.327589 7fbfdd7fa6c0 Delete type=0 #1156 2026/01/29-10:52:14.328545 7fac277fe6c0 Delete type=0 #1273
2025/07/25-10:29:34.011374 7fbd3ffff6c0 Level-0 table #1161: started 2026/01/29-10:55:44.787338 7fa9a6fef6c0 Level-0 table #1278: started
2025/07/25-10:29:34.011417 7fbd3ffff6c0 Level-0 table #1161: 0 bytes OK 2026/01/29-10:55:44.787395 7fa9a6fef6c0 Level-0 table #1278: 0 bytes OK
2025/07/25-10:29:34.018376 7fbd3ffff6c0 Delete type=0 #1159 2026/01/29-10:55:44.798087 7fa9a6fef6c0 Delete type=0 #1276
2025/07/25-10:29:34.039078 7fbd3ffff6c0 Manual compaction at level-0 from '!tables!4l60Lxv8cpsyy2Cg' @ 72057594037927935 : 1 .. '!tables.results!tfaYKDZqu7kgZvRG.yvbwKursaixh2dby' @ 0 : 0; will stop at (end) 2026/01/29-10:55:44.798228 7fa9a6fef6c0 Manual compaction at level-0 from '!tables!4l60Lxv8cpsyy2Cg' @ 72057594037927935 : 1 .. '!tables.results!tfaYKDZqu7kgZvRG.yvbwKursaixh2dby' @ 0 : 0; will stop at (end)

Binary file not shown.

View File

View File

View File

@@ -1 +1 @@
MANIFEST-000805 MANIFEST-000922

View File

@@ -1,7 +1,7 @@
2025/07/25-10:34:43.791108 7fbfde7fc6c0 Recovering log #803 2026/01/29-10:58:42.964451 7fac3cbfe6c0 Recovering log #920
2025/07/25-10:34:43.801505 7fbfde7fc6c0 Delete type=3 #801 2026/01/29-10:58:42.974523 7fac3cbfe6c0 Delete type=3 #918
2025/07/25-10:34:43.801580 7fbfde7fc6c0 Delete type=0 #803 2026/01/29-10:58:42.974583 7fac3cbfe6c0 Delete type=0 #920
2025/07/25-10:51:25.630138 7fbd3ffff6c0 Level-0 table #808: started 2026/01/29-11:43:14.948296 7fa9a6fef6c0 Level-0 table #925: started
2025/07/25-10:51:25.630167 7fbd3ffff6c0 Level-0 table #808: 0 bytes OK 2026/01/29-11:43:14.948359 7fa9a6fef6c0 Level-0 table #925: 0 bytes OK
2025/07/25-10:51:25.636435 7fbd3ffff6c0 Delete type=0 #806 2026/01/29-11:43:14.954859 7fa9a6fef6c0 Delete type=0 #923
2025/07/25-10:51:25.658669 7fbd3ffff6c0 Manual compaction at level-0 from '!journal!056ILNNrLiPq3Gi3' @ 72057594037927935 : 1 .. '!journal.pages!yfZxl4I7XAuUF6r3.apXmOlZRmGT4GreB' @ 0 : 0; will stop at (end) 2026/01/29-11:43:14.955000 7fa9a6fef6c0 Manual compaction at level-0 from '!journal!056ILNNrLiPq3Gi3' @ 72057594037927935 : 1 .. '!journal.pages!yfZxl4I7XAuUF6r3.apXmOlZRmGT4GreB' @ 0 : 0; will stop at (end)

View File

@@ -1,7 +1,7 @@
2025/07/25-10:29:27.372941 7fbfddffb6c0 Recovering log #799 2026/01/29-10:52:14.369127 7fac277fe6c0 Recovering log #916
2025/07/25-10:29:27.384085 7fbfddffb6c0 Delete type=3 #797 2026/01/29-10:52:14.379523 7fac277fe6c0 Delete type=3 #914
2025/07/25-10:29:27.384213 7fbfddffb6c0 Delete type=0 #799 2026/01/29-10:52:14.379589 7fac277fe6c0 Delete type=0 #916
2025/07/25-10:29:34.060239 7fbd3ffff6c0 Level-0 table #804: started 2026/01/29-10:55:44.836202 7fa9a6fef6c0 Level-0 table #921: started
2025/07/25-10:29:34.060267 7fbd3ffff6c0 Level-0 table #804: 0 bytes OK 2026/01/29-10:55:44.836244 7fa9a6fef6c0 Level-0 table #921: 0 bytes OK
2025/07/25-10:29:34.067502 7fbd3ffff6c0 Delete type=0 #802 2026/01/29-10:55:44.847287 7fa9a6fef6c0 Delete type=0 #919
2025/07/25-10:29:34.067715 7fbd3ffff6c0 Manual compaction at level-0 from '!journal!056ILNNrLiPq3Gi3' @ 72057594037927935 : 1 .. '!journal.pages!yfZxl4I7XAuUF6r3.apXmOlZRmGT4GreB' @ 0 : 0; will stop at (end) 2026/01/29-10:55:44.847494 7fa9a6fef6c0 Manual compaction at level-0 from '!journal!056ILNNrLiPq3Gi3' @ 72057594037927935 : 1 .. '!journal.pages!yfZxl4I7XAuUF6r3.apXmOlZRmGT4GreB' @ 0 : 0; will stop at (end)

Binary file not shown.

View File

@@ -8,3 +8,313 @@
align-self: center; align-self: center;
text-align: 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);
}

View File

@@ -1,2 +0,0 @@
let item = await fromUuid("Compendium.wfrp4e-core.items.weczkAMPlTjX7lqU")
this.actor.createEmbeddedDocuments("Item", [item])

View File

@@ -1 +0,0 @@
return args.item?.system?.isRanged && args.data.targets[0]?.actor?.sizeNum < 3

View File

@@ -1,25 +0,0 @@
// The imbiber immediately
// 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,
await this.actor.modifyWounds(this.actor.system.characteristics.t.bonus)
// and acquires the Regenerate Creature Trait.
const hasRegenerate = this.actor.has("Regenerate")
if (hasRegenerate === 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> has
<ul>
<li>gained 3 Poisoned Conditions that cannot be resisted at first</li>
<li>recovered ${this.actor.system.characteristics.t.bonus} Wounds</li>
<li>acquired the Regenerate Creature Trait.</li>
</ul>
Its up to Ranald if their regenerating can outpace their poisoning.</p>
<p>When all Poisoned Conditions are lost, so too is Regenerate.</p>`,
{ whisper: ChatMessage.getWhisperRecipients("GM"), blind: true })

View File

@@ -1 +0,0 @@
this.actor.addCondition("blinded", 3)

View File

@@ -1,4 +0,0 @@
let item = await fromUuid("Compendium.wfrp4e-core.items.8piWcBKFlQ2J1E3A")
let data = item.toObject();
data.system.location.key= this.item.system.location.key
this.actor.createEmbeddedDocuments("Item", [data])

View File

@@ -1,5 +0,0 @@
if (!args.flags.quietenedApplied)
{
args.fields.modifier += 10;
args.flags.quietenedApplied = true
}

View File

@@ -1 +0,0 @@
return !args.options.terror && !args.extendedTest?.flags.wfrp4e?.fear

View File

@@ -1 +0,0 @@
return !args.skill?.name.includes(game.i18n.localize("NAME.Row")) && !args.skill?.name.includes(game.i18n.localize("NAME.Sail"));

View File

@@ -1,22 +0,0 @@
let spells = await warhammer.utility.findAllItems("spell", "Loading Spells")
let text = (await game.wfrp4e.tables.rollTable("random-caster", {hideDSN: true})).result
lore = Array.from(text.matchAll(/{(.+?)}/gm))[0][1]
if (text == "GM's Choice")
{
return this.script.scriptNotification(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])
}
else
{
ui.notifications.notify(`Could not find ${lore} spell. Try Again`)
}

View File

@@ -1,6 +0,0 @@
let item = await fromUuid("Compendium.wfrp4e-core.items.4CMKeDTDrRQZbPIJ")
let fixation = (await game.wfrp4e.tables.rollTable("fixations"))
let data = item.toObject();
data.system.specification.value = fixation.result;
this.item.updateSource({name : this.item.name += ` (${fixation.result})`});
this.actor.createEmbeddedDocuments("Item", [data], {fromEffect : this.effect.id})

View File

@@ -1 +0,0 @@
return !["t", "wp"].includes(args.characteristic)

View File

@@ -1,18 +0,0 @@
let table = game.wfrp4e.tables.findTable("mutatephys");
if (!table)
{
return ui.notifications.error("Mutation table not found, please ensure a table with the `mutatephys` key is imported in the world.")
}
let result = (await table.roll()).results[0];
let uuid = `Compendium.${result.documentCollection}.${result.documentId}`
let item = await fromUuid(uuid);
if (item)
{
this.script.scriptNotification(`${item.name} added`)
this.actor.createEmbeddedDocuments("Item", [item])
}
else
{
ui.notifications.error("Item could not be found: " + uuid)
}

View File

@@ -1,21 +0,0 @@
let location = this.item.system.location.key;
if (location)
{
let dropped = this.item.system.weaponsAtLocation;
if (dropped.length)
{
this.script.scriptNotification(`Dropped ${dropped.map(i => i.name).join(", ")}!`)
for(let weapon of dropped)
{
await weapon.system.toggleEquip();
}
}
}
let roll = await new Roll("max(1, 1d10 - @system.characteristics.t.bonus)", this.actor).roll()
roll.toMessage(this.script.getChatData({flavor : `${this.effect.name} (Duration)`}));
this.effect.updateSource({"duration.rounds" : roll.total})

View File

@@ -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
}

View File

@@ -1,6 +0,0 @@
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 Metal (" + args.modifiers.ap.metal + ")");
args.modifiers.ap.metal = 0
}

View File

@@ -1,3 +0,0 @@
let item = await fromUuid("Compendium.wfrp4e-core.items.GbDyBCu8ZjDp6dkj")
let data = item.toObject();
this.actor.createEmbeddedDocuments("Item", [data], {fromEffect : this.effect.id})

View File

@@ -1,10 +0,0 @@
let item1 = await fromUuid("Compendium.wfrp4e-core.items.3S4OYOZLauXctmev")
let item2 = await fromUuid("Compendium.wfrp4e-core.items.7mCcI3q7hgWcmbBU")
let data1 = item1.toObject();
data1.system.location.key = this.item.system.location.key
let data2 = item2.toObject();
data2.system.location.key = this.item.system.location.key
this.actor.createEmbeddedDocuments("Item", [data1, data2], {fromEffect: this.effect.id})

View File

@@ -1 +0,0 @@
args.fields.modifier -= 20;

View File

@@ -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")
}
})

View File

@@ -1,7 +0,0 @@
if (!args.flags.strikeToStun)
{
args.flags.strikeToStun = true
args.fields.modifier += 20;
args.fields.hitLocation = "head";
}
args.fields.successBonus++;

View File

@@ -1 +0,0 @@
return args.options.terror || args.extendedTest?.flags.wfrp4e?.fear

View File

@@ -1,6 +0,0 @@
let type = this.item.getFlag("wfrp4e", "breath");
if (["fire", "electricity", "poison"].includes(type))
{
args.applyAP = false;
}

View File

@@ -1 +0,0 @@
args.fields.modifier -= 20

View File

@@ -1,7 +0,0 @@
let state = !this.effect.disabled;
this.effect.update({"disabled": state});
if (state)
return ui.notifications.info("EFFECT.CreatureBackInWater", {localize: true})
return ui.notifications.info("EFFECT.CreatureOutOfWater", {localize: true});

View File

@@ -1,31 +0,0 @@
if (!this.item.name.includes("(") || this.item.system.tests.value.includes("Terrain"))
{
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("("))
{
let terrain = name.split("(")[1].split(")")[0]
tests = tests.replace("the Terrain", terrain)
}
else // If no sense specified, provide dialog choice
{
let choice = await ItemDialog.create(ItemDialog.objectToArray({
coastal : "Coastal",
deserts : "Deserts",
marshes : "Marshes",
rocky : "Rocky",
tundra : "Tundra",
woodlands : "Woodlands"
}, this.item.img), 1, "Choose Terrain");
if (choice[0])
{
name = `${name.split("(")[0].trim()} (${choice[0].name})`
tests = tests.replace("the Terrain", choice[0].name + " Terrain")
}
}
this.effect.updateSource({name})
this.item.updateSource({name, "system.tests.value" : tests})
}

View File

@@ -1 +0,0 @@
return !args.skill?.name.includes(game.i18n.localize("NAME.Language"));

View File

@@ -1,40 +0,0 @@
let characteristics = {
"ws" : 5,
"bs" : 5,
"s" : 5,
"t" : 0,
"i" : 5,
"ag" : 5,
"dex" : 5,
"int" : 0,
"wp" : 5,
"fel" : 5
}
let items = []
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)
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)
if (talentItem)
{
items.push(talentItem.toObject());
}
else
{
ui.notifications.warn(`Could not find ${talent}`, {permanent : true})
}
}
await this.actor.update(updateObj)
this.actor.createEmbeddedDocuments("Item", items);

View File

@@ -1 +0,0 @@
return args.characteristic != "t"

View File

@@ -1 +0,0 @@
args.actor.details.move.run += 4

View File

@@ -1,2 +0,0 @@
await this.actor.addCondition("ablaze", 2)
await this.script.scriptMessage(await this.actor.applyBasicDamage(this.effect.sourceTest.result.damage, {suppressMsg: true}))

View File

@@ -1 +0,0 @@
return args.skill?.name.includes(game.i18n.localize("NAME.Channelling")) || args.skill?.name == `${game.i18n.localize("NAME.Language")} (${game.i18n.localize("SPEC.Magick")})`

Some files were not shown because too many files have changed in this diff Show More