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
+82
View File
@@ -1,3 +1,7 @@
import { CommerceDialog } from './CommerceDialog.js';
import { NpcDialog } from './NpcDialog.js';
import { SectorMapApp } from './SectorMapApp.js';
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
@@ -41,6 +45,23 @@ class BatchNpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
this._applyTheme(html);
html.find('[data-action="browse-directory"]').on('click', async () => {
// Foundry v14: FilePicker avec pickFolders pour sélectionner un répertoire
try {
const fp = new FilePicker({
type: 'folder',
current: this._formData.batch.directory || '',
callback: (path) => {
this._formData.batch.directory = path;
html.find('[name="batch.directory"]').val(path);
},
pickFolders: true,
field: null,
button: null,
});
fp.render(true);
return;
} catch (_e) { /* fallback v13 */ }
// Fallback v13: selection d'un fichier → extraction du dossier parent
const fp = new FilePicker({
type: 'image',
current: this._formData.batch.directory || '',
@@ -205,14 +226,75 @@ Hooks.on('renderActorDirectory', (app, html, data) => {
<i class="fas fa-users"></i> Importer PNJs par lots
</button>`);
const shortcuts = [
{ icon: 'fa-balance-scale', label: 'Commerce', abbr: 'Commerce', action: 'commerce', cls: 'mgt2-shortcut-commerce', title: 'Ouvrir le commerce' },
{ icon: 'fa-user', label: 'PNJ rapide', abbr: 'PNJ', action: 'npc', cls: 'mgt2-shortcut-pnj', title: 'Générer un PNJ rapide' },
{ icon: 'fa-random', label: 'Rencontre', abbr: 'Rencontre', action: 'encounter', cls: 'mgt2-shortcut-rencontre', title: 'Générer une rencontre' },
{ icon: 'fa-briefcase', label: 'Mission', abbr: 'Mission', action: 'mission', cls: 'mgt2-shortcut-mission', title: 'Générer une mission' },
{ icon: 'fa-image', label: 'Fermer portraits', abbr: 'Fermer img', action: 'closePortraits', cls: 'mgt2-shortcut-closeimg', title: 'Fermer les portraits affichés aux joueurs' },
];
const grid = $(`<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;margin:4px 8px;"></div>`);
shortcuts.forEach(s => {
const b = $(`<button class="${s.cls}" title="${s.title}" style="background:#2c2c3e;color:#d9b24c;border:1px solid #c9a227;padding:4px 6px;font-size:0.78em;cursor:pointer;border-radius:3px;text-align:center;">
<i class="fas ${s.icon}"></i> ${s.abbr}
</button>`).on('click', () => {
if (s.action === 'commerce') new CommerceDialog().render({ force: true });
else if (s.action === 'closePortraits') {
game.socket.emit(`module.${MODULE_ID}`, { _type: 'closeImagePopout' });
ui.notifications.info('Fermeture des portraits envoyée aux joueurs');
}
else new NpcDialog({ initialTab: s.action }).render({ force: true });
});
grid.append(b);
});
const footer = $html.find('.directory-footer');
if (footer.length) {
footer.append(btn);
footer.append(grid);
} else {
$html.find('.directory-header').after(btn);
$html.find('.directory-header').after(grid);
}
btn.on('click', () => {
new BatchNpcDialog().render({ force: true });
});
});
Hooks.on('renderSceneDirectory', (app, html, data) => {
if (!game.user?.isGM) return;
const $html = html instanceof jQuery ? html : $(html);
if ($html.find('.mgt2-sector-btn').length) return;
const sectorBtn = $(`<button class="mgt2-sector-btn" style="margin:4px 8px;flex:0;background:#2c2c3e;color:#d9b24c;border:1px solid #c9a227;cursor:pointer;">
<i class="fas fa-map"></i> Traveller Map
</button>`).on('click', () => {
new SectorMapApp().render({ force: true });
});
const canvasBtn = $(`<button class="mgt2-canvas-sync-btn" style="margin:4px 8px;flex:0;background:#2c2c3e;color:#d9b24c;border:1px solid #c9a227;cursor:pointer;">
<i class="fas fa-eye"></i> Sync vue MJ
</button>`).on('click', () => {
const pivot = canvas.stage?.pivot;
const scale = canvas.stage?.scale?.x;
if (!pivot || !scale) { ui.notifications.warn('Canvas pas encore prêt'); return; }
const players = game.users.filter(u => !u.isGM && u.active).map(u => u.id);
if (!players.length) { ui.notifications.warn('Aucun joueur connecté'); return; }
game.socket.emit(`module.${MODULE_ID}`, {
_type: 'canvasSync',
x: pivot.x,
y: pivot.y,
scale,
});
ui.notifications.info(`Vue synchronisée chez ${players.length} joueur(s)`);
});
const grid = $(`<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;margin:4px 8px;"></div>`);
grid.append(sectorBtn, canvasBtn);
const footer = $html.find('.directory-footer');
if (footer.length) footer.append(grid);
else $html.find('.directory-header').after(grid);
});