340 lines
16 KiB
JavaScript
340 lines
16 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|
|
}
|