MAp management and helpers
This commit is contained in:
+129
-52
@@ -5,6 +5,9 @@
|
||||
* Les clics sur la carte affichent les détails du monde dans le chat.
|
||||
*/
|
||||
|
||||
import { searchWorlds } from './travellerMapApi.js';
|
||||
import { TravelDialog } from './travelDialog.js';
|
||||
|
||||
const { ApplicationV2 } = foundry.applications.api;
|
||||
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
|
||||
|
||||
@@ -21,9 +24,12 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
this._sector = sector;
|
||||
this._subsector = subsector;
|
||||
this._handler = null;
|
||||
this._mapHex = null;
|
||||
this._searchTimeout = null;
|
||||
}
|
||||
|
||||
get title() {
|
||||
if (!this._sector) return 'Carte stellaire — Traveller Map';
|
||||
return this._subsector
|
||||
? `Sous-secteur ${this._subsector} — ${this._sector}`
|
||||
: `Secteur ${this._sector}`;
|
||||
@@ -31,10 +37,15 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
|
||||
get _mapUrl() {
|
||||
const base = 'https://travellermap.com';
|
||||
if (!this._sector) return `${base}/?style=mongoose&hideui=1`;
|
||||
if (this._subsector) {
|
||||
return `${base}/?sector=${encodeURIComponent(this._sector)}&subsector=${encodeURIComponent(this._subsector)}&style=mongoose&hideui=1`;
|
||||
}
|
||||
return `${base}/go/${encodeURIComponent(this._sector)}?style=mongoose`;
|
||||
let url = `${base}/go/${encodeURIComponent(this._sector)}?style=mongoose`;
|
||||
if (this._mapHex) {
|
||||
url += `&hex=${this._mapHex}&scale=512`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/* ───── Rendu ───── */
|
||||
@@ -49,10 +60,15 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
_renderHTML() {
|
||||
return `<div class="mgt2-sector-map-outer">
|
||||
<div class="mgt2-sector-map-toolbar">
|
||||
<span class="mgt2-sector-map-label">${this._sector}${this._subsector ? ` — sous-secteur ${this._subsector}` : ''}</span>
|
||||
<div class="mgt2-sector-map-search">
|
||||
<input type="text" class="mgt2-sector-map-input" placeholder="Rechercher un monde…" autocomplete="off">
|
||||
<ul class="mgt2-sector-map-results"></ul>
|
||||
</div>
|
||||
<span class="mgt2-sector-map-label">${this._sector ? `${this._sector}${this._subsector ? ` — ss.${this._subsector}` : ''}` : 'Toute la carte'}</span>
|
||||
<span class="mgt2-sector-map-hint">Cliquez sur un hex pour voir les détails du monde</span>
|
||||
<button type="button" class="mgt2-sector-map-share" title="Partager une image fixe dans le chat"><i class="fas fa-image"></i> Partager</button>
|
||||
<button type="button" class="mgt2-sector-map-sync" title="Ouvrir la carte interactive chez tous les joueurs"><i class="fas fa-users"></i> Synchroniser</button>
|
||||
<button type="button" class="mgt2-sector-map-travel" title="Planifier un voyage"><i class="fas fa-route"></i> Voyage</button>
|
||||
</div>
|
||||
<iframe
|
||||
src="${this._mapUrl}"
|
||||
@@ -71,6 +87,24 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
this.element?.querySelector('.mgt2-sector-map-sync')?.addEventListener('click', () => {
|
||||
this._syncAll();
|
||||
});
|
||||
this.element?.querySelector('.mgt2-sector-map-travel')?.addEventListener('click', () => {
|
||||
this._openTravelDialog();
|
||||
});
|
||||
|
||||
const input = this.element?.querySelector('.mgt2-sector-map-input');
|
||||
const results = this.element?.querySelector('.mgt2-sector-map-results');
|
||||
if (input && results) {
|
||||
input.addEventListener('input', () => {
|
||||
if (this._searchTimeout) clearTimeout(this._searchTimeout);
|
||||
this._searchTimeout = setTimeout(() => this._doSearch(input, results), 300);
|
||||
});
|
||||
input.addEventListener('blur', () => {
|
||||
setTimeout(() => { results.innerHTML = ''; }, 200);
|
||||
});
|
||||
input.addEventListener('focus', () => {
|
||||
if (input.value.trim().length >= 2) this._doSearch(input, results);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ───── Écoute des clics IFRAME ───── */
|
||||
@@ -131,7 +165,7 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
/* ───── Carte de chat ───── */
|
||||
|
||||
static _STARPORT = { A:'Excellent', B:'Bon', C:'Routinier', D:'Médiocre', E:'Frontière', X:'Aucun' };
|
||||
static _SIZE = ['Aucun (Astéroïde)','1 600 km','3 200 km','4 800 km','6 400 km','8 000 km','9 600 km','11 200 km','12 800 km','14 400 km','16 000 km']; // A=10, F=15=Gaz géant (géré à part)
|
||||
static _SIZE = ['Aucun (Astéroïde)','1 600 km','3 200 km','4 800 km','6 400 km','8 000 km','9 600 km','11 200 km','12 800 km','14 400 km','16 000 km'];
|
||||
static _ATMO = [
|
||||
'Aucune (vide)','Trace','Très ténue (polluée)','Très ténue','Ténue (polluée)','Ténue','Standard','Standard (polluée)','Dense','Dense (polluée)',
|
||||
'Exotique','Corrosive','Insidieuse','','',''];
|
||||
@@ -164,7 +198,7 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
return `<span class="uwp-dig" title="${desc}">${val}</span>`;
|
||||
}
|
||||
|
||||
_uwpBreakdown(uwp) {
|
||||
static _uwpBreakdown(uwp) {
|
||||
if (!uwp || uwp.length < 2) return '';
|
||||
const d = SectorMapApp._hexVal;
|
||||
const s = d(uwp[0]), sz = d(uwp[1]), a = d(uwp[2]), h = d(uwp[3]);
|
||||
@@ -237,22 +271,12 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
static _STAR_TYPES = { O:'Bleu (hypergéante)', B:'Bleu-blanc', A:'Blanc', F:'Blanc-jaune', G:'Jaune (naine)', K:'Orange (naine)', M:'Rouge (naine)', L:'Brune', T:'Brune', Y:'Brune' };
|
||||
static _STAR_CLASS = { 'I':'Supergéante', 'II':'Géante brillante', 'III':'Géante', 'IV':'Sous-géante', 'V':'Naine (séquence principale)', 'VI':'Sous-naine', 'VII':'Naine blanche' };
|
||||
|
||||
|
||||
|
||||
static _BASES_HELP = {
|
||||
N:'Base navale', S:'Base scout', W:'Relais', D:'Dépôt naval',
|
||||
T:'Base TAS', C:'Consulat', P:'Base pirate', R:'Base de réparation',
|
||||
K:'Base navale (K.)', X:'Relais Xboat',
|
||||
};
|
||||
|
||||
_tooltipHelp(map, codes) {
|
||||
if (!codes) return '';
|
||||
return codes.split(/[\s,;]+/).map(c => {
|
||||
const desc = map[c.toUpperCase()];
|
||||
return `${c}${desc ? ' — ' + desc : ''}`;
|
||||
}).join(' | ');
|
||||
}
|
||||
|
||||
static _NOBILITY = {
|
||||
B:'Chevalier (Baronet)', C:'Baron', D:'Marquis', E:'Comte', F:'Duc', G:'Archiduc', H:'Empereur',
|
||||
};
|
||||
@@ -263,9 +287,7 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
'5':'Majeure', '6':'Capitale',
|
||||
};
|
||||
|
||||
/* ───── Lignes dépliables (details/summary) ───── */
|
||||
|
||||
_foldRow(label, value, detail, titleAttr) {
|
||||
static _foldRow(label, value, detail, titleAttr) {
|
||||
const valAttr = titleAttr ? ` title="${titleAttr}"` : '';
|
||||
return `<tr><td colspan="2">
|
||||
<details>
|
||||
@@ -275,21 +297,21 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
</td></tr>`;
|
||||
}
|
||||
|
||||
_decodeImportance(ix) {
|
||||
static _decodeImportance(ix) {
|
||||
if (!ix) return '';
|
||||
const m = String(ix).match(/\{?\s*(-?\d+)\s*\}?/);
|
||||
if (!m) return this._foldRow('Importance', ix, '');
|
||||
if (!m) return SectorMapApp._foldRow('Importance', ix, '');
|
||||
const val = m[1];
|
||||
const desc = SectorMapApp._IMPORTANCE[val] || '—';
|
||||
const detail = `<div class="fold-desc">Valeur d’importance économique et stratégique du monde.<br>${val} = ${desc}</div>`;
|
||||
return this._foldRow('Importance', `{ ${val} } ${desc}`, detail);
|
||||
return SectorMapApp._foldRow('Importance', `{ ${val} } ${desc}`, detail);
|
||||
}
|
||||
|
||||
_decodeEconomics(ex) {
|
||||
static _decodeEconomics(ex) {
|
||||
if (!ex) return '';
|
||||
let s = String(ex).replace(/[()\s]/g, '');
|
||||
const m = s.match(/^([\dA-F])([\dA-F])([\dA-F])([+-]\d+)$/i);
|
||||
if (!m) return this._foldRow('Économie', ex, '');
|
||||
if (!m) return SectorMapApp._foldRow('Économie', ex, '');
|
||||
const res = m[1], lab = m[2], inf = m[3], eff = m[4];
|
||||
const detail = `<table class="fold-subtable">
|
||||
<tr><td>Ressources</td><td>${res}</td></tr>
|
||||
@@ -297,13 +319,13 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
<tr><td>Infrastructure</td><td>${inf}</td></tr>
|
||||
<tr><td>Efficacité</td><td>${eff}</td></tr>
|
||||
</table>`;
|
||||
return this._foldRow('Économie', `( ${res} ${lab} ${inf} ${eff} )`, detail);
|
||||
return SectorMapApp._foldRow('Économie', `( ${res} ${lab} ${inf} ${eff} )`, detail);
|
||||
}
|
||||
|
||||
_decodeCulture(cx) {
|
||||
static _decodeCulture(cx) {
|
||||
if (!cx) return '';
|
||||
const s = String(cx).replace(/[\[\]\s]/g, '');
|
||||
if (s.length < 4) return this._foldRow('Culture', cx, '');
|
||||
if (s.length < 4) return SectorMapApp._foldRow('Culture', cx, '');
|
||||
const h = s[0].toUpperCase(), t = s[1].toUpperCase(), p = s[2].toUpperCase(), a = s[3].toUpperCase();
|
||||
const detail = `<table class="fold-subtable">
|
||||
<tr><td>Hétérogénéité</td><td>${h}</td></tr>
|
||||
@@ -311,10 +333,10 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
<tr><td>Progressisme</td><td>${p}</td></tr>
|
||||
<tr><td>Agressivité</td><td>${a}</td></tr>
|
||||
</table>`;
|
||||
return this._foldRow('Culture', `[ ${h} ${t} ${p} ${a} ]`, detail);
|
||||
return SectorMapApp._foldRow('Culture', `[ ${h} ${t} ${p} ${a} ]`, detail);
|
||||
}
|
||||
|
||||
_decodePopulation(uwp, pbg) {
|
||||
static _decodePopulation(uwp, pbg) {
|
||||
if (!uwp || uwp.length < 5) return '';
|
||||
const popUwp = SectorMapApp._hexVal(uwp[4]);
|
||||
if (popUwp < 0) return '';
|
||||
@@ -332,22 +354,22 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
const gas = pbg ? parseInt(pbg[2], 10) : null;
|
||||
const detail = `<div class="fold-desc">Population = multiplicateur (PBG: <b>${popPbg}</b>) × 10<sup>chiffre UWP (${uwp[4]})</sup><br>
|
||||
Ceintures d’astéroïdes : <b>${belts ?? '?'}</b> | Géantes gazeuses : <b>${gas ?? '?'}</b></div>`;
|
||||
return this._foldRow('Population', `${fmtMult} × ${fmtBase} = ${fmtTotal}`, detail);
|
||||
return SectorMapApp._foldRow('Population', `${fmtMult} × ${fmtBase} = ${fmtTotal}`, detail);
|
||||
}
|
||||
|
||||
_decodeNobility(nob) {
|
||||
static _decodeNobility(nob) {
|
||||
if (!nob) return '';
|
||||
const titles = [];
|
||||
for (const ch of nob) {
|
||||
const desc = SectorMapApp._NOBILITY[ch.toUpperCase()];
|
||||
if (desc) titles.push(`${ch} (${desc})`);
|
||||
}
|
||||
if (!titles.length) return this._foldRow('Noblesse', nob, '');
|
||||
if (!titles.length) return SectorMapApp._foldRow('Noblesse', nob, '');
|
||||
const detail = `<div class="fold-desc">Titres de noblesse impériale présents sur ce monde.</div>`;
|
||||
return this._foldRow('Noblesse', titles.join(', '), detail);
|
||||
return SectorMapApp._foldRow('Noblesse', titles.join(', '), detail);
|
||||
}
|
||||
|
||||
_postWorldCard(w) {
|
||||
static _buildWorldCardHTML(w) {
|
||||
const sector = w.Sector || '';
|
||||
const hex = w.Hex || '';
|
||||
const name = w.Name || '—';
|
||||
@@ -363,26 +385,22 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
: zone === 'A' ? 'Ambre'
|
||||
: 'Verte';
|
||||
|
||||
// Construction des lignes pliables
|
||||
const lines = [];
|
||||
|
||||
// UWP
|
||||
const uwpDetail = this._uwpBreakdown(uwp);
|
||||
lines.push(this._foldRow('UWP', `<span class="mono">${uwp}</span>`, uwpDetail));
|
||||
const uwpDetail = SectorMapApp._uwpBreakdown(uwp);
|
||||
lines.push(SectorMapApp._foldRow('UWP', `<span class="mono">${uwp}</span>`, uwpDetail));
|
||||
|
||||
// Champs étendus
|
||||
const ixRow = this._decodeImportance(w.Ix);
|
||||
const ixRow = SectorMapApp._decodeImportance(w.Ix);
|
||||
if (ixRow) lines.push(ixRow);
|
||||
const exRow = this._decodeEconomics(w.Ex);
|
||||
const exRow = SectorMapApp._decodeEconomics(w.Ex);
|
||||
if (exRow) lines.push(exRow);
|
||||
const cxRow = this._decodeCulture(w.Cx);
|
||||
const cxRow = SectorMapApp._decodeCulture(w.Cx);
|
||||
if (cxRow) lines.push(cxRow);
|
||||
const popRow = this._decodePopulation(uwp, pbg);
|
||||
const popRow = SectorMapApp._decodePopulation(uwp, pbg);
|
||||
if (popRow) lines.push(popRow);
|
||||
const nobRow = this._decodeNobility(w.Nobility);
|
||||
const nobRow = SectorMapApp._decodeNobility(w.Nobility);
|
||||
if (nobRow) lines.push(nobRow);
|
||||
|
||||
// Bases
|
||||
if (bases) {
|
||||
const bCodes = bases.split(/[\s,;]+/);
|
||||
const bList = bCodes.map(c => {
|
||||
@@ -390,10 +408,9 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
return `<tr><td class="mono">${c}</td><td>${desc || '—'}</td></tr>`;
|
||||
}).join('');
|
||||
const bDetail = `<table class="fold-subtable"><tbody>${bList}</tbody></table>`;
|
||||
lines.push(this._foldRow('Bases', bases, bDetail));
|
||||
lines.push(SectorMapApp._foldRow('Bases', bases, bDetail));
|
||||
}
|
||||
|
||||
// Remarques
|
||||
if (remarks) {
|
||||
const rCodes = remarks.split(/[\s,;]+/);
|
||||
const rList = rCodes.map(c => {
|
||||
@@ -401,17 +418,15 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
return `<tr><td class="mono">${c}</td><td>${desc || '—'}</td></tr>`;
|
||||
}).join('');
|
||||
const rDetail = `<table class="fold-subtable"><tbody>${rList}</tbody></table>`;
|
||||
lines.push(this._foldRow('Remarques', remarks, rDetail));
|
||||
lines.push(SectorMapApp._foldRow('Remarques', remarks, rDetail));
|
||||
}
|
||||
|
||||
// Allégeance
|
||||
if (allegiance) {
|
||||
const allegFull = w.AllegianceName || '';
|
||||
const aDetail = `<div class="fold-desc">${allegFull || allegiance}</div>`;
|
||||
lines.push(this._foldRow('Allégeance', allegFull ? `${allegiance} (${allegFull})` : allegiance, aDetail));
|
||||
lines.push(SectorMapApp._foldRow('Allégeance', allegFull ? `${allegiance} (${allegFull})` : allegiance, aDetail));
|
||||
}
|
||||
|
||||
// Étoile (les notations comme "G3 V" contiennent un espace entre sous-classe et luminosité)
|
||||
if (stellar) {
|
||||
const sList = [];
|
||||
let remaining = stellar.trim();
|
||||
@@ -424,34 +439,89 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
sList.push(`<tr><td class="mono">${m[1]}${m[2]} ${m[3]}</td><td>${type} · ${cls}</td></tr>`);
|
||||
remaining = remaining.slice(m[0].length).trimStart();
|
||||
} else {
|
||||
// Saute les tokens non reconnus (BD, compagnon, etc.)
|
||||
const next = remaining.indexOf(' ');
|
||||
if (next < 0) break;
|
||||
remaining = remaining.slice(next + 1).trimStart();
|
||||
}
|
||||
}
|
||||
const sDetail = sList.length ? `<table class="fold-subtable"><tbody>${sList.join('')}</tbody></table>` : `<div class="fold-desc">${stellar}</div>`;
|
||||
lines.push(this._foldRow('Étoile', `<span class="mono">${stellar}</span>`, sDetail));
|
||||
lines.push(SectorMapApp._foldRow('Étoile', `<span class="mono">${stellar}</span>`, sDetail));
|
||||
}
|
||||
|
||||
const html = `<section class="mgt2-world-card">
|
||||
return `<section class="mgt2-world-card">
|
||||
<div class="mgt2-world-card-header">
|
||||
<span class="mgt2-world-name">${name}</span>
|
||||
<span class="mgt2-world-hex">${sector} ${hex}</span>
|
||||
<span class="mgt2-world-zone zone-${zone.toLowerCase() || 'g'}">${zoneLabel}</span>
|
||||
</div>
|
||||
<table class="mgt2-world-card-body"><tbody>${lines.join('')}</tbody></table>
|
||||
<div class="mgt2-world-card-actions">
|
||||
<a class="mgt2-world-commerce" data-sector="${this._escapeAttr(sector)}" data-hex="${hex}" data-uwp="${uwp}" data-zone="${zone || 'normal'}" data-name="${this._escapeAttr(name)}">
|
||||
<i class="fas fa-balance-scale"></i> Commerce
|
||||
</a>
|
||||
</div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
static _escapeAttr(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
_postWorldCard(w) {
|
||||
const html = SectorMapApp._buildWorldCardHTML(w);
|
||||
ChatMessage.create({
|
||||
content: html,
|
||||
whisper: [game.user.id],
|
||||
});
|
||||
}
|
||||
|
||||
/* ───── Recherche de monde ───── */
|
||||
|
||||
async _doSearch(input, results) {
|
||||
const query = input.value.trim();
|
||||
if (query.length < 2) { results.innerHTML = ''; return; }
|
||||
const worlds = await searchWorlds(query);
|
||||
if (!worlds.length) {
|
||||
results.innerHTML = '<li class="no-result">Aucun monde trouvé</li>';
|
||||
return;
|
||||
}
|
||||
results.innerHTML = worlds.slice(0, 12).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-uwp">${w.uwp}</span>
|
||||
<span class="world-sector">${w.sector}</span>
|
||||
</li>`
|
||||
).join('');
|
||||
results.querySelectorAll('li[data-sector]').forEach(li => {
|
||||
li.addEventListener('click', () => this._selectWorld(li, input, results));
|
||||
});
|
||||
}
|
||||
|
||||
async _selectWorld(li, input, results) {
|
||||
const sector = li.dataset.sector;
|
||||
const hex = li.dataset.hex;
|
||||
const name = li.dataset.name;
|
||||
results.innerHTML = '';
|
||||
input.value = name;
|
||||
|
||||
this._sector = sector;
|
||||
this._subsector = null;
|
||||
this._mapHex = hex;
|
||||
|
||||
const iframe = this.element?.querySelector('.mgt2-sector-map-frame');
|
||||
if (iframe) iframe.src = this._mapUrl;
|
||||
|
||||
ui.notifications.info(`Carte centrée sur ${name} (${sector} ${hex})`);
|
||||
}
|
||||
|
||||
/* ───── Partage ───── */
|
||||
|
||||
_shareMap() {
|
||||
if (!this._sector) {
|
||||
ui.notifications.warn('Aucun secteur sélectionné — utilisez la recherche pour centrer sur un secteur');
|
||||
return;
|
||||
}
|
||||
const posterUrl = `https://travellermap.com/api/poster?sector=${encodeURIComponent(this._sector)}${this._subsector ? `&subsector=${encodeURIComponent(this._subsector)}` : ''}&style=mongoose&scale=128&dpr=2`;
|
||||
const label = this._subsector
|
||||
? `Sous-secteur ${this._subsector} — ${this._sector}`
|
||||
@@ -484,6 +554,13 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
ui.notifications.info(`Carte synchronisée chez tous les joueurs`);
|
||||
}
|
||||
|
||||
_openTravelDialog() {
|
||||
const existing = Object.values(ui.windows).find(w => w.id === 'mgt2-travel-dialog');
|
||||
if (existing) { existing.bringToTop(); return; }
|
||||
const dialog = new TravelDialog();
|
||||
dialog.render({ force: true });
|
||||
}
|
||||
|
||||
/* ───── Nettoyage ───── */
|
||||
|
||||
close() {
|
||||
|
||||
Reference in New Issue
Block a user