Files
mgt2-compendium-amiral-denisov/scripts/travelDialog.js
T
2026-06-02 00:16:08 +02:00

275 lines
9.3 KiB
JavaScript

import { searchWorlds, calcParsecs } from './travellerMapApi.js';
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
function cleanSectorName(sector) {
return sector?.replace(/\s*\([^)]*\)\s*$/, '').trim() || sector;
}
function worldCoord(sx, sy, hx, hy) {
return { x: (sx - 0) * 32 + (hx - 1), y: (sy - 0) * 40 + (hy - 40) };
}
export class TravelDialog extends HandlebarsApplicationMixin(ApplicationV2) {
static DEFAULT_OPTIONS = {
id: 'mgt2-travel-dialog',
classes: ['mgt2-travel-dialog'],
position: { width: 600, height: 500 },
window: { icon: 'fas fa-route', title: 'Planificateur de voyage', resizable: true, controls: [] },
};
static PARTS = {
main: {
template: `modules/${MODULE_ID}/templates/travel-dialog.hbs`,
},
};
constructor() {
super();
this._fromWorld = null;
this._toWorld = null;
this._lastWorlds = null;
this._lastSegments = null;
}
_onRender(context, options) {
this._wireSearch('from');
this._wireSearch('to');
this.element?.querySelector('[data-action="calculate"]')?.addEventListener('click', () => {
this._calculateRoute();
});
this.element?.querySelector('[data-action="create-journal"]')?.addEventListener('click', () => {
this._createJournal();
});
}
_wireSearch(prefix) {
const input = this.element.querySelector(`[name="travel-${prefix}"]`);
const results = this.element.querySelector(`.travel-${prefix}-results`);
if (!input || !results) return;
let timeout = null;
input.addEventListener('input', () => {
clearTimeout(timeout);
const q = input.value.trim();
if (q.length < 2) { results.innerHTML = ''; return; }
timeout = setTimeout(async () => {
const worlds = await searchWorlds(q);
if (!worlds?.length) {
results.innerHTML = '<li class="no-result">Aucun résultat</li>';
return;
}
results.innerHTML = worlds.slice(0, 10).map(w =>
`<li data-sector="${w.sector}" data-hex="${w.hex}" data-name="${w.name}">
<span class="world-name">${w.name}</span>
<span class="world-sector">${w.sector}</span>
<span class="world-hex">${w.hex}</span>
</li>`
).join('');
}, 300);
});
results.addEventListener('click', (e) => {
const li = e.target.closest('[data-sector]');
if (!li) return;
const data = { sector: li.dataset.sector, hex: li.dataset.hex, name: li.dataset.name };
if (prefix === 'from') this._fromWorld = data;
else this._toWorld = data;
input.value = `${data.name} (${data.sector} ${data.hex})`;
results.innerHTML = '';
});
input.addEventListener('blur', () => {
setTimeout(() => { results.innerHTML = ''; }, 200);
});
}
async _calculateRoute() {
if (!this._fromWorld || !this._toWorld) {
ui.notifications.warn('Veuillez sélectionner un monde de départ et un monde d\'arrivée');
return;
}
const jumpEl = this.element.querySelector('[name="travel-jump"]');
const jump = parseInt(jumpEl?.value, 10) || 2;
const resultsEl = this.element.querySelector('.travel-results');
if (!resultsEl) return;
resultsEl.innerHTML = '<div class="travel-loading"><i class="fas fa-spinner fa-spin"></i> Calcul de l\'itinéraire…</div>';
const startSector = cleanSectorName(this._fromWorld.sector);
const endSector = cleanSectorName(this._toWorld.sector);
const startLoc = `${startSector} ${this._fromWorld.hex}`;
const endLoc = `${endSector} ${this._toWorld.hex}`;
try {
const resp = await fetch(
`https://travellermap.com/api/route?start=${encodeURIComponent(startLoc)}&end=${encodeURIComponent(endLoc)}&jump=${jump}`
);
if (!resp.ok) {
if (resp.status === 404) {
const text = await resp.text().catch(() => 'Aucun itinéraire trouvé');
resultsEl.innerHTML = `<div class="travel-error">${this._escapeHtml(text)}</div>`;
} else {
resultsEl.innerHTML = `<div class="travel-error">Erreur API (${resp.status})</div>`;
}
return;
}
const data = await resp.json();
if (!Array.isArray(data) || data.length < 2) {
resultsEl.innerHTML = '<div class="travel-error">Aucun itinéraire trouvé</div>';
return;
}
this._displayRoute(data, resultsEl);
} catch (err) {
console.error('TravelDialog | Erreur:', err);
resultsEl.innerHTML = `<div class="travel-error">Erreur : ${this._escapeHtml(err.message)}</div>`;
}
}
_displayRoute(worlds, resultsEl) {
this._lastWorlds = worlds;
const segments = [];
let totalParsecs = 0;
for (let i = 0; i < worlds.length - 1; i++) {
const a = worlds[i];
const b = worlds[i + 1];
const fromC = worldCoord(a.SectorX, a.SectorY, a.HexX, a.HexY);
const toC = worldCoord(b.SectorX, b.SectorY, b.HexX, b.HexY);
const dist = calcParsecs(fromC, toC);
totalParsecs += dist;
segments.push({ from: a, to: b, dist });
}
this._lastSegments = segments;
let html = `<div class="travel-route-summary">
<i class="fas fa-route"></i>
<strong>${segments.length}</strong> saut${segments.length > 1 ? 's' : ''}
· <strong>${totalParsecs}</strong> parsecs
</div>`;
html += `<div class="travel-route-duration">
<i class="fas fa-clock"></i> Durée estimée : <strong>${segments.length}</strong> semaine${segments.length > 1 ? 's' : ''}
</div>`;
html += '<ol class="travel-jump-list">';
segments.forEach((seg) => {
const f = seg.from;
const t = seg.to;
html += `<li>
<div class="jump-segment">
<div class="jump-world jump-from">
<span class="jump-world-name">${f.Name || '?'}</span>
<span class="jump-world-detail">${f.Sector} ${f.Hex || ''}</span>
</div>
<div class="jump-arrow"><i class="fas fa-long-arrow-alt-right"></i></div>
<div class="jump-world jump-to">
<span class="jump-world-name">${t.Name || '?'}</span>
<span class="jump-world-detail">${t.Sector} ${t.Hex || ''}</span>
</div>
<div class="jump-distance">Saut-${seg.dist}</div>
</div>
</li>`;
});
html += '</ol>';
resultsEl.innerHTML = html;
const journalBtn = this.element?.querySelector('.travel-journal-actions');
if (journalBtn) journalBtn.style.display = 'block';
}
async _createJournal() {
const worlds = this._lastWorlds;
const segments = this._lastSegments;
if (!worlds?.length || !segments?.length) {
ui.notifications.warn('Calculez d\'abord un itinéraire');
return;
}
const from = worlds[0];
const to = worlds[worlds.length - 1];
const totalParsecs = segments.reduce((s, seg) => s + seg.dist, 0);
const totalJumps = segments.length;
const jumpRating = this.element?.querySelector('[name="travel-jump"]')?.value || '?';
const lines = [];
lines.push(`<h2>Journal de voyage</h2>`);
lines.push(`<p><strong>Départ :</strong> ${this._worldLink(from)}</p>`);
lines.push(`<p><strong>Destination :</strong> ${this._worldLink(to)}</p>`);
lines.push(`<p><strong>Moteur :</strong> J-${jumpRating}</p>`);
lines.push(`<hr>`);
lines.push(`<h3>Itinéraire (${totalJumps} saut${totalJumps > 1 ? 's' : ''}, ${totalParsecs} pc)</h3>`);
lines.push(`<table><thead><tr><th>#</th><th>Départ</th><th>→</th><th>Arrivée</th><th>Distance</th></tr></thead><tbody>`);
segments.forEach((seg, i) => {
const f = seg.from;
const t = seg.to;
lines.push(`<tr>
<td>${i + 1}</td>
<td>${this._worldLink(f)}</td>
<td>→</td>
<td>${this._worldLink(t)}</td>
<td>${seg.dist} pc</td>
</tr>`);
});
lines.push(`</tbody></table>`);
lines.push(`<hr>`);
lines.push(`<h3>Mondes visités</h3>`);
lines.push(`<ul>`);
const seen = new Set();
for (const w of worlds) {
const key = `${w.Sector}|${w.Hex}`;
if (seen.has(key)) continue;
seen.add(key);
lines.push(`<li>${this._worldLink(w)}${w.UWP ? ` — UWP: ${w.UWP}` : ''}</li>`);
}
lines.push(`</ul>`);
const content = lines.join('\n');
try {
const journal = await JournalEntry.create({
name: `Voyage : ${from.Name || '?'}${to.Name || '?'}`,
pages: [{
name: 'Itinéraire',
type: 'text',
text: { content, format: 2 },
}],
});
if (journal) {
ui.notifications.info(`Journal créé : ${journal.name}`);
journal.sheet?.render(true);
}
} catch (err) {
console.error('TravelDialog | Erreur création journal:', err);
ui.notifications.error('Erreur lors de la création du journal');
}
}
_worldLink(w) {
const name = w.Name || '?';
const sector = w.Sector || '';
const hex = w.Hex || '';
return `<a class="mgt2-world-link" data-sector="${this._escapeAttr(sector)}" data-hex="${this._escapeAttr(hex)}">${name}</a> <em>(${sector} ${hex})</em>`;
}
_escapeAttr(str) {
if (!str) return '';
return str.replace(/"/g, '&quot;').replace(/&/g, '&amp;');
}
_escapeHtml(str) {
if (!str) return '';
const d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
}