Compare commits

...

7 Commits

2741 changed files with 24096 additions and 16179 deletions

View File

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

View File

@@ -8,8 +8,9 @@
}
],
"url": "https://www.uberwald.me/gitea/public/foundryvtt-wh4-lang-fr-fr",
"version": "9.3.3",
"version": "9.3.5",
"esmodules": [
"wh4_fr.js",
"modules/babele-register.js",
"modules/addon-register.js",
"modules/import-stat-2.js",
@@ -119,7 +120,7 @@
}
],
"manifest": "https://www.uberwald.me/gitea/public/foundryvtt-wh4-lang-fr-fr/raw/v10/module.json",
"download": "https://www.uberwald.me/gitea/public/foundryvtt-wh4-lang-fr-fr/archive/foundryvtt-wh4-lang-fr-9-3-2.zip",
"download": "https://www.uberwald.me/gitea/public/foundryvtt-wh4-lang-fr-fr/archive/foundryvtt-wh4-lang-fr-9-3-5.zip",
"id": "wh4-fr-translation",
"compatibility": {
"minimum": "13",

View File

@@ -82,39 +82,8 @@ const _patch_up_in_arms = () => {
}
/************************************************************************************/
/* Manages /auberge command */
const _manage_inn_roll = async (content, msg) => {
// Split input into arguments
let command = content.split(" ").map(function (item) {
return item.trim();
})
console.log("COMMANDES", command);
if (command[0] == "/auberge" && command[1]) {
msg["type"] = 0;
msg["rollMode"] = "gmroll";
let compendium = game.packs.get('wh4-fr-translation.plats-dauberges')
game.packs.get(compendium);
let rollList = await compendium.getDocuments()
for (const element of rollList) {
let rollTab = element;
console.log("Got compendium...", rollList, rollTab.name);
if (rollTab.name.toLowerCase().includes(command[1].toLowerCase())) {
let my_rollTable;
await compendium.getDocument(rollTab._id).then(mytab => my_rollTable = mytab);
my_rollTable.draw({ rollMode: "gmroll" });
return false;
}
}
}
if (content.includes("/auberge")) {
msg["type"] = 0;
msg["rollMode"] = "gmroll";
msg["content"] = "Syntaxe : /auberge MOT_CLE, avec MOT_CLE parmi:<br>BoissonsBase, BoissonsFortes, Desserts, PlatsCommuns, PlatsExcellents, PlatsMaritimes, PlatsMédiocres, PlatsQualité, PlatsRivières<br>Des raccourcis sont possibles avec une partie du nom : /auberge Base (correspond à BoissonBase) ou /auberge Mari (correspond à PlatsMaritimes), etc."
ChatMessage.create(msg);
return false;
}
}
/* Module /auberge géré par modules/inn/inn-init.js */
// L'ancienne implémentation a été migrée vers le module inn pour cohérence avec /voyage
/************************************************************************************/
let __eis_tables = {
@@ -262,17 +231,13 @@ const __check_fix_wrong_modules = (chatFlag, patchFinished) => {
}
} else if (game.user.isGM && patchFinished) {
ChatMessage.create({
content: "<div>Les modules WFRP4E ont été <strong>patchés avec succés</strong>. Vous pouvez y aller et que <strong>Shallya vous garde !</strong></div><div>Derniers changements : Support WFRP4E v8.3.X</div></ul>",
content: "<div>Les modules WFRP4E ont été <strong>patchés avec succés</strong>. Vous pouvez y aller et que <strong>Shallya vous garde !</strong><div><div>Changements v9.3.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,
whisper: ChatMessage.getWhisperRecipients("GM")
});
}
}
const __history = [
"Nouveautés 9.0.0: <ul><li>Support Foundry v13 et diverses petites corrections !</li></ul>"
]
/************************************************************************************/
const convertColumnToMulti = (table) => {
let columns = table.columns;
@@ -366,14 +331,8 @@ const __add_actors_translation = () => {
/************************************************************************************/
/* Hook for specific command */
Hooks.on("chatMessage", (html, content, msg) => {
if (content.toLowerCase().includes('auberge')) {
_manage_inn_roll(content, msg);
return false;
}
});
/* Hook for specific command - Module /auberge migré vers modules/inn/ */
// La commande /auberge est désormais gérée par le module inn-init.js
/************************************************************************************/
/* Additionnal hooks ready */
@@ -426,7 +385,9 @@ Hooks.on('ready', () => {
"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, 20000, true, true);
setTimeout(__add_actors_translation, 21000, false, true);

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-001231
MANIFEST-001259

View File

@@ -1,7 +1,7 @@
2026/01/05-17:20:14.396780 7f93eb7fe6c0 Recovering log #1229
2026/01/05-17:20:14.456467 7f93eb7fe6c0 Delete type=3 #1227
2026/01/05-17:20:14.456537 7f93eb7fe6c0 Delete type=0 #1229
2026/01/05-17:22:12.851160 7f93e9ffb6c0 Level-0 table #1234: started
2026/01/05-17:22:12.851180 7f93e9ffb6c0 Level-0 table #1234: 0 bytes OK
2026/01/05-17:22:12.857087 7f93e9ffb6c0 Delete type=0 #1232
2026/01/05-17:22:12.870416 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!3IgmiprzLB6Lwenc' @ 72057594037927935 : 1 .. '!journal.pages!suuYN87Al1ZZWtQQ.jhgNnhWhrkOpKs1B' @ 0 : 0; will stop at (end)
2026/01/07-15:26:43.527176 7f93eaffd6c0 Recovering log #1257
2026/01/07-15:26:43.536993 7f93eaffd6c0 Delete type=3 #1255
2026/01/07-15:26:43.537037 7f93eaffd6c0 Delete type=0 #1257
2026/01/07-15:53:28.412551 7f93e9ffb6c0 Level-0 table #1262: started
2026/01/07-15:53:28.412580 7f93e9ffb6c0 Level-0 table #1262: 0 bytes OK
2026/01/07-15:53:28.419118 7f93e9ffb6c0 Delete type=0 #1260
2026/01/07-15:53:28.432727 7f93e9ffb6c0 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 @@
2026/01/04-22:01:24.254078 7f93eaffd6c0 Recovering log #1224
2026/01/04-22:01:24.350065 7f93eaffd6c0 Delete type=3 #1222
2026/01/04-22:01:24.350146 7f93eaffd6c0 Delete type=0 #1224
2026/01/04-22:02:13.377643 7f93e9ffb6c0 Level-0 table #1230: started
2026/01/04-22:02:13.377670 7f93e9ffb6c0 Level-0 table #1230: 0 bytes OK
2026/01/04-22:02:13.384805 7f93e9ffb6c0 Delete type=0 #1228
2026/01/04-22:02:13.385131 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!3IgmiprzLB6Lwenc' @ 72057594037927935 : 1 .. '!journal.pages!suuYN87Al1ZZWtQQ.jhgNnhWhrkOpKs1B' @ 0 : 0; will stop at (end)
2026/01/07-15:09:19.276373 7f93eaffd6c0 Recovering log #1253
2026/01/07-15:09:19.291800 7f93eaffd6c0 Delete type=3 #1251
2026/01/07-15:09:19.291847 7f93eaffd6c0 Delete type=0 #1253
2026/01/07-15:13:49.642315 7f93e9ffb6c0 Level-0 table #1258: started
2026/01/07-15:13:49.642358 7f93e9ffb6c0 Level-0 table #1258: 0 bytes OK
2026/01/07-15:13:49.649460 7f93e9ffb6c0 Delete type=0 #1256
2026/01/07-15:13:49.662442 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!3IgmiprzLB6Lwenc' @ 72057594037927935 : 1 .. '!journal.pages!suuYN87Al1ZZWtQQ.jhgNnhWhrkOpKs1B' @ 0 : 0; will stop at (end)

View File

@@ -1 +1 @@
MANIFEST-001233
MANIFEST-001261

View File

@@ -1,7 +1,7 @@
2026/01/05-17:20:14.458755 7f93eb7fe6c0 Recovering log #1231
2026/01/05-17:20:14.498635 7f93eb7fe6c0 Delete type=3 #1229
2026/01/05-17:20:14.498701 7f93eb7fe6c0 Delete type=0 #1231
2026/01/05-17:22:12.845184 7f93e9ffb6c0 Level-0 table #1236: started
2026/01/05-17:22:12.845214 7f93e9ffb6c0 Level-0 table #1236: 0 bytes OK
2026/01/05-17:22:12.851070 7f93e9ffb6c0 Delete type=0 #1234
2026/01/05-17:22:12.870407 7f93e9ffb6c0 Manual compaction at level-0 from '!folders!3uquYH73ttCdoH0I' @ 72057594037927935 : 1 .. '!items!ylFhk7mGZOnAJTUT' @ 0 : 0; will stop at (end)
2026/01/07-15:26:43.539401 7f93ebfff6c0 Recovering log #1259
2026/01/07-15:26:43.549699 7f93ebfff6c0 Delete type=3 #1257
2026/01/07-15:26:43.549758 7f93ebfff6c0 Delete type=0 #1259
2026/01/07-15:53:28.419273 7f93e9ffb6c0 Level-0 table #1264: started
2026/01/07-15:53:28.419304 7f93e9ffb6c0 Level-0 table #1264: 0 bytes OK
2026/01/07-15:53:28.426268 7f93e9ffb6c0 Delete type=0 #1262
2026/01/07-15:53:28.440100 7f93e9ffb6c0 Manual compaction at level-0 from '!folders!3uquYH73ttCdoH0I' @ 72057594037927935 : 1 .. '!items!ylFhk7mGZOnAJTUT' @ 0 : 0; will stop at (end)

View File

@@ -1,7 +1,7 @@
2026/01/04-22:01:24.357771 7f93ebfff6c0 Recovering log #1226
2026/01/04-22:01:24.468440 7f93ebfff6c0 Delete type=3 #1224
2026/01/04-22:01:24.468504 7f93ebfff6c0 Delete type=0 #1226
2026/01/04-22:02:13.391738 7f93e9ffb6c0 Level-0 table #1232: started
2026/01/04-22:02:13.391763 7f93e9ffb6c0 Level-0 table #1232: 0 bytes OK
2026/01/04-22:02:13.397715 7f93e9ffb6c0 Delete type=0 #1230
2026/01/04-22:02:13.410782 7f93e9ffb6c0 Manual compaction at level-0 from '!folders!3uquYH73ttCdoH0I' @ 72057594037927935 : 1 .. '!items!ylFhk7mGZOnAJTUT' @ 0 : 0; will stop at (end)
2026/01/07-15:09:19.293766 7f93ea7fc6c0 Recovering log #1255
2026/01/07-15:09:19.310998 7f93ea7fc6c0 Delete type=3 #1253
2026/01/07-15:09:19.311072 7f93ea7fc6c0 Delete type=0 #1255
2026/01/07-15:13:49.675722 7f93e9ffb6c0 Level-0 table #1260: started
2026/01/07-15:13:49.675750 7f93e9ffb6c0 Level-0 table #1260: 0 bytes OK
2026/01/07-15:13:49.682225 7f93e9ffb6c0 Delete type=0 #1258
2026/01/07-15:13:49.688884 7f93e9ffb6c0 Manual compaction at level-0 from '!folders!3uquYH73ttCdoH0I' @ 72057594037927935 : 1 .. '!items!ylFhk7mGZOnAJTUT' @ 0 : 0; will stop at (end)

View File

@@ -1 +1 @@
MANIFEST-001231
MANIFEST-001259

View File

@@ -1,7 +1,7 @@
2026/01/05-17:20:14.575428 7f93ea7fc6c0 Recovering log #1229
2026/01/05-17:20:14.619638 7f93ea7fc6c0 Delete type=3 #1227
2026/01/05-17:20:14.619716 7f93ea7fc6c0 Delete type=0 #1229
2026/01/05-17:22:12.876750 7f93e9ffb6c0 Level-0 table #1234: started
2026/01/05-17:22:12.876780 7f93e9ffb6c0 Level-0 table #1234: 0 bytes OK
2026/01/05-17:22:12.884154 7f93e9ffb6c0 Delete type=0 #1232
2026/01/05-17:22:12.900968 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!cZtNgayIw2QFhC9u' @ 72057594037927935 : 1 .. '!journal.pages!cZtNgayIw2QFhC9u.ts265H1XkisLgdow' @ 0 : 0; will stop at (end)
2026/01/07-15:26:43.564495 7f93eb7fe6c0 Recovering log #1257
2026/01/07-15:26:43.574446 7f93eb7fe6c0 Delete type=3 #1255
2026/01/07-15:26:43.574512 7f93eb7fe6c0 Delete type=0 #1257
2026/01/07-15:53:28.432738 7f93e9ffb6c0 Level-0 table #1262: started
2026/01/07-15:53:28.432770 7f93e9ffb6c0 Level-0 table #1262: 0 bytes OK
2026/01/07-15:53:28.439930 7f93e9ffb6c0 Delete type=0 #1260
2026/01/07-15:53:28.446625 7f93e9ffb6c0 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 @@
2026/01/04-22:01:24.562817 7f93eb7fe6c0 Recovering log #1224
2026/01/04-22:01:24.662382 7f93eb7fe6c0 Delete type=3 #1222
2026/01/04-22:01:24.662455 7f93eb7fe6c0 Delete type=0 #1224
2026/01/04-22:02:13.397968 7f93e9ffb6c0 Level-0 table #1230: started
2026/01/04-22:02:13.398010 7f93e9ffb6c0 Level-0 table #1230: 0 bytes OK
2026/01/04-22:02:13.404471 7f93e9ffb6c0 Delete type=0 #1228
2026/01/04-22:02:13.410790 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!cZtNgayIw2QFhC9u' @ 72057594037927935 : 1 .. '!journal.pages!cZtNgayIw2QFhC9u.ts265H1XkisLgdow' @ 0 : 0; will stop at (end)
2026/01/07-15:09:19.333002 7f93ea7fc6c0 Recovering log #1253
2026/01/07-15:09:19.348597 7f93ea7fc6c0 Delete type=3 #1251
2026/01/07-15:09:19.348662 7f93ea7fc6c0 Delete type=0 #1253
2026/01/07-15:13:49.682345 7f93e9ffb6c0 Level-0 table #1258: started
2026/01/07-15:13:49.682371 7f93e9ffb6c0 Level-0 table #1258: 0 bytes OK
2026/01/07-15:13:49.688691 7f93e9ffb6c0 Delete type=0 #1256
2026/01/07-15:13:49.688891 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!cZtNgayIw2QFhC9u' @ 72057594037927935 : 1 .. '!journal.pages!cZtNgayIw2QFhC9u.ts265H1XkisLgdow' @ 0 : 0; will stop at (end)

View File

@@ -1 +1 @@
MANIFEST-001231
MANIFEST-001259

View File

@@ -1,7 +1,7 @@
2026/01/05-17:20:14.340612 7f93eb7fe6c0 Recovering log #1229
2026/01/05-17:20:14.394663 7f93eb7fe6c0 Delete type=3 #1227
2026/01/05-17:20:14.394743 7f93eb7fe6c0 Delete type=0 #1229
2026/01/05-17:22:12.857180 7f93e9ffb6c0 Level-0 table #1234: started
2026/01/05-17:22:12.857204 7f93e9ffb6c0 Level-0 table #1234: 0 bytes OK
2026/01/05-17:22:12.863647 7f93e9ffb6c0 Delete type=0 #1232
2026/01/05-17:22:12.870424 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!50u8VAjdmovyr0hx' @ 72057594037927935 : 1 .. '!journal.pages!yzw9I0r3hCK7PJnz.sPNCYj2nR3Cp3jHd' @ 0 : 0; will stop at (end)
2026/01/07-15:26:43.514726 7f93ea7fc6c0 Recovering log #1257
2026/01/07-15:26:43.524436 7f93ea7fc6c0 Delete type=3 #1255
2026/01/07-15:26:43.524498 7f93ea7fc6c0 Delete type=0 #1257
2026/01/07-15:53:28.406072 7f93e9ffb6c0 Level-0 table #1262: started
2026/01/07-15:53:28.406116 7f93e9ffb6c0 Level-0 table #1262: 0 bytes OK
2026/01/07-15:53:28.412401 7f93e9ffb6c0 Delete type=0 #1260
2026/01/07-15:53:28.419261 7f93e9ffb6c0 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 @@
2026/01/04-22:01:24.160908 7f93ea7fc6c0 Recovering log #1224
2026/01/04-22:01:24.250354 7f93ea7fc6c0 Delete type=3 #1222
2026/01/04-22:01:24.250444 7f93ea7fc6c0 Delete type=0 #1224
2026/01/04-22:02:13.371437 7f93e9ffb6c0 Level-0 table #1230: started
2026/01/04-22:02:13.371468 7f93e9ffb6c0 Level-0 table #1230: 0 bytes OK
2026/01/04-22:02:13.377536 7f93e9ffb6c0 Delete type=0 #1228
2026/01/04-22:02:13.385115 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!50u8VAjdmovyr0hx' @ 72057594037927935 : 1 .. '!journal.pages!yzw9I0r3hCK7PJnz.sPNCYj2nR3Cp3jHd' @ 0 : 0; will stop at (end)
2026/01/07-15:09:19.258501 7f93eb7fe6c0 Recovering log #1253
2026/01/07-15:09:19.274117 7f93eb7fe6c0 Delete type=3 #1251
2026/01/07-15:09:19.274190 7f93eb7fe6c0 Delete type=0 #1253
2026/01/07-15:13:49.656004 7f93e9ffb6c0 Level-0 table #1258: started
2026/01/07-15:13:49.656029 7f93e9ffb6c0 Level-0 table #1258: 0 bytes OK
2026/01/07-15:13:49.662317 7f93e9ffb6c0 Delete type=0 #1256
2026/01/07-15:13:49.662464 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!50u8VAjdmovyr0hx' @ 72057594037927935 : 1 .. '!journal.pages!yzw9I0r3hCK7PJnz.sPNCYj2nR3Cp3jHd' @ 0 : 0; will stop at (end)

View File

@@ -1 +1 @@
MANIFEST-001231
MANIFEST-001259

View File

@@ -1,7 +1,7 @@
2026/01/05-17:20:14.280921 7f93ea7fc6c0 Recovering log #1229
2026/01/05-17:20:14.337958 7f93ea7fc6c0 Delete type=3 #1227
2026/01/05-17:20:14.338043 7f93ea7fc6c0 Delete type=0 #1229
2026/01/05-17:22:12.838440 7f93e9ffb6c0 Level-0 table #1234: started
2026/01/05-17:22:12.838473 7f93e9ffb6c0 Level-0 table #1234: 0 bytes OK
2026/01/05-17:22:12.844706 7f93e9ffb6c0 Delete type=0 #1232
2026/01/05-17:22:12.844991 7f93e9ffb6c0 Manual compaction at level-0 from '!tables!4l60Lxv8cpsyy2Cg' @ 72057594037927935 : 1 .. '!tables.results!tfaYKDZqu7kgZvRG.yvbwKursaixh2dby' @ 0 : 0; will stop at (end)
2026/01/07-15:26:43.501310 7f93eb7fe6c0 Recovering log #1257
2026/01/07-15:26:43.512173 7f93eb7fe6c0 Delete type=3 #1255
2026/01/07-15:26:43.512224 7f93eb7fe6c0 Delete type=0 #1257
2026/01/07-15:53:28.398067 7f93e9ffb6c0 Level-0 table #1262: started
2026/01/07-15:53:28.398117 7f93e9ffb6c0 Level-0 table #1262: 0 bytes OK
2026/01/07-15:53:28.405830 7f93e9ffb6c0 Delete type=0 #1260
2026/01/07-15:53:28.412533 7f93e9ffb6c0 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 @@
2026/01/04-22:01:24.060098 7f93ebfff6c0 Recovering log #1224
2026/01/04-22:01:24.158104 7f93ebfff6c0 Delete type=3 #1222
2026/01/04-22:01:24.158187 7f93ebfff6c0 Delete type=0 #1224
2026/01/04-22:02:13.363838 7f93e9ffb6c0 Level-0 table #1230: started
2026/01/04-22:02:13.363872 7f93e9ffb6c0 Level-0 table #1230: 0 bytes OK
2026/01/04-22:02:13.371335 7f93e9ffb6c0 Delete type=0 #1228
2026/01/04-22:02:13.385100 7f93e9ffb6c0 Manual compaction at level-0 from '!tables!4l60Lxv8cpsyy2Cg' @ 72057594037927935 : 1 .. '!tables.results!tfaYKDZqu7kgZvRG.yvbwKursaixh2dby' @ 0 : 0; will stop at (end)
2026/01/07-15:09:19.238733 7f93ebfff6c0 Recovering log #1253
2026/01/07-15:09:19.256250 7f93ebfff6c0 Delete type=3 #1251
2026/01/07-15:09:19.256331 7f93ebfff6c0 Delete type=0 #1253
2026/01/07-15:13:49.649560 7f93e9ffb6c0 Level-0 table #1258: started
2026/01/07-15:13:49.649585 7f93e9ffb6c0 Level-0 table #1258: 0 bytes OK
2026/01/07-15:13:49.655885 7f93e9ffb6c0 Delete type=0 #1256
2026/01/07-15:13:49.662454 7f93e9ffb6c0 Manual compaction at level-0 from '!tables!4l60Lxv8cpsyy2Cg' @ 72057594037927935 : 1 .. '!tables.results!tfaYKDZqu7kgZvRG.yvbwKursaixh2dby' @ 0 : 0; will stop at (end)

View File

@@ -1 +1 @@
MANIFEST-000874
MANIFEST-000902

View File

@@ -1,7 +1,7 @@
2026/01/05-17:20:14.509126 7f93ea7fc6c0 Recovering log #872
2026/01/05-17:20:14.568971 7f93ea7fc6c0 Delete type=3 #870
2026/01/05-17:20:14.569021 7f93ea7fc6c0 Delete type=0 #872
2026/01/05-17:22:12.863773 7f93e9ffb6c0 Level-0 table #877: started
2026/01/05-17:22:12.863802 7f93e9ffb6c0 Level-0 table #877: 0 bytes OK
2026/01/05-17:22:12.870319 7f93e9ffb6c0 Delete type=0 #875
2026/01/05-17:22:12.870431 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!056ILNNrLiPq3Gi3' @ 72057594037927935 : 1 .. '!journal.pages!yfZxl4I7XAuUF6r3.apXmOlZRmGT4GreB' @ 0 : 0; will stop at (end)
2026/01/07-15:26:43.552530 7f93eaffd6c0 Recovering log #900
2026/01/07-15:26:43.562158 7f93eaffd6c0 Delete type=3 #898
2026/01/07-15:26:43.562231 7f93eaffd6c0 Delete type=0 #900
2026/01/07-15:53:28.426399 7f93e9ffb6c0 Level-0 table #905: started
2026/01/07-15:53:28.426431 7f93e9ffb6c0 Level-0 table #905: 0 bytes OK
2026/01/07-15:53:28.432585 7f93e9ffb6c0 Delete type=0 #903
2026/01/07-15:53:28.440113 7f93e9ffb6c0 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 @@
2026/01/04-22:01:24.471625 7f93eaffd6c0 Recovering log #867
2026/01/04-22:01:24.559238 7f93eaffd6c0 Delete type=3 #865
2026/01/04-22:01:24.559306 7f93eaffd6c0 Delete type=0 #867
2026/01/04-22:02:13.404597 7f93e9ffb6c0 Level-0 table #873: started
2026/01/04-22:02:13.404646 7f93e9ffb6c0 Level-0 table #873: 0 bytes OK
2026/01/04-22:02:13.410707 7f93e9ffb6c0 Delete type=0 #871
2026/01/04-22:02:13.410796 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!056ILNNrLiPq3Gi3' @ 72057594037927935 : 1 .. '!journal.pages!yfZxl4I7XAuUF6r3.apXmOlZRmGT4GreB' @ 0 : 0; will stop at (end)
2026/01/07-15:09:19.313936 7f93eaffd6c0 Recovering log #896
2026/01/07-15:09:19.330494 7f93eaffd6c0 Delete type=3 #894
2026/01/07-15:09:19.330571 7f93eaffd6c0 Delete type=0 #896
2026/01/07-15:13:49.669421 7f93e9ffb6c0 Level-0 table #901: started
2026/01/07-15:13:49.669459 7f93e9ffb6c0 Level-0 table #901: 0 bytes OK
2026/01/07-15:13:49.675608 7f93e9ffb6c0 Delete type=0 #899
2026/01/07-15:13:49.688874 7f93e9ffb6c0 Manual compaction at level-0 from '!journal!056ILNNrLiPq3Gi3' @ 72057594037927935 : 1 .. '!journal.pages!yfZxl4I7XAuUF6r3.apXmOlZRmGT4GreB' @ 0 : 0; will stop at (end)

View File

@@ -7,4 +7,314 @@
color: darkolivegreen;
align-self: center;
text-align: center;
}
}
/* Styles pour le module de voyage TravelV2 */
.voyage-main-title {
font-size: 1.3em;
font-weight: bold;
margin-top: 0.5em;
margin-bottom: 0.3em;
}
.voyage-route-title {
font-size: 1.15em;
font-weight: bold;
margin-top: 0.8em;
margin-bottom: 0.3em;
color: #4a5568;
}
.voyage-section-title {
font-size: 1em;
font-weight: bold;
margin-top: 0.5em;
margin-bottom: 0.2em;
}
.voyage-destinations-title {
font-size: 1.2em;
font-weight: bold;
margin-bottom: 0.3em;
}
.voyage-separator {
margin-top: 1em;
margin-bottom: 1em;
border: 0;
border-top: 1px solid #ccc;
}
/* Styles pour le module Inn (Auberge) */
.wfrp4e-inn-help h3,
.wfrp4e-inn-list h3 {
font-size: 1.3em;
font-weight: bold;
margin-top: 0.5em;
margin-bottom: 0.5em;
color: #8b4513;
border-bottom: 2px solid #d2691e;
padding-bottom: 0.3em;
}
.wfrp4e-inn-help ul,
.wfrp4e-inn-list ul {
margin-left: 1.5em;
list-style-type: disc;
}
.wfrp4e-inn-help li,
.wfrp4e-inn-table-list li {
margin: 0.3em 0;
}
.wfrp4e-inn-help hr {
margin: 1em 0;
border: 0;
border-top: 1px solid #ccc;
}
.wfrp4e-inn-help code {
background-color: #f5f5f5;
padding: 0.2em 0.4em;
border-radius: 3px;
font-family: monospace;
color: #d63384;
}
.wfrp4e-inn-help h4 {
font-size: 1em;
font-weight: bold;
margin: 0.8em 0 0.5em 0;
color: var(--color-warm);
}
/* Grille de boutons pour les tables */
.wfrp4e-inn-table-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 0.5em;
margin: 0.5em 0;
}
.inn-table-btn {
display: block;
padding: 0.6em 0.8em;
background: rgba(139, 69, 19, 0.12);
border: 2px solid rgba(139, 69, 19, 0.35);
border-radius: 4px;
text-align: center;
font-weight: 600;
font-size: 0.95em;
color: var(--color-warm);
transition: all 0.2s;
text-decoration: none;
}
.inn-table-btn:hover {
background: rgba(139, 69, 19, 0.2);
border-color: rgba(139, 69, 19, 0.5);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
.inn-table-btn i {
margin-right: 0.4em;
color: var(--color-warm-3);
}
/* Anciens styles - à supprimer ou garder pour compatibilité */
.wfrp4e-inn-table-list {
list-style: none;
margin-left: 0;
}
.wfrp4e-inn-table-list li {
margin: 0.5em 0;
padding-left: 0;
}
.wfrp4e-inn-table-list a.action-link {
display: inline-block;
padding: 0.3em 0.8em;
background-color: #8b4513;
color: white;
text-decoration: none;
border-radius: 4px;
transition: background-color 0.2s;
}
.wfrp4e-inn-table-list a.action-link:hover {
background-color: #a0522d;
}
.wfrp4e-inn-table-list a.action-link i {
margin-right: 0.5em;
}
.inn-keyword {
font-size: 0.85em;
color: var(--color-grey2);
font-style: italic;
}
/* Styles pour les résultats de jets d'auberge - Thème WFRP4e */
.wfrp4e-inn-result {
margin: 0.3em 0;
}
.wfrp4e-inn-result .message-header {
text-shadow: 0px 0px 1px #00000087;
border: 2px solid rgba(62, 0, 0, 0.3);
padding: 0.4em 0.6em;
margin-bottom: 0.5em;
background: rgba(0, 0, 0, 0.05);
}
.wfrp4e-inn-result .flavor-text {
color: var(--color-warm);
font-weight: bold;
font-size: 1em;
}
.wfrp4e-inn-result .message-header i {
margin-right: 0.3em;
color: var(--color-warm-3);
}
.inn-dish-name {
font-size: 1.15em;
font-weight: bold;
text-align: center;
padding: 0.6em;
margin: 0.3em 0;
background: rgba(255, 255, 255, 0.3);
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
.inn-roll-info {
text-align: center;
font-size: 0.85em;
color: var(--color-grey2);
margin-top: 0.4em;
padding: 0.3em;
}
.inn-roll-info i {
margin-right: 0.2em;
}
/* Styles pour le choix de menu */
.wfrp4e-inn-menu-choice {
padding: 0.5em;
}
.wfrp4e-inn-menu-choice h3 {
color: var(--color-warm);
margin: 0 0 0.5em 0;
}
.inn-menu-buttons {
display: flex;
flex-direction: column;
gap: 0.5em;
margin: 0.5em 0;
}
.inn-menu-btn {
display: block;
padding: 0.6em 1em;
background: rgba(139, 69, 19, 0.1);
border: 2px solid rgba(139, 69, 19, 0.3);
border-radius: 4px;
text-align: center;
font-weight: bold;
transition: all 0.2s;
}
.inn-menu-btn:hover {
background: rgba(139, 69, 19, 0.2);
border-color: rgba(139, 69, 19, 0.5);
transform: translateY(-1px);
}
.inn-menu-btn i {
margin-right: 0.4em;
color: var(--color-warm-3);
}
.inn-menu-desc {
display: block;
font-size: 0.85em;
font-weight: normal;
color: var(--color-grey2);
margin-top: 0.2em;
}
/* Styles pour le résultat de menu */
.wfrp4e-inn-menu-result {
margin: 0.3em 0;
}
.wfrp4e-inn-menu-result .message-header {
text-shadow: 0px 0px 1px #00000087;
border: 2px solid rgba(62, 0, 0, 0.3);
padding: 0.4em 0.6em;
margin-bottom: 0.5em;
background: rgba(0, 0, 0, 0.05);
}
.wfrp4e-inn-menu-result .flavor-text {
color: var(--color-warm);
font-weight: bold;
font-size: 1em;
}
.inn-menu-items {
display: flex;
flex-direction: column;
gap: 0.3em;
}
.inn-menu-item {
padding: 0.5em;
background: rgba(255, 255, 255, 0.3);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 3px;
}
.inn-menu-item i {
margin-right: 0.4em;
color: var(--color-warm-3);
}
.inn-menu-item strong {
color: var(--color-warm);
}
/* Bouton rapide Menu dans l'aide */
.inn-menu-quick-btn {
display: block;
padding: 0.7em 1em;
background: rgba(139, 69, 19, 0.15);
border: 2px solid rgba(139, 69, 19, 0.4);
border-radius: 4px;
text-align: center;
font-weight: bold;
font-size: 1.05em;
color: var(--color-warm);
transition: all 0.2s;
}
.inn-menu-quick-btn:hover {
background: rgba(139, 69, 19, 0.25);
border-color: rgba(139, 69, 19, 0.6);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.inn-menu-quick-btn i {
margin-right: 0.5em;
color: var(--color-warm-3);
}

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

View File

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

View File

@@ -1,6 +0,0 @@
let test = await this.actor.setupCharacteristic("wp", {skipTargets: true, appendTitle : ` ${this.effect.name}`})
await test.roll()
if (test.succeeded)
{
this.effect.delete();
}

View File

@@ -1 +0,0 @@
args.options.cardsharp = true;

View File

@@ -1,5 +0,0 @@
if (args.opposedTest.result.hitloc.value == this.item.system.location.key && args.totalWoundLoss > 0)
{
args.actor.addCondition("bleeding", 2);
}

View File

@@ -1 +0,0 @@
return ["ws", "bs", "s", "ag", "t", "dex"].includes(args.characteristic)

View File

@@ -1,7 +0,0 @@
let test = await args.actor.setupCharacteristic("wp", {skipTargets: true, appendTitle : " - " + this.effect.name, context : {failure: "Gained a Stunned Condition"}})
await test.roll();
if (test.failed)
{
args.actor.addCondition("stunned")
}

View File

@@ -1 +0,0 @@
this.actor.status.addArmour(1, {source: this.effect, magical : true})

View File

@@ -1 +0,0 @@
this.script.scriptNotification(`Cannot enter ${this.effect.name}!`);

View File

@@ -1,3 +0,0 @@
let healed = parseInt(this.effect.sourceTest.result.SL)
this.actor.modifyWounds(healed)
this.script.scriptMessage(`Healed ${healed} Wounds`)

View File

@@ -1,20 +0,0 @@
let damage = this.effect.sourceActor.hasCondition("fatigued") ? 6 : 10;
let loc = "body"
let APatLoc = this.actor.system.status.armour[loc];
let metalAP = APatLoc.layers.reduce((metal, layer) => metal += ((layer.metal && !layer.magical) ? layer.value : 0), 0)
let APused = Math.max(0, APatLoc.value - metalAP); // remove metal AP at location;
damage -= (APused + this.actor.system.characteristics.t.bonus)
let msg = await this.actor.applyBasicDamage(damage, {suppressMsg : true, damageType : game.wfrp4e.config.DAMAGE_TYPE.IGNORE_ALL});
msg += ` (ignored ${metalAP} metal AP on ${game.wfrp4e.config.locations[loc]})`
this.script.scriptMessage(msg)
let test = await this.actor.setupSkill("Endurance", {fields : {difficulty : "difficult"}, appendTitle : ` - ${this.effect.name}`});
await test.roll();
if (test.failed)
this.actor.addCondition("stunned");

View File

@@ -1,19 +0,0 @@
let test = await this.actor.setupSkill(game.i18n.localize("NAME.Endurance"), {skipTargets: true, appendTitle : " - " + this.effect.name})
await test.roll();
if (!test.succeeded)
{
let item = await fromUuid("Compendium.wfrp4e-core.items.ZhMADOqoo0y8Q9bx")
let data = item.toObject();
if (this.item.system.location.key == "rLeg")
{
data.system.location.value = "Right Toe"
data.system.location.key = "rToe";
}
else if (this.item.system.location.key == "lLeg")
{
data.system.location.value = "Left Toe"
data.system.location.key = "lToe";
}
this.actor.createEmbeddedDocuments("Item", [data])
}
this.effect.delete();

View File

@@ -1 +0,0 @@
args.options.ballockKnife = true;

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", 3)
}
})

View File

@@ -1,2 +0,0 @@
if (args.effect.conditionId == "bleeding")
args.data.damage -= 1

View File

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

View File

@@ -1,10 +0,0 @@
let choice = await ItemDialog.create(ItemDialog.objectToArray(game.wfrp4e.config.locations), 1, "Choose Location");
let location = choice[0].id;
let itemTargeted = this.actor.items.get(this.effect.getFlag("wfrp4e", "itemTargets")[0])
if (itemTargeted)
{
itemTargeted.update({[`system.APdamage.${location}`] : itemTargeted.system.APdamage[location] + 1})
}

View File

@@ -1 +0,0 @@
return args.options.reload

View File

@@ -1 +0,0 @@
return args.type != "channelling" && !args.skill?.name.includes(game.i18n.localize("NAME.Channelling"))

View File

@@ -1,5 +0,0 @@
if (args.item.range && args.item.range.bands)
{
args.item.range.bands[game.i18n.localize("Long Range")].modifier = 0
args.item.range.bands[game.i18n.localize("Extreme")].modifier /= 2
}

View File

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

View File

@@ -1 +0,0 @@
game.wfrp4e.utility.postCorruptionTest(this.item.system.specification.value, {speaker : {alias: this.actor.prototypeToken.name}})

View File

@@ -1 +0,0 @@
return args.skill?.name == game.i18n.localize("NAME.Bribery") || args.skill?.name.includes(game.i18n.localize("NAME.Stealth"));

View File

@@ -1 +0,0 @@
return args.item?.system.attackType

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