/** * 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 += `

Aucun chemin trouvé entre ${originalFrom} et ${originalTo}

`; message += `

Il n'existe pas de route reliant ces deux villes dans les données disponibles.

`; } else { message += `
De ${originalFrom} à ${originalTo}
`; message += `

Différentes options de voyage disponibles :


`; // Option 1 : Trajet mixte optimal (le plus rapide) if (mixedPath) { message += this.formatMultiStepRoute(mixedPath, originalFrom, originalTo, 'mixed'); message += `
`; } // Option 2 : Trajet 100% terrestre if (roadPath) { message += this.formatMultiStepRoute(roadPath, originalFrom, originalTo, 'road'); message += `
`; } // 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 += `

Aide pour /voyage
`; message += `Usage: /voyage [ville_départ] [ville_arrivée]

`; message += `Exemples:
`; message += `/voyage - Liste toutes les villes de départ
`; message += `/voyage Altdorf - Liste les destinations depuis Altdorf
`; message += `/voyage Altdorf Nuln - Affiche les détails de voyage entre Altdorf et Nuln`; message += `

`; } 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 += `
Destinations depuis ${normalizedFrom}
`; message += `

Toutes les villes accessibles (routes directes et itinéraires calculés)

`; // 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 += `

${city}

`; } } else { message += `

Ville inconnue: ${fromTown}

`; message += `

Cette ville n'existe pas dans la base de données.

`; } } else { console.log("TravelV2: Branche: Liste de toutes les villes de départ"); // Lister toutes les villes de départ message += `
Sélectionnez une ville de départ
`; let uniqTown = {}; for (const travel of this.travel_data) { if (uniqTown[travel.from] == undefined) { uniqTown[travel.from] = 1; message += `

${travel.from}

`; } } } 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 = `

De ${travel.from} à ${travel.to} (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 += `

Par route:`; message += `
Distance: ${travel.road_distance} km`; message += `
Durée (chariot): ${travel.road_days} jours - Danger: ${road_danger_string}`; message += `
Durée (cheval de charge): ${road_horse_heavy_days} jours - Danger: ${road_danger_string}`; message += `
Durée (cheval rapide): ${road_horse_fast_days} jours - Danger: ${road_danger_string}`; message += `
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 += `

Par rivière:`; message += `
Distance: ${travel.river_distance} km`; message += `
Durée: ${travel.river_days} jours - Danger: ${river_danger_string}`; } if (travel.sea_distance != "") { let sea_danger_string = this.dangerToString(travel.sea_danger); message += `

Par mer:`; message += `
Distance: ${travel.sea_distance} km`; message += `
Durée: ${travel.sea_days} jours - Danger: ${sea_danger_string}`; } message += "

"; 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 = `
${routeTitle}
`; message += `

${fromCity} → ${toCity} (${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 += `

📊 Résumé du voyage:`; message += `
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 += `
Durée (chariot): ${totalDays} jours - Danger max: ${danger_string}`; message += `
Durée (cheval de charge): ${road_horse_heavy_days} jours - Danger max: ${danger_string}`; message += `
Durée (cheval rapide): ${road_horse_fast_days} jours - Danger max: ${danger_string}`; message += `
Durée (à pied): ${road_feet_days} jours - Danger max: ${danger_feet_string}`; } else { message += `
Durée totale: ${totalDays} jours - Danger max: ${danger_string}`; } // Détails des étapes avec mode de transport message += `

🗺️ Itinéraire détaillé:`; 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 += `

Étape ${stepNum}: ${step.from} → ${step.to}`; message += `
${modeIcon} Par ${modeName}: ${Math.round(step.distance)} km, ${step.days} jour${step.days > 1 ? 's' : ''} - Danger: ${stepDanger}`; } message += "

"; return message; } }