AJout gestion map
This commit is contained in:
@@ -0,0 +1,496 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user