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:
2026-07-09 07:49:12 +02:00
parent bfe24812bd
commit 7fc8d2c09c
10 changed files with 337 additions and 84 deletions
+108
View File
@@ -0,0 +1,108 @@
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
let _playerDialog = null;
class PlayerMapDialog extends HandlebarsApplicationMixin(ApplicationV2) {
static DEFAULT_OPTIONS = {
id: 'mgt2-sector-map-player',
classes: ['mgt2-sector-map'],
position: { width: 960, height: 720 },
window: { icon: 'fas fa-map', resizable: true },
};
static PARTS = {
main: {
template: `modules/${MODULE_ID}/templates/player-map.hbs`,
},
};
constructor(url, sector) {
super();
this._mapUrl = url;
this._sectorLabel = sector ? `Secteur ${sector}` : 'Carte stellaire';
}
get title() { return this._sectorLabel; }
async _prepareContext() {
return { url: this._mapUrl };
}
updateMap(url) {
this._mapUrl = url;
const iframe = this.element?.querySelector('iframe');
if (iframe) iframe.src = url;
}
}
function buildMapUrl(sector, subsector, mapHex, mapCenter, mapScale) {
const base = 'https://travellermap.com';
if (!sector) return `${base}/?style=mongoose&hideui=1`;
if (subsector) {
let url = `${base}/?sector=${encodeURIComponent(sector)}&subsector=${encodeURIComponent(subsector)}&style=mongoose&hideui=1`;
if (mapCenter && mapScale) url += `&center=${mapCenter}&scale=${mapScale}`;
return url;
}
let url = `${base}/?sector=${encodeURIComponent(sector)}&style=mongoose&hideui=1`;
if (mapHex) {
url += `&hex=${mapHex}&scale=512`;
} else if (mapCenter && mapScale) {
url += `&center=${mapCenter}&scale=${mapScale}`;
}
return url;
}
Hooks.once('ready', () => {
console.log(`${MODULE_ID} | sectorSocket pret`);
game.socket.on(`module.${MODULE_ID}`, (data) => {
if (game.user?.isGM) return;
if (data?._type === 'sectorMapSync') {
try {
const url = buildMapUrl(data.sector, data.subsector, data.mapHex, data.mapCenter, data.mapScale);
if (_playerDialog) {
_playerDialog.updateMap(url);
_playerDialog.bringToTop?.();
console.log(`${MODULE_ID} | Carte mise a jour`);
return;
}
_playerDialog = new PlayerMapDialog(url, data.sector);
_playerDialog.render({ force: true }).then(() => {
console.log(`${MODULE_ID} | Dialog affiche`);
}).catch(err => {
console.error(`${MODULE_ID} | Erreur render:`, err);
});
} catch (err) {
console.error(`${MODULE_ID} | Erreur:`, err);
}
return;
}
if (data?._type === 'canvasSync') {
try {
const { x, y, scale } = data;
console.log(`${MODULE_ID} | Canvas sync recu:`, x, y, scale);
canvas.animatePan({ x, y, scale, duration: 0 });
} catch (err) {
console.error(`${MODULE_ID} | Erreur canvas sync:`, err);
}
return;
}
if (data?._type === 'closeImagePopout') {
try {
let closed = 0;
document.querySelectorAll('.image-popout, [class*="image-popout"], [id*="image-popout"]').forEach(el => {
el.remove(); closed++;
});
console.log(`${MODULE_ID} | ${closed} portrait(s) fermé(s)`);
} catch (err) {
console.error(`${MODULE_ID} | Erreur close portraits:`, err);
}
return;
}
});
});