/** * MGT2 Commerce – Client API Traveller Map * * Documentation : https://travellermap.com/doc/api * * Deux fonctions exportées : * searchWorlds(query) → [{name, sector, hex, uwp, zone}] * fetchWorldDetail(sector, hex) → {uwp, zone} (zone: 'normal'|'amber'|'red') */ const BASE_URL = 'https://travellermap.com'; /** Convertit la zone Traveller Map ('A', 'R', '') vers notre convention. */ function normalizeZone(z) { if (z === 'A') return 'amber'; if (z === 'R') return 'red'; return 'normal'; } /** * Recherche des mondes par nom via l'API Traveller Map. * * @param {string} query Nom partiel ou complet (ex. "Regina") * @returns {Promise>} * Tableau de résultats (uniquement les mondes, pas secteurs/subsecteurs). * Vide si erreur réseau. */ export async function searchWorlds(query) { if (!query || query.trim().length < 2) return []; const url = `${BASE_URL}/api/search?q=${encodeURIComponent(query.trim())}`; let data; try { const resp = await fetch(url); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); data = await resp.json(); } catch (err) { console.warn('MGT2 Commerce | Traveller Map search error:', err); return []; } const items = data?.Results?.Items ?? []; return items .filter(item => item.World) // garder uniquement les mondes .map(item => { const w = item.World; // Formatter le code hex sur 4 chiffres (HexX→XX, HexY→YY) const hex = String(w.HexX).padStart(2, '0') + String(w.HexY).padStart(2, '0'); return { name: w.Name, sector: w.Sector, hex, uwp: w.Uwp ?? '', }; }); } /** * Récupère les coordonnées absolues d'un monde en parsecs. * Utilise l'endpoint /api/coordinates. * * @param {string} sector Nom du secteur (ex. "Spinward Marches") * @param {string} hex Code hex 4 chiffres (ex. "1910") * @returns {Promise<{x:number, y:number}|null>} */ export async function fetchWorldCoordinates(sector, hex) { const url = `${BASE_URL}/api/coordinates?sector=${encodeURIComponent(sector)}&hex=${encodeURIComponent(hex)}`; try { const resp = await fetch(url); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const data = await resp.json(); if (data?.x == null || data?.y == null) return null; return { x: data.x, y: data.y }; } catch (err) { console.warn('MGT2 Commerce | Traveller Map coordinates error:', err); return null; } } /** * Calcule la distance en parsecs entre deux mondes à partir de leurs * coordonnées absolues (retournées par fetchWorldCoordinates). * Utilise la formule de distance en coordonnées cubiques pour grille hexagonale. * * Dans le système Traveller Map : les colonnes impaires (hx impair) sont * décalées vers le bas. En coordonnées API, hx impair ↔ x pair. * Conversion offset→cube : q=x, r=y−⌈x/2⌉, s=−q−r. * Distance = max(|Δq|, |Δr|, |Δs|). * * @param {{x:number,y:number}} c1 * @param {{x:number,y:number}} c2 * @returns {number} Distance en parsecs (entier, minimum 1) */ export function calcParsecs(c1, c2) { const q1 = c1.x, r1 = c1.y - Math.ceil(c1.x / 2); const q2 = c2.x, r2 = c2.y - Math.ceil(c2.x / 2); const dq = q2 - q1, dr = r2 - r1, ds = -dq - dr; return Math.max(1, Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds))); } /** * Récupère les détails d'un monde pour obtenir la zone de voyage. * Utilise l'endpoint /data/{sector}/{hex} (jump=0). * * @param {string} sector Nom du secteur (ex. "Spinward Marches") * @param {string} hex Code hex 4 chiffres (ex. "1910") * @returns {Promise<{uwp:string, zone:string}>} */ export async function fetchWorldDetail(sector, hex) { const url = `${BASE_URL}/data/${encodeURIComponent(sector)}/${hex}`; let data; try { const resp = await fetch(url); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); data = await resp.json(); } catch (err) { console.warn('MGT2 Commerce | Traveller Map detail error:', err); return null; } const world = data?.Worlds?.[0]; if (!world) return null; return { uwp: world.UWP ?? '', zone: normalizeZone(world.Zone ?? ''), }; }