497 lines
20 KiB
JavaScript
497 lines
20 KiB
JavaScript
/**
|
||
* MGT2 – SectorMapApp
|
||
*
|
||
* Application interactive affichant une carte Traveller Map dans un IFRAME.
|
||
* Les clics sur la carte affichent les détails du monde dans le chat.
|
||
*/
|
||
|
||
const { ApplicationV2 } = foundry.applications.api;
|
||
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
|
||
|
||
export class SectorMapApp extends ApplicationV2 {
|
||
static DEFAULT_OPTIONS = {
|
||
id: 'mgt2-sector-map',
|
||
classes: ['mgt2-sector-map'],
|
||
position: { width: 960, height: 720 },
|
||
window: { icon: 'fas fa-map', resizable: true, controls: [] },
|
||
};
|
||
|
||
constructor(sector, subsector) {
|
||
super();
|
||
this._sector = sector;
|
||
this._subsector = subsector;
|
||
this._handler = null;
|
||
}
|
||
|
||
get title() {
|
||
return this._subsector
|
||
? `Sous-secteur ${this._subsector} — ${this._sector}`
|
||
: `Secteur ${this._sector}`;
|
||
}
|
||
|
||
get _mapUrl() {
|
||
const base = 'https://travellermap.com';
|
||
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`;
|
||
}
|
||
|
||
/* ───── Rendu ───── */
|
||
|
||
_replaceHTML(result, config) {
|
||
const content = this.element?.querySelector('.window-content');
|
||
if (!content) return;
|
||
const html = typeof result === 'string' ? result : this._lastHTML;
|
||
content.innerHTML = typeof html === 'string' ? html : '';
|
||
}
|
||
|
||
_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>
|
||
<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>
|
||
</div>
|
||
<iframe
|
||
src="${this._mapUrl}"
|
||
class="mgt2-sector-map-frame"
|
||
allow="clipboard-write"
|
||
referrerpolicy="no-referrer">
|
||
</iframe>
|
||
</div>`;
|
||
}
|
||
|
||
async _onRender(context, options) {
|
||
this._listen();
|
||
this.element?.querySelector('.mgt2-sector-map-share')?.addEventListener('click', () => {
|
||
this._shareMap();
|
||
});
|
||
this.element?.querySelector('.mgt2-sector-map-sync')?.addEventListener('click', () => {
|
||
this._syncAll();
|
||
});
|
||
}
|
||
|
||
/* ───── Écoute des clics IFRAME ───── */
|
||
|
||
_listen() {
|
||
if (this._handler) return;
|
||
this._handler = (event) => {
|
||
// Accept messages from travellermap or any origin (for testing)
|
||
const d = event.data || {};
|
||
const wx = d.x ?? d.location?.x;
|
||
const wy = d.y ?? d.location?.y;
|
||
if (wx == null || wy == null) return;
|
||
const x = Number(wx);
|
||
const y = Number(wy);
|
||
if (isNaN(x) || isNaN(y)) return;
|
||
console.log('SectorMapApp | click at', x, y, 'from', event.origin);
|
||
this._onMapClick({x, y}).catch(err => {
|
||
console.error('SectorMapApp | click handler failed:', err);
|
||
});
|
||
};
|
||
window.addEventListener('message', this._handler);
|
||
}
|
||
|
||
async _onMapClick(loc) {
|
||
const wx = loc?.x;
|
||
const wy = loc?.y;
|
||
if (wx == null || wy == null) return;
|
||
|
||
const coordResp = await fetch(
|
||
`https://travellermap.com/api/coordinates?x=${wx}&y=${wy}`
|
||
);
|
||
if (!coordResp.ok) { console.error('SectorMapApp | /api/coordinates failed', coordResp.status); return; }
|
||
const coord = await coordResp.json();
|
||
const { sx, sy, hx, hy } = coord;
|
||
if (sx == null || hx == null || hy == null) { console.error('SectorMapApp | no sx/hx/hy in', coord); return; }
|
||
|
||
const metaResp = await fetch(
|
||
`https://travellermap.com/api/metadata?sx=${sx}&sy=${sy}`
|
||
);
|
||
if (!metaResp.ok) { console.error('SectorMapApp | /api/metadata failed', metaResp.status); return; }
|
||
const meta = await metaResp.json();
|
||
const sectorName = meta.Names?.[0]?.Text;
|
||
if (!sectorName) { console.error('SectorMapApp | no Names[0].Text in metadata', meta); return; }
|
||
|
||
const hex = String(hx).padStart(2, '0') + String(hy).padStart(2, '0');
|
||
const resp = await fetch(
|
||
`https://travellermap.com/data/${encodeURIComponent(sectorName)}/${hex}`
|
||
);
|
||
if (!resp.ok) { console.error('SectorMapApp | /data failed', resp.status, sectorName, hex); return; }
|
||
|
||
const data = await resp.json();
|
||
const world = data.Worlds?.[0];
|
||
if (!world) { console.error('SectorMapApp | no Worlds in data', data); return; }
|
||
|
||
this._postWorldCard(world);
|
||
}
|
||
|
||
/* ───── 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 _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','','',''];
|
||
static _HYDRO = [ '0–5% (désert)','6–15%','16–25%','26–35%','36–45%','46–55%','56–65%','66–75%','76–85%','86–95%','96–100%' ];
|
||
static _POP = ['','Dizaines','Centaines','Milliers','Dizaines de milliers','Centaines de milliers','Millions','Dizaines de millions','Centaines de millions','Milliards','Dizaines de milliards','','','','','',''];
|
||
static _GOV = [
|
||
'Aucun','Compagnie / Corporation','Démocratie participative','Oligarchie auto-perpétuée',
|
||
'Démocratie représentative','Technocratie féodale','Gouvernement captif / Colonie',
|
||
'Balkanisation','Bureaucratie de service civil','Bureaucratie impersonnelle',
|
||
'Dictature charismatique','Dictature non-charismatique','Oligarchie charismatique',
|
||
'Dictature religieuse','Oligarchie religieuse','Gouvernement tribal'];
|
||
static _LAW = [
|
||
'Aucune', 'Armes de poing, explosifs, poison','Armes à énergie portatives','Mitrailleuses, armes auto',
|
||
'Armes d\'assaut légères, PM','Armes de poing individuelles','Toutes les armes à feu sauf neutralisateur',
|
||
'Fusils, neutralisateur','Armes blanches, neutralisateur','Armes hors du domicile','Armes interdites',
|
||
'Contrôle rigide','Aucune arme','Contrôle militariste sévère'];
|
||
static _TL = [
|
||
'Âge de pierre','Âge du bronze/fer','Médiéval','Grandes découvertes','Révolution industrielle',
|
||
'Production mécanisée','Ère nucléaire','Pré-stellaire (ère de l\'information)','Propulsion à saut (1re gen)',
|
||
'Propulsion à saut-2','Propulsion à saut-3','Propulsion à saut-4','Propulsion à saut-5',
|
||
'Propulsion à saut-6','Transporteur','Moyenne stellaire'];
|
||
|
||
static _hexVal(ch) {
|
||
const n = parseInt(ch, 36);
|
||
if (isNaN(n)) return -1;
|
||
return n;
|
||
}
|
||
|
||
static _uwpDigit(desc, val) {
|
||
return `<span class="uwp-dig" title="${desc}">${val}</span>`;
|
||
}
|
||
|
||
_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]);
|
||
const p = d(uwp[4]), g = d(uwp[5]), l = d(uwp[6]);
|
||
const t = uwp.length > 8 ? d(uwp[8]) : -1;
|
||
|
||
const lines = [];
|
||
|
||
const starport = SectorMapApp._STARPORT[uwp[0]];
|
||
lines.push(`<tr><td>${uwp[0]}</td><td>Starport</td><td>${starport ?? '—'}</td></tr>`);
|
||
|
||
if (uwp[1] === 'F' || uwp[1] === 'f') {
|
||
lines.push(`<tr><td>${uwp[1]}</td><td>Taille</td><td>Gaz géant</td></tr>`);
|
||
} else if (sz >= 0 && sz <= 10) {
|
||
const km = SectorMapApp._SIZE[sz];
|
||
const grav = sz === 0 ? '0g' : (sz < 10 ? `0.${sz}g` : '1.0g+');
|
||
lines.push(`<tr><td>${uwp[1]}</td><td>Taille</td><td>${km} (${grav})</td></tr>`);
|
||
}
|
||
|
||
if (a >= 0 && a <= 15) {
|
||
const atmo = SectorMapApp._ATMO[a] ?? '—';
|
||
lines.push(`<tr><td>${uwp[2]}</td><td>Atmosphère</td><td>${atmo}</td></tr>`);
|
||
}
|
||
|
||
if (h >= 0 && h <= 10) {
|
||
lines.push(`<tr><td>${uwp[3]}</td><td>Hydrosphère</td><td>${SectorMapApp._HYDRO[h]}</td></tr>`);
|
||
}
|
||
|
||
if (p >= 0 && p <= 15) {
|
||
lines.push(`<tr><td>${uwp[4]}</td><td>Population</td><td>${SectorMapApp._POP[p] ?? '—'}</td></tr>`);
|
||
}
|
||
|
||
if (g >= 0 && g <= 15) {
|
||
lines.push(`<tr><td>${uwp[5]}</td><td>Gouvernement</td><td>${SectorMapApp._GOV[g] ?? '—'}</td></tr>`);
|
||
}
|
||
|
||
if (l >= 0 && l <= 15) {
|
||
lines.push(`<tr><td>${uwp[6]}</td><td>Niveau légal</td><td>${SectorMapApp._LAW[l] ?? '—'}</td></tr>`);
|
||
}
|
||
|
||
if (t >= 0 && t <= 15) {
|
||
lines.push(`<tr><td>${uwp[8]}</td><td>Technologie</td><td>${SectorMapApp._TL[t] ?? '—'}</td></tr>`);
|
||
}
|
||
|
||
return `<table class="uwp-breakdown"><tbody>${lines.join('')}</tbody></table>`;
|
||
}
|
||
|
||
static _REMARKS_HELP = {
|
||
AB:'Anneau (ceinture)', AG:'Agricole', AN:'Site ancien', AS:'Astéroïde',
|
||
BA:'Bande astéroïdale', CP:'Sous-secteur capitale', CS:'Colonie',
|
||
CX:'Chasseur (Croiseur)', CY:'Colonie', DA:'Déchu', DE:'Désertique',
|
||
DI:'Interdit (Diebar)', FL:'Fluides Lo', FO:'Interdit (Forbidden)',
|
||
FR:'Gelé (Frozen)', GA:'Jardin (Garden)', HE:'Helios', HI:'Haute population',
|
||
HT:'Haute technologie', IC:'Mondes gelés (Ice)', IN:'Industrialisé',
|
||
LI:'Faible population', LO:'Faible population (Low)', LT:'Basse technologie (Low Tech)',
|
||
MI:'Militaire', MR:'Mine (ressources)', NA:'Non-agricole',
|
||
NI:'Non-industrialisé', OC:'Océanique', OX:'Oxydant',
|
||
PA:'Pré-agricole (Pre-Agricultural)', PH:'Phosphore',
|
||
PO:'Pauvre (Poor)', PR:'Pré-industriel (Pre-Industrial)',
|
||
PX:'Prisonnier (exil)', PZ:'Puzzle (énigmatique)',
|
||
RE:'Religieux (Religious)', RI:'Riche (Rich)',
|
||
SA:'Bande d\'astéroïdes (Satellite)', SC:'Sainte (colonie)',
|
||
SL:'Esclavage (Slave)', SO:'Soleil (Sol)', SP:'Désert (Despoiled)',
|
||
SR:'Réserve (Reserve)', ST:'Base stellaire', SU:'Secteur capitale',
|
||
TR:'Traces (Trace)', TU:'Tucannides', TZ:'Mondes Tz',
|
||
UN:'Inhabité (Uninhabited)', VA:'Vide (Vacuum)',
|
||
WA:'Monde aquatique (Water)', WT:'Monde d\'eau (Watery)',
|
||
};
|
||
|
||
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',
|
||
};
|
||
|
||
static _IMPORTANCE = {
|
||
'-5':'Très mineur', '-4':'Mineur', '-3':'Mineur', '-2':'Très secondaire', '-1':'Secondaire',
|
||
'0':'Ordinaire', '1':'Important', '2':'Important', '3':'Très important', '4':'Majeure',
|
||
'5':'Majeure', '6':'Capitale',
|
||
};
|
||
|
||
/* ───── Lignes dépliables (details/summary) ───── */
|
||
|
||
_foldRow(label, value, detail, titleAttr) {
|
||
const valAttr = titleAttr ? ` title="${titleAttr}"` : '';
|
||
return `<tr><td colspan="2">
|
||
<details>
|
||
<summary><span class="fold-label">${label}</span><span class="fold-value"${valAttr}>${value}</span></summary>
|
||
<div class="fold-content">${detail}</div>
|
||
</details>
|
||
</td></tr>`;
|
||
}
|
||
|
||
_decodeImportance(ix) {
|
||
if (!ix) return '';
|
||
const m = String(ix).match(/\{?\s*(-?\d+)\s*\}?/);
|
||
if (!m) return this._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);
|
||
}
|
||
|
||
_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, '');
|
||
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>
|
||
<tr><td>Main-d’œuvre</td><td>${lab}</td></tr>
|
||
<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);
|
||
}
|
||
|
||
_decodeCulture(cx) {
|
||
if (!cx) return '';
|
||
const s = String(cx).replace(/[\[\]\s]/g, '');
|
||
if (s.length < 4) return this._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>
|
||
<tr><td>Traditionalisme</td><td>${t}</td></tr>
|
||
<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);
|
||
}
|
||
|
||
_decodePopulation(uwp, pbg) {
|
||
if (!uwp || uwp.length < 5) return '';
|
||
const popUwp = SectorMapApp._hexVal(uwp[4]);
|
||
if (popUwp < 0) return '';
|
||
const popPbg = pbg ? parseInt(pbg[0], 10) : null;
|
||
const multiplier = popPbg != null && !isNaN(popPbg) ? popPbg : 1;
|
||
const base = Math.pow(10, popUwp);
|
||
const total = multiplier * base;
|
||
const fmtBase = `10<sup>${popUwp}</sup>`;
|
||
const fmtMult = multiplier;
|
||
const fmtTotal = total >= 1e9 ? `${(total / 1e9).toFixed(1)} milliards`
|
||
: total >= 1e6 ? `${(total / 1e6).toFixed(1)} millions`
|
||
: total >= 1e3 ? `${(total / 1e3).toFixed(0)} 000`
|
||
: String(total);
|
||
const belts = pbg ? parseInt(pbg[1], 10) : null;
|
||
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);
|
||
}
|
||
|
||
_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, '');
|
||
const detail = `<div class="fold-desc">Titres de noblesse impériale présents sur ce monde.</div>`;
|
||
return this._foldRow('Noblesse', titles.join(', '), detail);
|
||
}
|
||
|
||
_postWorldCard(w) {
|
||
const sector = w.Sector || '';
|
||
const hex = w.Hex || '';
|
||
const name = w.Name || '—';
|
||
const uwp = w.UWP || '???????-?';
|
||
const bases = w.Bases || '';
|
||
const remarks = w.Remarks || '';
|
||
const allegiance = w.Allegiance || '';
|
||
const stellar = w.Stellar || '';
|
||
const zone = w.Zone || '';
|
||
const pbg = w.PBG || '';
|
||
|
||
const zoneLabel = zone === 'R' ? 'Rouge'
|
||
: 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));
|
||
|
||
// Champs étendus
|
||
const ixRow = this._decodeImportance(w.Ix);
|
||
if (ixRow) lines.push(ixRow);
|
||
const exRow = this._decodeEconomics(w.Ex);
|
||
if (exRow) lines.push(exRow);
|
||
const cxRow = this._decodeCulture(w.Cx);
|
||
if (cxRow) lines.push(cxRow);
|
||
const popRow = this._decodePopulation(uwp, pbg);
|
||
if (popRow) lines.push(popRow);
|
||
const nobRow = this._decodeNobility(w.Nobility);
|
||
if (nobRow) lines.push(nobRow);
|
||
|
||
// Bases
|
||
if (bases) {
|
||
const bCodes = bases.split(/[\s,;]+/);
|
||
const bList = bCodes.map(c => {
|
||
const desc = SectorMapApp._BASES_HELP[c.toUpperCase()];
|
||
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));
|
||
}
|
||
|
||
// Remarques
|
||
if (remarks) {
|
||
const rCodes = remarks.split(/[\s,;]+/);
|
||
const rList = rCodes.map(c => {
|
||
const desc = SectorMapApp._REMARKS_HELP[c.toUpperCase()];
|
||
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));
|
||
}
|
||
|
||
// 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));
|
||
}
|
||
|
||
// Étoile (les notations comme "G3 V" contiennent un espace entre sous-classe et luminosité)
|
||
if (stellar) {
|
||
const sList = [];
|
||
let remaining = stellar.trim();
|
||
const reStar = /^([OBAFGKMLTY])(\d)\s*(VII|VI|V|IV|III|II|I)\s*/i;
|
||
while (remaining) {
|
||
const m = remaining.match(reStar);
|
||
if (m) {
|
||
const type = SectorMapApp._STAR_TYPES[m[1].toUpperCase()] || m[1];
|
||
const cls = SectorMapApp._STAR_CLASS[m[3]] || m[3];
|
||
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));
|
||
}
|
||
|
||
const html = `<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>
|
||
</section>`;
|
||
|
||
ChatMessage.create({
|
||
content: html,
|
||
whisper: [game.user.id],
|
||
});
|
||
}
|
||
|
||
/* ───── Partage ───── */
|
||
|
||
_shareMap() {
|
||
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}`
|
||
: `Secteur ${this._sector}`;
|
||
|
||
const html = `<section class="mgt2-shared-map">
|
||
<div class="mgt2-world-card-header">
|
||
<span class="mgt2-world-name">${label}</span>
|
||
</div>
|
||
<div class="mgt2-shared-map-image">
|
||
<img src="${posterUrl}" alt="${label}">
|
||
</div>
|
||
<div class="mgt2-shared-map-footer">
|
||
<a href="https://travellermap.com/go/${encodeURIComponent(this._sector)}" target="_blank" rel="noopener">
|
||
Ouvrir sur Traveller Map <i class="fas fa-external-link-alt"></i>
|
||
</a>
|
||
</div>
|
||
</section>`;
|
||
|
||
ChatMessage.create({ content: html, rollMode: 'public' });
|
||
ui.notifications.info(`Carte partagée avec les joueurs`);
|
||
}
|
||
|
||
_syncAll() {
|
||
game.socket.emit(`module.${MODULE_ID}`, {
|
||
type: 'sectorMapSync',
|
||
sector: this._sector,
|
||
subsector: this._subsector,
|
||
});
|
||
ui.notifications.info(`Carte synchronisée chez tous les joueurs`);
|
||
}
|
||
|
||
/* ───── Nettoyage ───── */
|
||
|
||
close() {
|
||
if (this._handler) {
|
||
window.removeEventListener('message', this._handler);
|
||
this._handler = null;
|
||
}
|
||
return super.close();
|
||
}
|
||
}
|