feat: batch NPC import, ally/enemy gen, map sync, UI shortcuts
- batch NPC creator: import images from dir → actors (dedup, folder) - ally/enemy generator: data tables, D66 specials, tests (39 pass) - sector map sync: DialogV2 player view, socket v13/v14, sync click pos - actor/scene directory: shortcut buttons (Commerce, PNJ, Map, Sync vue) - closeImagePopout: GM closes player portraits via socket - canvasSync: force same canvas view on all players - FilePicker: v14 folder picker support with v13 fallback - cleanup: debug logs, template dedup, return guards
This commit is contained in:
+114
-40
@@ -19,12 +19,15 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
window: { icon: 'fas fa-map', resizable: true, controls: [] },
|
||||
};
|
||||
|
||||
constructor(sector, subsector) {
|
||||
constructor(sector, subsector, { readOnly = false } = {}) {
|
||||
super();
|
||||
this._sector = sector;
|
||||
this._subsector = subsector;
|
||||
this._readOnly = readOnly;
|
||||
this._handler = null;
|
||||
this._mapHex = null;
|
||||
this._mapCenter = null;
|
||||
this._mapScale = null;
|
||||
this._searchTimeout = null;
|
||||
}
|
||||
|
||||
@@ -37,13 +40,16 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
|
||||
get _mapUrl() {
|
||||
const base = 'https://travellermap.com';
|
||||
const cb = this._readOnly ? '' : '&callback=postmessage';
|
||||
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}/?sector=${encodeURIComponent(this._sector)}&subsector=${encodeURIComponent(this._subsector)}&style=mongoose&hideui=1${cb}`;
|
||||
}
|
||||
let url = `${base}/go/${encodeURIComponent(this._sector)}?style=mongoose`;
|
||||
let url = `${base}/?sector=${encodeURIComponent(this._sector)}&style=mongoose&hideui=1${cb}`;
|
||||
if (this._mapHex) {
|
||||
url += `&hex=${this._mapHex}&scale=512`;
|
||||
} else if (this._mapCenter && this._mapScale) {
|
||||
url += `¢er=${this._mapCenter}&scale=${this._mapScale}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
@@ -58,6 +64,20 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
}
|
||||
|
||||
_renderHTML() {
|
||||
if (this._readOnly) {
|
||||
return `<div class="mgt2-sector-map-outer">
|
||||
<div class="mgt2-sector-map-toolbar mgt2-sector-map-toolbar--ro">
|
||||
<span class="mgt2-sector-map-label">${SectorMapApp._escapeAttr(this.title)}</span>
|
||||
<span class="mgt2-sector-map-hint">Carte partagée par le MJ</span>
|
||||
</div>
|
||||
<iframe
|
||||
src="${this._mapUrl}"
|
||||
class="mgt2-sector-map-frame"
|
||||
allow="clipboard-write"
|
||||
referrerpolicy="no-referrer">
|
||||
</iframe>
|
||||
</div>`;
|
||||
}
|
||||
return `<div class="mgt2-sector-map-outer">
|
||||
<div class="mgt2-sector-map-toolbar">
|
||||
<div class="mgt2-sector-map-search">
|
||||
@@ -66,8 +86,7 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
</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-sync" disabled title="Cliquez d'abord sur un hex pour définir la position à synchroniser"><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
|
||||
@@ -80,10 +99,11 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
}
|
||||
|
||||
async _onRender(context, options) {
|
||||
if (this._readOnly) {
|
||||
this._listen();
|
||||
return;
|
||||
}
|
||||
this._listen();
|
||||
this.element?.querySelector('.mgt2-sector-map-share')?.addEventListener('click', () => {
|
||||
this._shareMap();
|
||||
});
|
||||
this.element?.querySelector('.mgt2-sector-map-sync')?.addEventListener('click', () => {
|
||||
this._syncAll();
|
||||
});
|
||||
@@ -112,16 +132,24 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
_listen() {
|
||||
if (this._handler) return;
|
||||
this._handler = (event) => {
|
||||
// Accept messages from travellermap or any origin (for testing)
|
||||
const d = event.data || {};
|
||||
if (d.type === 'viewport') {
|
||||
this._sector = d.sector || this._sector;
|
||||
this._subsector = d.subsector || this._subsector;
|
||||
this._mapHex = d.hex || null;
|
||||
this._mapCenter = (d.x != null && d.y != null) ? `${d.x},${d.y}` : null;
|
||||
this._mapScale = d.scale || null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Click on map
|
||||
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 => {
|
||||
this._onMapClick({ x, y }).catch(err => {
|
||||
console.error('SectorMapApp | click handler failed:', err);
|
||||
});
|
||||
};
|
||||
@@ -149,6 +177,13 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
const sectorName = meta.Names?.[0]?.Text;
|
||||
if (!sectorName) { console.error('SectorMapApp | no Names[0].Text in metadata', meta); return; }
|
||||
|
||||
// Store click position for sync
|
||||
this._sector = sectorName;
|
||||
this._subsector = null;
|
||||
this._mapHex = String(hx).padStart(2, '0') + String(hy).padStart(2, '0');
|
||||
this._mapCenter = `${wx},${wy}`;
|
||||
this._mapScale = loc.scale || 512;
|
||||
|
||||
const hex = String(hx).padStart(2, '0') + String(hy).padStart(2, '0');
|
||||
const resp = await fetch(
|
||||
`https://travellermap.com/data/${encodeURIComponent(sectorName)}/${hex}`
|
||||
@@ -159,9 +194,19 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
const world = data.Worlds?.[0];
|
||||
if (!world) { console.error('SectorMapApp | no Worlds in data', data); return; }
|
||||
|
||||
this._enableSyncButton(world.Name || hex);
|
||||
|
||||
this._postWorldCard(world);
|
||||
}
|
||||
|
||||
_enableSyncButton(worldName) {
|
||||
const btn = this.element?.querySelector('.mgt2-sector-map-sync');
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.title = worldName ? `Synchroniser la vue sur ${worldName}` : 'Ouvrir la carte interactive chez tous les joueurs';
|
||||
}
|
||||
}
|
||||
|
||||
/* ───── Carte de chat ───── */
|
||||
|
||||
static _STARPORT = { A:'Excellent', B:'Bon', C:'Routinier', D:'Médiocre', E:'Frontière', X:'Aucun' };
|
||||
@@ -508,6 +553,9 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
this._sector = sector;
|
||||
this._subsector = null;
|
||||
this._mapHex = hex;
|
||||
this._mapCenter = null;
|
||||
this._mapScale = null;
|
||||
this._enableSyncButton(name);
|
||||
|
||||
const iframe = this.element?.querySelector('.mgt2-sector-map-frame');
|
||||
if (iframe) iframe.src = this._mapUrl;
|
||||
@@ -517,41 +565,67 @@ export class SectorMapApp extends ApplicationV2 {
|
||||
|
||||
/* ───── Partage ───── */
|
||||
|
||||
_shareMap() {
|
||||
if (!this._sector) {
|
||||
ui.notifications.warn('Aucun secteur sélectionné — utilisez la recherche pour centrer sur un secteur');
|
||||
async _syncAll() {
|
||||
if (!game.socket?.emit) {
|
||||
ui.notifications.warn('Socket indisponible — impossible de synchroniser');
|
||||
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}`
|
||||
: `Secteur ${this._sector}`;
|
||||
try {
|
||||
// Tentative de récupération du zoom via getViewport
|
||||
const viewport = await new Promise((resolve) => {
|
||||
const handler = (event) => {
|
||||
if (event.data?.type === 'viewport') {
|
||||
window.removeEventListener('message', handler);
|
||||
resolve(event.data);
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', handler);
|
||||
const iframe = this.element?.querySelector('.mgt2-sector-map-frame');
|
||||
if (iframe?.contentWindow) {
|
||||
iframe.contentWindow.postMessage(
|
||||
{ source: 'travellermap', type: 'getViewport' },
|
||||
'*'
|
||||
);
|
||||
}
|
||||
setTimeout(() => {
|
||||
window.removeEventListener('message', handler);
|
||||
resolve(null);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
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>`;
|
||||
if (viewport) {
|
||||
this._sector = viewport.sector || this._sector;
|
||||
this._subsector = viewport.subsector || this._subsector;
|
||||
this._mapHex = viewport.hex || this._mapHex;
|
||||
this._mapCenter = (viewport.x != null && viewport.y != null) ? `${viewport.x},${viewport.y}` : this._mapCenter;
|
||||
this._mapScale = viewport.scale || this._mapScale;
|
||||
}
|
||||
|
||||
ChatMessage.create({ content: html, rollMode: 'public' });
|
||||
ui.notifications.info(`Carte partagée avec les joueurs`);
|
||||
const payload = {
|
||||
_type: 'sectorMapSync',
|
||||
sector: this._sector,
|
||||
subsector: this._subsector,
|
||||
mapHex: this._mapHex,
|
||||
mapCenter: this._mapCenter,
|
||||
mapScale: this._mapScale,
|
||||
};
|
||||
const players = game.users.filter(u => !u.isGM && u.active);
|
||||
for (const player of players) {
|
||||
game.socket.emit(`module.${MODULE_ID}`, payload);
|
||||
}
|
||||
ui.notifications.info(`Vue synchronisée chez ${players.length} joueur(s)`);
|
||||
} catch (err) {
|
||||
console.error(`${MODULE_ID} | Erreur socket sync:`, err);
|
||||
ui.notifications.error('Erreur lors de la synchronisation');
|
||||
}
|
||||
}
|
||||
|
||||
_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`);
|
||||
updatePosition(sector, subsector, mapHex, mapUrl) {
|
||||
this._sector = sector;
|
||||
this._subsector = subsector;
|
||||
this._mapHex = mapHex || null;
|
||||
const iframe = this.element?.querySelector('.mgt2-sector-map-frame');
|
||||
if (iframe) iframe.src = mapUrl || this._mapUrl;
|
||||
}
|
||||
|
||||
_openTravelDialog() {
|
||||
|
||||
Reference in New Issue
Block a user