forked from public/foundryvtt-wh4-lang-fr-fr
Ajout de la commande /voyage et grosse MAJK de la commande /auberge
This commit is contained in:
339
modules/travelv2/TravelDistanceV2.js
Normal file
339
modules/travelv2/TravelDistanceV2.js
Normal file
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* TravelDistanceV2
|
||||
* Classe de gestion du calcul des distances de voyage pour WFRP4e
|
||||
* Version adaptée pour le module de traduction française
|
||||
*/
|
||||
import { PathFinder } from './pathfinding.js';
|
||||
|
||||
export default class TravelDistanceV2 {
|
||||
static roadGraph = null; // Graphe pour les routes terrestres uniquement
|
||||
static waterGraph = null; // Graphe pour les voies fluviales et maritimes
|
||||
static mixedGraph = null; // Graphe combinant tous les modes de transport
|
||||
|
||||
/**
|
||||
* Charge les données de voyage depuis le fichier JSON
|
||||
*/
|
||||
static async loadTravelData() {
|
||||
try {
|
||||
console.log("TravelV2: Début du chargement des données...");
|
||||
const response = await fetch('modules/wh4-fr-translation/modules/travelv2/travel_data.json');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
this.travel_data = await response.json();
|
||||
console.log(`TravelV2: ${this.travel_data.length} routes chargées avec succès`);
|
||||
|
||||
// Construire les 3 graphes pour le pathfinding
|
||||
this.roadGraph = PathFinder.buildGraph(this.travel_data, 'road');
|
||||
this.waterGraph = PathFinder.buildGraph(this.travel_data, 'water');
|
||||
this.mixedGraph = PathFinder.buildGraph(this.travel_data, 'mixed');
|
||||
|
||||
console.log(`TravelV2: Graphe routier: ${Object.keys(this.roadGraph).length} villes`);
|
||||
console.log(`TravelV2: Graphe fluvial/maritime: ${Object.keys(this.waterGraph).length} villes`);
|
||||
console.log(`TravelV2: Graphe mixte: ${Object.keys(this.mixedGraph).length} villes`);
|
||||
|
||||
ui.notifications.info(`TravelV2: ${this.travel_data.length} routes de voyage chargées`);
|
||||
} catch (error) {
|
||||
console.error("TravelV2: Erreur lors du chargement des données de voyage:", error);
|
||||
ui.notifications.error("Erreur lors du chargement des données de voyage. Vérifiez la console.");
|
||||
this.travel_data = []; // Initialiser avec un tableau vide pour éviter les erreurs
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne une chaîne lisible pour le niveau de danger
|
||||
* @param {String} dangerLevel
|
||||
* @returns {String}
|
||||
*/
|
||||
static dangerToString(dangerLevel) {
|
||||
if (dangerLevel == "") return "Très bas";
|
||||
if (dangerLevel == '!') return "Bas";
|
||||
if (dangerLevel == '!!') return "Moyen";
|
||||
if (dangerLevel == '!!!') return "Élevé";
|
||||
return "Très élevé";
|
||||
}
|
||||
|
||||
/**
|
||||
* Arrondit la durée à une valeur entière ou .5
|
||||
* @param {Number} duration
|
||||
* @returns {Number}
|
||||
*/
|
||||
static roundDuration(duration) {
|
||||
let trunc = Math.trunc(duration);
|
||||
let frac = duration - trunc;
|
||||
let adjust = 0;
|
||||
if (frac > 0.75) adjust = 1;
|
||||
else if (frac >= 0.25) adjust = 0.5;
|
||||
return trunc + adjust;
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche les distances de voyage entre deux villes ou liste les destinations
|
||||
* @param {String} fromTown Ville de départ
|
||||
* @param {String} toTown Ville d'arrivée (optionnel)
|
||||
*/
|
||||
static displayTravelDistance(fromTown, toTown) {
|
||||
// Vérifier que les données sont chargées
|
||||
if (!this.travel_data || this.travel_data.length === 0) {
|
||||
ui.notifications.error("Les données de voyage ne sont pas encore chargées. Veuillez patienter...");
|
||||
console.error("TravelV2: travel_data n'est pas chargé");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`TravelV2: displayTravelDistance appelé avec fromTown="${fromTown}", toTown="${toTown}"`);
|
||||
console.log(`TravelV2: fromTown type: ${typeof fromTown}, valeur falsy: ${!fromTown}`);
|
||||
console.log(`TravelV2: toTown type: ${typeof toTown}, valeur falsy: ${!toTown}`);
|
||||
console.log(`TravelV2: ${this.travel_data.length} routes disponibles`);
|
||||
|
||||
let message = "";
|
||||
|
||||
console.log("TravelV2: Vérification des conditions...");
|
||||
console.log(`TravelV2: toTown ? ${!!toTown}`);
|
||||
console.log(`TravelV2: fromTown == 'help' ? ${fromTown == 'help'}`);
|
||||
console.log(`TravelV2: fromTown ? ${!!fromTown}`);
|
||||
console.log(`TravelV2: else (pas de fromTown) ? ${!fromTown && !toTown}`);
|
||||
|
||||
if (toTown) {
|
||||
console.log("TravelV2: Branche: Affichage des détails entre deux villes");
|
||||
// Afficher les détails de voyage entre deux villes spécifiques
|
||||
const originalFrom = fromTown;
|
||||
const originalTo = toTown;
|
||||
fromTown = fromTown.toLowerCase();
|
||||
toTown = toTown.toLowerCase();
|
||||
|
||||
// Chercher d'abord une route directe
|
||||
let directRoute = null;
|
||||
for (const travel of this.travel_data) {
|
||||
if (travel.from.toLowerCase() == fromTown && travel.to.toLowerCase() == toTown) {
|
||||
directRoute = travel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (directRoute) {
|
||||
// Route directe trouvée - affichage classique avec toutes les options
|
||||
message += this.formatDirectRoute(directRoute);
|
||||
} else {
|
||||
// Pas de route directe - calculer les 3 types de trajets
|
||||
console.log("TravelV2: Pas de route directe, calcul des itinéraires...");
|
||||
|
||||
const mixedPath = PathFinder.dijkstra(this.mixedGraph, originalFrom, originalTo, 'days');
|
||||
const roadPath = PathFinder.dijkstra(this.roadGraph, originalFrom, originalTo, 'days');
|
||||
const waterPath = PathFinder.dijkstra(this.waterGraph, originalFrom, originalTo, 'days');
|
||||
|
||||
if (!mixedPath && !roadPath && !waterPath) {
|
||||
message += `<p><strong>Aucun chemin trouvé entre ${originalFrom} et ${originalTo}</strong></p>`;
|
||||
message += `<p>Il n'existe pas de route reliant ces deux villes dans les données disponibles.</p>`;
|
||||
} else {
|
||||
message += `<div class="voyage-main-title">De ${originalFrom} à ${originalTo}</div>`;
|
||||
message += `<p><em>Différentes options de voyage disponibles :</em></p><hr class="voyage-separator">`;
|
||||
|
||||
// Option 1 : Trajet mixte optimal (le plus rapide)
|
||||
if (mixedPath) {
|
||||
message += this.formatMultiStepRoute(mixedPath, originalFrom, originalTo, 'mixed');
|
||||
message += `<hr class="voyage-separator">`;
|
||||
}
|
||||
|
||||
// Option 2 : Trajet 100% terrestre
|
||||
if (roadPath) {
|
||||
message += this.formatMultiStepRoute(roadPath, originalFrom, originalTo, 'road');
|
||||
message += `<hr class="voyage-separator">`;
|
||||
}
|
||||
|
||||
// Option 3 : Trajet 100% eau (fleuve/mer)
|
||||
if (waterPath) {
|
||||
message += this.formatMultiStepRoute(waterPath, originalFrom, originalTo, 'water');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (fromTown && fromTown == "help") {
|
||||
console.log("TravelV2: Branche: Affichage de l'aide");
|
||||
// Afficher l'aide
|
||||
message += `<p><strong>Aide pour /voyage</strong><br>`;
|
||||
message += `Usage: <code>/voyage [ville_départ] [ville_arrivée]</code><br><br>`;
|
||||
message += `Exemples:<br>`;
|
||||
message += `<code>/voyage</code> - Liste toutes les villes de départ<br>`;
|
||||
message += `<code>/voyage Altdorf</code> - Liste les destinations depuis Altdorf<br>`;
|
||||
message += `<code>/voyage Altdorf Nuln</code> - Affiche les détails de voyage entre Altdorf et Nuln`;
|
||||
message += `</p>`;
|
||||
} else if (fromTown) {
|
||||
console.log("TravelV2: Branche: Liste des destinations depuis une ville");
|
||||
// Lister toutes les destinations possibles depuis une ville (avec pathfinding)
|
||||
const normalizedFrom = PathFinder.findCityInGraph(this.roadGraph, fromTown);
|
||||
|
||||
if (normalizedFrom) {
|
||||
message += `<div class="voyage-destinations-title">Destinations depuis ${normalizedFrom}</div>`;
|
||||
message += `<p><em>Toutes les villes accessibles (routes directes et itinéraires calculés)</em></p>`;
|
||||
|
||||
// Récupérer toutes les villes du graphe sauf la ville de départ
|
||||
const allCities = Object.keys(this.roadGraph)
|
||||
.filter(city => city.toLowerCase() !== normalizedFrom.toLowerCase())
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
// Afficher toutes les destinations
|
||||
for (const city of allCities) {
|
||||
message += `<p><a class="action-link" data-action="clickVoyage" data-from="${normalizedFrom}" data-to="${city}"><i class="fas fa-map-marked-alt"></i> ${city}</a></p>`;
|
||||
}
|
||||
} else {
|
||||
message += `<p><strong>Ville inconnue: ${fromTown}</strong></p>`;
|
||||
message += `<p>Cette ville n'existe pas dans la base de données.</p>`;
|
||||
}
|
||||
} else {
|
||||
console.log("TravelV2: Branche: Liste de toutes les villes de départ");
|
||||
// Lister toutes les villes de départ
|
||||
message += `<div class="voyage-destinations-title">Sélectionnez une ville de départ</div>`;
|
||||
let uniqTown = {};
|
||||
|
||||
for (const travel of this.travel_data) {
|
||||
if (uniqTown[travel.from] == undefined) {
|
||||
uniqTown[travel.from] = 1;
|
||||
message += `<p><a class="action-link" data-action="clickVoyage" data-from="${travel.from}"><i class="fas fa-list"></i> ${travel.from}</a></p>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`TravelV2: Message généré (longueur: ${message.length})`);
|
||||
|
||||
if (message.length === 0) {
|
||||
console.warn("TravelV2: Aucune donnée trouvée pour les critères fournis");
|
||||
ui.notifications.warn("Aucune route trouvée pour ces critères");
|
||||
return;
|
||||
}
|
||||
|
||||
ChatMessage.create({
|
||||
content: message,
|
||||
whisper: [game.user.id], // Afficher uniquement pour le GM qui a lancé la commande
|
||||
speaker: { alias: "Outil de voyage" }
|
||||
});
|
||||
|
||||
console.log("TravelV2: ChatMessage créé avec succès");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère le clic sur un lien de voyage
|
||||
* @param {Event} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static handleTravelClick(event, target) {
|
||||
let fromTown = target.dataset.from;
|
||||
let toTown = target.dataset.to;
|
||||
TravelDistanceV2.displayTravelDistance(fromTown, toTown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate l'affichage d'une route directe
|
||||
* @param {Object} travel - Données de la route
|
||||
* @returns {String} HTML formaté
|
||||
*/
|
||||
static formatDirectRoute(travel) {
|
||||
let message = `<p><strong>De ${travel.from} à ${travel.to}</strong> (Route directe)`;
|
||||
|
||||
if (travel.road_distance != "") {
|
||||
let road_horse_heavy_days = this.roundDuration(travel.road_days * 0.8);
|
||||
let road_horse_fast_days = this.roundDuration(travel.road_days * 0.65);
|
||||
let road_feet_days = this.roundDuration(travel.road_days * 1.25);
|
||||
let road_danger_string = this.dangerToString(travel.road_danger);
|
||||
let road_danger_feet_string = this.dangerToString(travel.road_danger + "!");
|
||||
|
||||
message += `<br><br><strong>Par route:</strong>`;
|
||||
message += `<br>Distance: ${travel.road_distance} km`;
|
||||
message += `<br>Durée (chariot): ${travel.road_days} jours - Danger: ${road_danger_string}`;
|
||||
message += `<br>Durée (cheval de charge): ${road_horse_heavy_days} jours - Danger: ${road_danger_string}`;
|
||||
message += `<br>Durée (cheval rapide): ${road_horse_fast_days} jours - Danger: ${road_danger_string}`;
|
||||
message += `<br>Durée (à pied): ${road_feet_days} jours - Danger: ${road_danger_feet_string}`;
|
||||
}
|
||||
|
||||
if (travel.river_distance != "") {
|
||||
let river_danger_string = this.dangerToString(travel.river_danger);
|
||||
message += `<br><br><strong>Par rivière:</strong>`;
|
||||
message += `<br>Distance: ${travel.river_distance} km`;
|
||||
message += `<br>Durée: ${travel.river_days} jours - Danger: ${river_danger_string}`;
|
||||
}
|
||||
|
||||
if (travel.sea_distance != "") {
|
||||
let sea_danger_string = this.dangerToString(travel.sea_danger);
|
||||
message += `<br><br><strong>Par mer:</strong>`;
|
||||
message += `<br>Distance: ${travel.sea_distance} km`;
|
||||
message += `<br>Durée: ${travel.sea_days} jours - Danger: ${sea_danger_string}`;
|
||||
}
|
||||
|
||||
message += "</p>";
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate l'affichage d'une route multi-étapes
|
||||
* @param {Object} pathResult - Résultat du pathfinding
|
||||
* @param {String} fromCity - Ville de départ
|
||||
* @param {String} toCity - Ville d'arrivée
|
||||
* @param {String} routeType - Type de route ('mixed', 'road', 'water')
|
||||
* @returns {String} HTML formaté
|
||||
*/
|
||||
static formatMultiStepRoute(pathResult, fromCity, toCity, routeType = 'road') {
|
||||
// Déterminer le titre selon le type
|
||||
let routeTitle = "";
|
||||
|
||||
if (routeType === 'mixed') {
|
||||
routeTitle = "🌟 Itinéraire optimal (tous modes de transport)";
|
||||
} else if (routeType === 'road') {
|
||||
routeTitle = "🛤️ Itinéraire 100% terrestre";
|
||||
} else if (routeType === 'water') {
|
||||
routeTitle = "⛵ Itinéraire 100% fluvial/maritime";
|
||||
}
|
||||
|
||||
let message = `<div class="voyage-route-title">${routeTitle}</div>`;
|
||||
message += `<p><strong>${fromCity} → ${toCity}</strong> (${pathResult.steps} étape${pathResult.steps > 1 ? 's' : ''})`;
|
||||
|
||||
// Résumé du voyage
|
||||
const totalDistance = Math.round(pathResult.totalDistance);
|
||||
const totalDays = Math.round(pathResult.totalDays);
|
||||
const danger_string = this.dangerToString(pathResult.maxDanger);
|
||||
|
||||
// Pour les routes terrestres, afficher les variantes de durée
|
||||
const includesRoad = !pathResult.modesUsed || pathResult.modesUsed.includes('road');
|
||||
|
||||
message += `<br><br><strong>📊 Résumé du voyage:</strong>`;
|
||||
message += `<br>Distance totale: ${totalDistance} km`;
|
||||
|
||||
if (includesRoad) {
|
||||
const road_horse_heavy_days = this.roundDuration(pathResult.totalDays * 0.8);
|
||||
const road_horse_fast_days = this.roundDuration(pathResult.totalDays * 0.65);
|
||||
const road_feet_days = this.roundDuration(pathResult.totalDays * 1.25);
|
||||
const danger_feet_string = this.dangerToString(pathResult.maxDanger + "!");
|
||||
|
||||
message += `<br>Durée (chariot): ${totalDays} jours - Danger max: ${danger_string}`;
|
||||
message += `<br>Durée (cheval de charge): ${road_horse_heavy_days} jours - Danger max: ${danger_string}`;
|
||||
message += `<br>Durée (cheval rapide): ${road_horse_fast_days} jours - Danger max: ${danger_string}`;
|
||||
message += `<br>Durée (à pied): ${road_feet_days} jours - Danger max: ${danger_feet_string}`;
|
||||
} else {
|
||||
message += `<br>Durée totale: ${totalDays} jours - Danger max: ${danger_string}`;
|
||||
}
|
||||
|
||||
// Détails des étapes avec mode de transport
|
||||
message += `<br><br><strong>🗺️ Itinéraire détaillé:</strong>`;
|
||||
for (let i = 0; i < pathResult.path.length; i++) {
|
||||
const step = pathResult.path[i];
|
||||
const stepNum = i + 1;
|
||||
const stepDanger = this.dangerToString(step.danger);
|
||||
|
||||
// Icône selon le mode de transport
|
||||
let modeIcon = "🛤️";
|
||||
let modeName = "route";
|
||||
if (step.mode === 'river') {
|
||||
modeIcon = "🚣";
|
||||
modeName = "fleuve";
|
||||
} else if (step.mode === 'sea') {
|
||||
modeIcon = "⛵";
|
||||
modeName = "mer";
|
||||
}
|
||||
|
||||
message += `<br><br><em>Étape ${stepNum}:</em> ${step.from} → ${step.to}`;
|
||||
message += `<br> ${modeIcon} Par ${modeName}: ${Math.round(step.distance)} km, ${step.days} jour${step.days > 1 ? 's' : ''} - Danger: ${stepDanger}`;
|
||||
}
|
||||
|
||||
message += "</p>";
|
||||
return message;
|
||||
}
|
||||
}
|
||||
30
modules/travelv2/debug-display.js
Normal file
30
modules/travelv2/debug-display.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Script de débogage pour vérifier l'affichage du message
|
||||
*/
|
||||
|
||||
// Vérifier que les données sont chargées
|
||||
console.log("=== DEBUG DISPLAY ===");
|
||||
console.log("travel_data:", game.wfrp4e.travelv2.travel_data);
|
||||
console.log("Nombre de routes:", game.wfrp4e.travelv2.travel_data?.length);
|
||||
|
||||
// Générer le message manuellement comme le fait la fonction
|
||||
let message = "";
|
||||
message += `<h3>Sélectionnez une ville de départ</h3>`;
|
||||
let uniqTown = {};
|
||||
|
||||
for (var travel of game.wfrp4e.travelv2.travel_data) {
|
||||
if (uniqTown[travel.from] == undefined) {
|
||||
uniqTown[travel.from] = 1;
|
||||
message += `<p><a class="action-link" data-action="clickTravel2" data-from="${travel.from}"><i class="fas fa-list"></i> ${travel.from}</a></p>`;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Message généré:", message);
|
||||
console.log("Longueur du message:", message.length);
|
||||
console.log("Nombre de villes uniques:", Object.keys(uniqTown).length);
|
||||
|
||||
// Tester ChatMessage.create
|
||||
ChatMessage.create({
|
||||
content: message,
|
||||
whisper: game.user.isGM ? [] : [game.user.id]
|
||||
});
|
||||
69
modules/travelv2/diagnostic.js
Normal file
69
modules/travelv2/diagnostic.js
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Script de diagnostic pour TravelV2
|
||||
*
|
||||
* Copiez-collez ce code dans la console de Foundry VTT (F12)
|
||||
* pour diagnostiquer les problèmes de chargement
|
||||
*/
|
||||
|
||||
console.log("=== Diagnostic TravelV2 ===\n");
|
||||
|
||||
// Test 1: Vérifier si la classe existe
|
||||
console.log("1. Classe TravelDistanceV2 existe ?");
|
||||
if (typeof TravelDistanceV2 !== 'undefined') {
|
||||
console.log(" ✓ OUI - La classe est disponible");
|
||||
} else {
|
||||
console.log(" ✗ NON - La classe n'est pas chargée !");
|
||||
console.log(" → Vérifiez que le module est activé et rechargez (F5)");
|
||||
}
|
||||
|
||||
// Test 2: Vérifier les données
|
||||
console.log("\n2. Données chargées ?");
|
||||
if (typeof TravelDistanceV2 !== 'undefined' && TravelDistanceV2.travel_data) {
|
||||
console.log(` ✓ OUI - ${TravelDistanceV2.travel_data.length} routes chargées`);
|
||||
console.log(` → Première route: ${TravelDistanceV2.travel_data[0]?.from} → ${TravelDistanceV2.travel_data[0]?.to}`);
|
||||
} else {
|
||||
console.log(" ✗ NON - Les données ne sont pas chargées");
|
||||
console.log(" → Essayez de les charger manuellement:");
|
||||
console.log(" → await TravelDistanceV2.loadTravelData()");
|
||||
}
|
||||
|
||||
// Test 3: Tester le chargement manuel
|
||||
console.log("\n3. Test de chargement manuel:");
|
||||
console.log(" Exécutez: await TravelDistanceV2.loadTravelData()");
|
||||
console.log(" Puis vérifiez avec: TravelDistanceV2.travel_data.length");
|
||||
|
||||
// Test 4: Tester le chemin du fichier
|
||||
console.log("\n4. Vérification du chemin du fichier:");
|
||||
const path = 'modules/foundryvtt-wh4-lang-fr-fr/modules/travelv2/travel_data.json';
|
||||
console.log(` Chemin: ${path}`);
|
||||
console.log(" Test de fetch...");
|
||||
fetch(path)
|
||||
.then(response => {
|
||||
console.log(` ✓ Fichier accessible - Status: ${response.status}`);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log(` ✓ JSON valide - ${data.length} routes trouvées`);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(` ✗ Erreur: ${error.message}`);
|
||||
console.log(" → Vérifiez que le fichier travel_data.json existe bien");
|
||||
});
|
||||
|
||||
// Test 5: Afficher l'état du module
|
||||
console.log("\n5. État du module:");
|
||||
console.log(` game.modules = ${game.modules ? 'Disponible' : 'Non disponible'}`);
|
||||
const frModule = game.modules.get('foundryvtt-wh4-lang-fr-fr');
|
||||
if (frModule) {
|
||||
console.log(` ✓ Module trouvé: ${frModule.title}`);
|
||||
console.log(` → Actif: ${frModule.active}`);
|
||||
} else {
|
||||
console.log(" ✗ Module 'foundryvtt-wh4-lang-fr-fr' non trouvé");
|
||||
}
|
||||
|
||||
console.log("\n=== Fin du diagnostic ===");
|
||||
console.log("\nCommandes utiles:");
|
||||
console.log("• Charger les données: await TravelDistanceV2.loadTravelData()");
|
||||
console.log("• Vérifier les données: TravelDistanceV2.travel_data");
|
||||
console.log("• Tester l'affichage: TravelDistanceV2.displayTravelDistance()");
|
||||
console.log("• Tester avec ville: TravelDistanceV2.displayTravelDistance('Altdorf')");
|
||||
8
modules/travelv2/index.js
Normal file
8
modules/travelv2/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Index du module TravelV2
|
||||
*
|
||||
* Ce fichier exporte tous les composants du module TravelV2
|
||||
*/
|
||||
|
||||
export { default as TravelDistanceV2 } from './TravelDistanceV2.js';
|
||||
export { initTravelV2 } from './travelv2-init.js';
|
||||
218
modules/travelv2/pathfinding.js
Normal file
218
modules/travelv2/pathfinding.js
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* Algorithmes de calcul de plus court chemin pour les voyages
|
||||
*/
|
||||
|
||||
/**
|
||||
* Classe pour le calcul de plus court chemin (algorithme de Dijkstra)
|
||||
*/
|
||||
export class PathFinder {
|
||||
/**
|
||||
* Construit un graphe à partir des données de voyage
|
||||
* @param {Array} travelData - Données de voyage
|
||||
* @param {String|Array} modes - Mode(s) de transport ('road', 'river', 'sea', ['road', 'river', 'sea'], 'water')
|
||||
* @returns {Object} Graphe avec adjacence et poids
|
||||
*/
|
||||
static buildGraph(travelData, modes = 'road') {
|
||||
const graph = {};
|
||||
|
||||
// Normaliser modes en tableau
|
||||
let modeList = [];
|
||||
if (modes === 'water') {
|
||||
modeList = ['river', 'sea'];
|
||||
} else if (modes === 'mixed') {
|
||||
modeList = ['road', 'river', 'sea'];
|
||||
} else if (Array.isArray(modes)) {
|
||||
modeList = modes;
|
||||
} else {
|
||||
modeList = [modes];
|
||||
}
|
||||
|
||||
for (const route of travelData) {
|
||||
const from = route.from;
|
||||
const to = route.to;
|
||||
|
||||
// Initialiser les nœuds
|
||||
if (!graph[from]) {
|
||||
graph[from] = [];
|
||||
}
|
||||
if (!graph[to]) {
|
||||
graph[to] = [];
|
||||
}
|
||||
|
||||
// Pour chaque mode de transport disponible
|
||||
for (const mode of modeList) {
|
||||
const distanceKey = `${mode}_distance`;
|
||||
const daysKey = `${mode}_days`;
|
||||
const distance = route[distanceKey];
|
||||
const days = route[daysKey];
|
||||
|
||||
// Ignorer les routes sans ce mode de transport
|
||||
if (!distance || distance === "" || !days || days === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ajouter les arêtes (bidirectionnelles)
|
||||
graph[from].push({
|
||||
destination: to,
|
||||
distance: parseFloat(distance),
|
||||
days: parseFloat(days),
|
||||
danger: route[`${mode}_danger`] || "",
|
||||
mode: mode
|
||||
});
|
||||
|
||||
graph[to].push({
|
||||
destination: from,
|
||||
distance: parseFloat(distance),
|
||||
days: parseFloat(days),
|
||||
danger: route[`${mode}_danger`] || "",
|
||||
mode: mode
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithme de Dijkstra pour trouver le plus court chemin
|
||||
* @param {Object} graph - Graphe d'adjacence
|
||||
* @param {String} start - Ville de départ
|
||||
* @param {String} end - Ville d'arrivée
|
||||
* @param {String} metric - Métrique à minimiser ('distance' ou 'days')
|
||||
* @returns {Object|null} Chemin trouvé avec détails ou null si pas de chemin
|
||||
*/
|
||||
static dijkstra(graph, start, end, metric = 'days') {
|
||||
// Normaliser les noms de villes
|
||||
const normalizedStart = this.findCityInGraph(graph, start);
|
||||
const normalizedEnd = this.findCityInGraph(graph, end);
|
||||
|
||||
if (!normalizedStart || !normalizedEnd) {
|
||||
console.warn(`PathFinder: Ville non trouvée - start: ${start}, end: ${end}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Initialisation
|
||||
const distances = {};
|
||||
const previous = {};
|
||||
const visited = new Set();
|
||||
const queue = [];
|
||||
|
||||
// Initialiser toutes les distances à l'infini
|
||||
for (const node in graph) {
|
||||
distances[node] = Infinity;
|
||||
previous[node] = null;
|
||||
}
|
||||
|
||||
distances[normalizedStart] = 0;
|
||||
queue.push({ node: normalizedStart, distance: 0 });
|
||||
|
||||
while (queue.length > 0) {
|
||||
// Trouver le nœud avec la plus petite distance
|
||||
queue.sort((a, b) => a.distance - b.distance);
|
||||
const { node: current } = queue.shift();
|
||||
|
||||
if (visited.has(current)) continue;
|
||||
visited.add(current);
|
||||
|
||||
// Si on a atteint la destination
|
||||
if (current === normalizedEnd) {
|
||||
return this.reconstructPath(previous, normalizedStart, normalizedEnd, graph, metric);
|
||||
}
|
||||
|
||||
// Explorer les voisins
|
||||
const neighbors = graph[current] || [];
|
||||
for (const neighbor of neighbors) {
|
||||
if (visited.has(neighbor.destination)) continue;
|
||||
|
||||
const weight = neighbor[metric]; // 'distance' ou 'days'
|
||||
const newDistance = distances[current] + weight;
|
||||
|
||||
if (newDistance < distances[neighbor.destination]) {
|
||||
distances[neighbor.destination] = newDistance;
|
||||
previous[neighbor.destination] = {
|
||||
from: current,
|
||||
edge: neighbor
|
||||
};
|
||||
queue.push({ node: neighbor.destination, distance: newDistance });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pas de chemin trouvé
|
||||
console.warn(`PathFinder: Aucun chemin trouvé entre ${start} et ${end}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve une ville dans le graphe (insensible à la casse)
|
||||
* @param {Object} graph - Graphe
|
||||
* @param {String} cityName - Nom de la ville
|
||||
* @returns {String|null} Nom normalisé de la ville ou null
|
||||
*/
|
||||
static findCityInGraph(graph, cityName) {
|
||||
const lowerName = cityName.toLowerCase();
|
||||
for (const city in graph) {
|
||||
if (city.toLowerCase() === lowerName) {
|
||||
return city;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstruit le chemin à partir des prédécesseurs
|
||||
* @param {Object} previous - Map des prédécesseurs
|
||||
* @param {String} start - Ville de départ
|
||||
* @param {String} end - Ville d'arrivée
|
||||
* @param {Object} graph - Graphe d'adjacence
|
||||
* @param {String} metric - Métrique utilisée
|
||||
* @returns {Object} Détails du chemin
|
||||
*/
|
||||
static reconstructPath(previous, start, end, graph, metric) {
|
||||
const path = [];
|
||||
let current = end;
|
||||
let totalDistance = 0;
|
||||
let totalDays = 0;
|
||||
let maxDanger = "";
|
||||
const modesUsed = new Set();
|
||||
|
||||
// Reconstruire le chemin en remontant
|
||||
while (current !== start) {
|
||||
const prev = previous[current];
|
||||
if (!prev) break;
|
||||
|
||||
const mode = prev.edge.mode || 'road';
|
||||
modesUsed.add(mode);
|
||||
|
||||
path.unshift({
|
||||
from: prev.from,
|
||||
to: current,
|
||||
distance: prev.edge.distance,
|
||||
days: prev.edge.days,
|
||||
danger: prev.edge.danger,
|
||||
mode: mode
|
||||
});
|
||||
|
||||
totalDistance += prev.edge.distance;
|
||||
totalDays += prev.edge.days;
|
||||
|
||||
// Calculer le danger maximum
|
||||
const dangerLevel = (prev.edge.danger || "").length;
|
||||
const currentMaxLevel = maxDanger.length;
|
||||
if (dangerLevel > currentMaxLevel) {
|
||||
maxDanger = prev.edge.danger;
|
||||
}
|
||||
|
||||
current = prev.from;
|
||||
}
|
||||
|
||||
return {
|
||||
path: path,
|
||||
totalDistance: totalDistance,
|
||||
totalDays: totalDays,
|
||||
maxDanger: maxDanger,
|
||||
steps: path.length,
|
||||
modesUsed: Array.from(modesUsed)
|
||||
};
|
||||
}
|
||||
}
|
||||
53
modules/travelv2/test.js
Normal file
53
modules/travelv2/test.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Fichier de test pour le module TravelV2
|
||||
*
|
||||
* Pour tester le module dans la console de développement de Foundry VTT:
|
||||
*
|
||||
* 1. Charger le module
|
||||
* 2. Ouvrir la console (F12)
|
||||
* 3. Exécuter ces tests
|
||||
*/
|
||||
|
||||
// Test 1: Vérifier que la classe est chargée
|
||||
console.log("Test 1: Classe TravelDistanceV2 disponible?");
|
||||
console.log(typeof TravelDistanceV2 !== 'undefined' ? "✓ OK" : "✗ ÉCHEC");
|
||||
|
||||
// Test 2: Vérifier que les données sont chargées
|
||||
console.log("\nTest 2: Données de voyage chargées?");
|
||||
console.log(TravelDistanceV2.travel_data ? "✓ OK - " + TravelDistanceV2.travel_data.length + " routes trouvées" : "✗ ÉCHEC");
|
||||
|
||||
// Test 3: Tester dangerToString
|
||||
console.log("\nTest 3: Test de dangerToString");
|
||||
console.log("'' -> " + TravelDistanceV2.dangerToString(""));
|
||||
console.log("'!' -> " + TravelDistanceV2.dangerToString("!"));
|
||||
console.log("'!!' -> " + TravelDistanceV2.dangerToString("!!"));
|
||||
console.log("'!!!' -> " + TravelDistanceV2.dangerToString("!!!"));
|
||||
|
||||
// Test 4: Tester roundDuration
|
||||
console.log("\nTest 4: Test de roundDuration");
|
||||
console.log("22.1 -> " + TravelDistanceV2.roundDuration(22.1));
|
||||
console.log("22.3 -> " + TravelDistanceV2.roundDuration(22.3));
|
||||
console.log("22.5 -> " + TravelDistanceV2.roundDuration(22.5));
|
||||
console.log("22.8 -> " + TravelDistanceV2.roundDuration(22.8));
|
||||
|
||||
// Test 5: Tester displayTravelDistance avec une ville
|
||||
console.log("\nTest 5: Affichage des destinations depuis Altdorf");
|
||||
// TravelDistanceV2.displayTravelDistance("Altdorf");
|
||||
console.log("Exécutez manuellement: TravelDistanceV2.displayTravelDistance('Altdorf')");
|
||||
|
||||
// Test 6: Tester displayTravelDistance avec deux villes
|
||||
console.log("\nTest 6: Affichage du trajet Altdorf -> Nuln");
|
||||
// TravelDistanceV2.displayTravelDistance("Altdorf", "Nuln");
|
||||
console.log("Exécutez manuellement: TravelDistanceV2.displayTravelDistance('Altdorf', 'Nuln')");
|
||||
|
||||
// Test 7: Tester la commande help
|
||||
console.log("\nTest 7: Affichage de l'aide");
|
||||
// TravelDistanceV2.displayTravelDistance("help");
|
||||
console.log("Exécutez manuellement: TravelDistanceV2.displayTravelDistance('help')");
|
||||
|
||||
console.log("\n=== Tests terminés ===");
|
||||
console.log("\nPour tester la commande complète, tapez dans le chat:");
|
||||
console.log("/travel2");
|
||||
console.log("/travel2 Altdorf");
|
||||
console.log("/travel2 Altdorf Nuln");
|
||||
console.log("/travel2 help");
|
||||
2745
modules/travelv2/travel_data.json
Normal file
2745
modules/travelv2/travel_data.json
Normal file
File diff suppressed because it is too large
Load Diff
67
modules/travelv2/travelv2-init.js
Normal file
67
modules/travelv2/travelv2-init.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import TravelDistanceV2 from './TravelDistanceV2.js';
|
||||
|
||||
/**
|
||||
* Initialisation du module TravelV2
|
||||
*/
|
||||
export function initTravelV2() {
|
||||
console.log("TravelV2: Initialisation du module de voyage");
|
||||
|
||||
// Hook pour charger les données au démarrage
|
||||
Hooks.once('ready', async () => {
|
||||
console.log("TravelV2: Chargement des données de voyage");
|
||||
|
||||
// Exposer la classe globalement pour accès depuis la console
|
||||
game.wfrp4e = game.wfrp4e || {};
|
||||
game.wfrp4e.travelv2 = TravelDistanceV2;
|
||||
|
||||
await TravelDistanceV2.loadTravelData();
|
||||
|
||||
console.log("TravelV2: Classe accessible via game.wfrp4e.travelv2");
|
||||
|
||||
// Enregistrer la commande dans le système WFRP4e si disponible
|
||||
if (game.wfrp4e?.commands) {
|
||||
console.log("TravelV2: Enregistrement de la commande /voyage");
|
||||
game.wfrp4e.commands.add({
|
||||
voyage: {
|
||||
description: "Outil de calcul de distances de voyage (FR)",
|
||||
args: ["from", "to"],
|
||||
defaultArg: "from",
|
||||
callback: (from, to) => {
|
||||
// Vérifier que l'utilisateur est GM
|
||||
if (!game.user.isGM) {
|
||||
ui.notifications.warn("La commande /voyage est réservée au MJ.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`TravelV2: Commande /voyage exécutée`);
|
||||
console.log(`TravelV2: from =`, from, `(type: ${typeof from})`);
|
||||
console.log(`TravelV2: to =`, to, `(type: ${typeof to})`);
|
||||
console.log(`TravelV2: from === null ?`, from === null);
|
||||
console.log(`TravelV2: to === null ?`, to === null);
|
||||
|
||||
// Convertir null en undefined pour que la logique fonctionne
|
||||
from = from || undefined;
|
||||
to = to || undefined;
|
||||
|
||||
TravelDistanceV2.displayTravelDistance(from, to);
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log("TravelV2: Commande /voyage enregistrée avec succès");
|
||||
} else {
|
||||
console.warn("TravelV2: game.wfrp4e.commands non disponible");
|
||||
}
|
||||
});
|
||||
|
||||
// Hook pour ajouter un gestionnaire de clics sur les liens de voyage
|
||||
Hooks.on('renderChatMessage', (message, html, data) => {
|
||||
// Ajouter un listener pour les clics sur les liens de voyage
|
||||
html.find('a[data-action="clickVoyage"]').click((event) => {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget;
|
||||
TravelDistanceV2.handleTravelClick(event, target);
|
||||
});
|
||||
});
|
||||
|
||||
console.log("TravelV2: Module de voyage initialisé");
|
||||
}
|
||||
Reference in New Issue
Block a user