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:
@@ -7,12 +7,14 @@
|
|||||||
"verified": "13",
|
"verified": "13",
|
||||||
"maximum": "14"
|
"maximum": "14"
|
||||||
},
|
},
|
||||||
|
"socket": "true",
|
||||||
"description": "Module de compendium et d'outils Mongoose Traveller 2e pour FoundryVTT écrit par JdR.Ninja.\nInclut les commandes /commerce, /pnj, /rencontre et /mission pour automatiser le commerce, les PNJ rapides, les rencontres et les contrats aléatoires. La fenêtre /pnj inclut un onglet 'PNJ Détaillé' pour la génération de PNJ Traveller selon les règles du générateur officiel, en s'appuyant sur les compétences natives des fiches.",
|
"description": "Module de compendium et d'outils Mongoose Traveller 2e pour FoundryVTT écrit par JdR.Ninja.\nInclut les commandes /commerce, /pnj, /rencontre et /mission pour automatiser le commerce, les PNJ rapides, les rencontres et les contrats aléatoires. La fenêtre /pnj inclut un onglet 'PNJ Détaillé' pour la génération de PNJ Traveller selon les règles du générateur officiel, en s'appuyant sur les compétences natives des fiches.",
|
||||||
"esmodules": [
|
"esmodules": [
|
||||||
"scripts/batchNpcCreator.js",
|
"scripts/batchNpcCreator.js",
|
||||||
"scripts/commerce.js",
|
"scripts/commerce.js",
|
||||||
"scripts/npc.js",
|
"scripts/npc.js",
|
||||||
"scripts/sector.js",
|
"scripts/sector.js",
|
||||||
|
"scripts/sectorSocket.js",
|
||||||
"scripts/utils/travellerNpcUtils.js",
|
"scripts/utils/travellerNpcUtils.js",
|
||||||
"scripts/data/travellerNpcGenerator.js",
|
"scripts/data/travellerNpcGenerator.js",
|
||||||
"scripts/travellerNpcGenerator.js",
|
"scripts/travellerNpcGenerator.js",
|
||||||
|
|||||||
+113
-39
@@ -19,12 +19,15 @@ export class SectorMapApp extends ApplicationV2 {
|
|||||||
window: { icon: 'fas fa-map', resizable: true, controls: [] },
|
window: { icon: 'fas fa-map', resizable: true, controls: [] },
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(sector, subsector) {
|
constructor(sector, subsector, { readOnly = false } = {}) {
|
||||||
super();
|
super();
|
||||||
this._sector = sector;
|
this._sector = sector;
|
||||||
this._subsector = subsector;
|
this._subsector = subsector;
|
||||||
|
this._readOnly = readOnly;
|
||||||
this._handler = null;
|
this._handler = null;
|
||||||
this._mapHex = null;
|
this._mapHex = null;
|
||||||
|
this._mapCenter = null;
|
||||||
|
this._mapScale = null;
|
||||||
this._searchTimeout = null;
|
this._searchTimeout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,13 +40,16 @@ export class SectorMapApp extends ApplicationV2 {
|
|||||||
|
|
||||||
get _mapUrl() {
|
get _mapUrl() {
|
||||||
const base = 'https://travellermap.com';
|
const base = 'https://travellermap.com';
|
||||||
|
const cb = this._readOnly ? '' : '&callback=postmessage';
|
||||||
if (!this._sector) return `${base}/?style=mongoose&hideui=1`;
|
if (!this._sector) return `${base}/?style=mongoose&hideui=1`;
|
||||||
if (this._subsector) {
|
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) {
|
if (this._mapHex) {
|
||||||
url += `&hex=${this._mapHex}&scale=512`;
|
url += `&hex=${this._mapHex}&scale=512`;
|
||||||
|
} else if (this._mapCenter && this._mapScale) {
|
||||||
|
url += `¢er=${this._mapCenter}&scale=${this._mapScale}`;
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
@@ -58,6 +64,20 @@ export class SectorMapApp extends ApplicationV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_renderHTML() {
|
_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">
|
return `<div class="mgt2-sector-map-outer">
|
||||||
<div class="mgt2-sector-map-toolbar">
|
<div class="mgt2-sector-map-toolbar">
|
||||||
<div class="mgt2-sector-map-search">
|
<div class="mgt2-sector-map-search">
|
||||||
@@ -66,8 +86,7 @@ export class SectorMapApp extends ApplicationV2 {
|
|||||||
</div>
|
</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-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>
|
<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" 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-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-travel" title="Planifier un voyage"><i class="fas fa-route"></i> Voyage</button>
|
<button type="button" class="mgt2-sector-map-travel" title="Planifier un voyage"><i class="fas fa-route"></i> Voyage</button>
|
||||||
</div>
|
</div>
|
||||||
<iframe
|
<iframe
|
||||||
@@ -80,10 +99,11 @@ export class SectorMapApp extends ApplicationV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _onRender(context, options) {
|
async _onRender(context, options) {
|
||||||
|
if (this._readOnly) {
|
||||||
|
this._listen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._listen();
|
this._listen();
|
||||||
this.element?.querySelector('.mgt2-sector-map-share')?.addEventListener('click', () => {
|
|
||||||
this._shareMap();
|
|
||||||
});
|
|
||||||
this.element?.querySelector('.mgt2-sector-map-sync')?.addEventListener('click', () => {
|
this.element?.querySelector('.mgt2-sector-map-sync')?.addEventListener('click', () => {
|
||||||
this._syncAll();
|
this._syncAll();
|
||||||
});
|
});
|
||||||
@@ -112,16 +132,24 @@ export class SectorMapApp extends ApplicationV2 {
|
|||||||
_listen() {
|
_listen() {
|
||||||
if (this._handler) return;
|
if (this._handler) return;
|
||||||
this._handler = (event) => {
|
this._handler = (event) => {
|
||||||
// Accept messages from travellermap or any origin (for testing)
|
|
||||||
const d = event.data || {};
|
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 wx = d.x ?? d.location?.x;
|
||||||
const wy = d.y ?? d.location?.y;
|
const wy = d.y ?? d.location?.y;
|
||||||
if (wx == null || wy == null) return;
|
if (wx == null || wy == null) return;
|
||||||
const x = Number(wx);
|
const x = Number(wx);
|
||||||
const y = Number(wy);
|
const y = Number(wy);
|
||||||
if (isNaN(x) || isNaN(y)) return;
|
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);
|
console.error('SectorMapApp | click handler failed:', err);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -149,6 +177,13 @@ export class SectorMapApp extends ApplicationV2 {
|
|||||||
const sectorName = meta.Names?.[0]?.Text;
|
const sectorName = meta.Names?.[0]?.Text;
|
||||||
if (!sectorName) { console.error('SectorMapApp | no Names[0].Text in metadata', meta); return; }
|
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 hex = String(hx).padStart(2, '0') + String(hy).padStart(2, '0');
|
||||||
const resp = await fetch(
|
const resp = await fetch(
|
||||||
`https://travellermap.com/data/${encodeURIComponent(sectorName)}/${hex}`
|
`https://travellermap.com/data/${encodeURIComponent(sectorName)}/${hex}`
|
||||||
@@ -159,9 +194,19 @@ export class SectorMapApp extends ApplicationV2 {
|
|||||||
const world = data.Worlds?.[0];
|
const world = data.Worlds?.[0];
|
||||||
if (!world) { console.error('SectorMapApp | no Worlds in data', data); return; }
|
if (!world) { console.error('SectorMapApp | no Worlds in data', data); return; }
|
||||||
|
|
||||||
|
this._enableSyncButton(world.Name || hex);
|
||||||
|
|
||||||
this._postWorldCard(world);
|
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 ───── */
|
/* ───── Carte de chat ───── */
|
||||||
|
|
||||||
static _STARPORT = { A:'Excellent', B:'Bon', C:'Routinier', D:'Médiocre', E:'Frontière', X:'Aucun' };
|
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._sector = sector;
|
||||||
this._subsector = null;
|
this._subsector = null;
|
||||||
this._mapHex = hex;
|
this._mapHex = hex;
|
||||||
|
this._mapCenter = null;
|
||||||
|
this._mapScale = null;
|
||||||
|
this._enableSyncButton(name);
|
||||||
|
|
||||||
const iframe = this.element?.querySelector('.mgt2-sector-map-frame');
|
const iframe = this.element?.querySelector('.mgt2-sector-map-frame');
|
||||||
if (iframe) iframe.src = this._mapUrl;
|
if (iframe) iframe.src = this._mapUrl;
|
||||||
@@ -517,41 +565,67 @@ export class SectorMapApp extends ApplicationV2 {
|
|||||||
|
|
||||||
/* ───── Partage ───── */
|
/* ───── Partage ───── */
|
||||||
|
|
||||||
_shareMap() {
|
async _syncAll() {
|
||||||
if (!this._sector) {
|
if (!game.socket?.emit) {
|
||||||
ui.notifications.warn('Aucun secteur sélectionné — utilisez la recherche pour centrer sur un secteur');
|
ui.notifications.warn('Socket indisponible — impossible de synchroniser');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const posterUrl = `https://travellermap.com/api/poster?sector=${encodeURIComponent(this._sector)}${this._subsector ? `&subsector=${encodeURIComponent(this._subsector)}` : ''}&style=mongoose&scale=128&dpr=2`;
|
try {
|
||||||
const label = this._subsector
|
// Tentative de récupération du zoom via getViewport
|
||||||
? `Sous-secteur ${this._subsector} — ${this._sector}`
|
const viewport = await new Promise((resolve) => {
|
||||||
: `Secteur ${this._sector}`;
|
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">
|
if (viewport) {
|
||||||
<div class="mgt2-world-card-header">
|
this._sector = viewport.sector || this._sector;
|
||||||
<span class="mgt2-world-name">${label}</span>
|
this._subsector = viewport.subsector || this._subsector;
|
||||||
</div>
|
this._mapHex = viewport.hex || this._mapHex;
|
||||||
<div class="mgt2-shared-map-image">
|
this._mapCenter = (viewport.x != null && viewport.y != null) ? `${viewport.x},${viewport.y}` : this._mapCenter;
|
||||||
<img src="${posterUrl}" alt="${label}">
|
this._mapScale = viewport.scale || this._mapScale;
|
||||||
</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() {
|
const payload = {
|
||||||
game.socket.emit(`module.${MODULE_ID}`, {
|
_type: 'sectorMapSync',
|
||||||
type: 'sectorMapSync',
|
|
||||||
sector: this._sector,
|
sector: this._sector,
|
||||||
subsector: this._subsector,
|
subsector: this._subsector,
|
||||||
});
|
mapHex: this._mapHex,
|
||||||
ui.notifications.info(`Carte synchronisée chez tous les joueurs`);
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
_openTravelDialog() {
|
||||||
|
|||||||
@@ -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 MODULE_ID = 'mgt2-compendium-amiral-denisov';
|
||||||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
|
|
||||||
@@ -41,6 +45,23 @@ class BatchNpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||||||
this._applyTheme(html);
|
this._applyTheme(html);
|
||||||
|
|
||||||
html.find('[data-action="browse-directory"]').on('click', async () => {
|
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({
|
const fp = new FilePicker({
|
||||||
type: 'image',
|
type: 'image',
|
||||||
current: this._formData.batch.directory || '',
|
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
|
<i class="fas fa-users"></i> Importer PNJs par lots
|
||||||
</button>`);
|
</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');
|
const footer = $html.find('.directory-footer');
|
||||||
if (footer.length) {
|
if (footer.length) {
|
||||||
footer.append(btn);
|
footer.append(btn);
|
||||||
|
footer.append(grid);
|
||||||
} else {
|
} else {
|
||||||
$html.find('.directory-header').after(btn);
|
$html.find('.directory-header').after(btn);
|
||||||
|
$html.find('.directory-header').after(grid);
|
||||||
}
|
}
|
||||||
|
|
||||||
btn.on('click', () => {
|
btn.on('click', () => {
|
||||||
new BatchNpcDialog().render({ force: true });
|
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);
|
||||||
|
});
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ Hooks.once('init', () => {
|
|||||||
`modules/${MODULE_ID}/templates/traveller-npc-result.hbs`,
|
`modules/${MODULE_ID}/templates/traveller-npc-result.hbs`,
|
||||||
`modules/${MODULE_ID}/templates/ally-enemy-result.hbs`,
|
`modules/${MODULE_ID}/templates/ally-enemy-result.hbs`,
|
||||||
`modules/${MODULE_ID}/templates/batch-npc-dialog.hbs`,
|
`modules/${MODULE_ID}/templates/batch-npc-dialog.hbs`,
|
||||||
|
`modules/${MODULE_ID}/templates/player-map.hbs`,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-10
@@ -16,9 +16,10 @@ let _pendingHandle = false;
|
|||||||
|
|
||||||
/* ───── Fonctions partagées ───── */
|
/* ───── Fonctions partagées ───── */
|
||||||
|
|
||||||
async function openMap(sector, subsector) {
|
async function openMap(sector, subsector, { readOnly = false } = {}) {
|
||||||
const app = new SectorMapApp(sector, subsector);
|
const app = new SectorMapApp(sector, subsector, { readOnly });
|
||||||
await app.render({ force: true });
|
await app.render({ force: true });
|
||||||
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSectorCommand(sector, subsector) {
|
async function handleSectorCommand(sector, subsector) {
|
||||||
@@ -139,16 +140,9 @@ Hooks.on('preCreateChatMessage', (message, data, options) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ───── Socket (synchronisation MJ → joueurs) ───── */
|
/* ───── Socket (synchronisation MJ → joueurs) ───── voir sectorSocket.js ───── */
|
||||||
|
|
||||||
Hooks.once('ready', () => {
|
Hooks.once('ready', () => {
|
||||||
game.socket.on(`module.${MODULE_ID}`, (data) => {
|
|
||||||
if (data?.type !== 'sectorMapSync') return;
|
|
||||||
if (game.user?.isGM) return;
|
|
||||||
openMap(data.sector, data.subsector);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clics sur les liens de monde dans les journals de trajet
|
|
||||||
document.addEventListener('click', (event) => {
|
document.addEventListener('click', (event) => {
|
||||||
const link = event.target.closest('.mgt2-world-link');
|
const link = event.target.closest('.mgt2-world-link');
|
||||||
if (!link) return;
|
if (!link) return;
|
||||||
|
|||||||
@@ -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 += `¢er=${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 += `¢er=${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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,7 +12,7 @@ function buildStartupNoticeHtml() {
|
|||||||
<div style="padding:6px 8px;border-left:4px solid #c9a227;background:#f5f0e8;color:#2c2c3e;line-height:1.45;">
|
<div style="padding:6px 8px;border-left:4px solid #c9a227;background:#f5f0e8;color:#2c2c3e;line-height:1.45;">
|
||||||
<strong>MgT2e – outils</strong>
|
<strong>MgT2e – outils</strong>
|
||||||
<span style="color:#6a5422;">·</span>
|
<span style="color:#6a5422;">·</span>
|
||||||
<code>/commerce</code> <code>/pnj</code> <code>/rencontre</code> <code>/mission</code> <code>/sector</code> <code>/subsector</code>
|
<code>/commerce</code> <code>/pnj</code> <code>/rencontre</code> <code>/mission</code> <code>/sector</code>
|
||||||
<div style="margin-top:2px;font-size:0.92em;color:#6a5422;">
|
<div style="margin-top:2px;font-size:0.92em;color:#6a5422;">
|
||||||
par <a href="https://www.uberwald.me" target="_blank" rel="noopener noreferrer">LeRatierBretonnien / Uberwald</a>
|
par <a href="https://www.uberwald.me" target="_blank" rel="noopener noreferrer">LeRatierBretonnien / Uberwald</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+8
-6
@@ -414,6 +414,14 @@ button.btn-calculate:hover,
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mgt2-sector-map-toolbar--ro {
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #1e2833;
|
||||||
|
border-bottom: 1px solid #5f7a99;
|
||||||
|
}
|
||||||
|
|
||||||
.mgt2-sector-map-label {
|
.mgt2-sector-map-label {
|
||||||
color: #d9b24c;
|
color: #d9b24c;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -508,7 +516,6 @@ button.btn-calculate:hover,
|
|||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mgt2-sector-map-share,
|
|
||||||
.mgt2-sector-map-sync,
|
.mgt2-sector-map-sync,
|
||||||
.mgt2-sector-map-travel {
|
.mgt2-sector-map-travel {
|
||||||
background: #c9a227;
|
background: #c9a227;
|
||||||
@@ -522,7 +529,6 @@ button.btn-calculate:hover,
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mgt2-sector-map-share:hover,
|
|
||||||
.mgt2-sector-map-sync:hover,
|
.mgt2-sector-map-sync:hover,
|
||||||
.mgt2-sector-map-travel:hover {
|
.mgt2-sector-map-travel:hover {
|
||||||
background: #d9b24c;
|
background: #d9b24c;
|
||||||
@@ -766,10 +772,6 @@ button.btn-calculate:hover,
|
|||||||
color: #222;
|
color: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mgt2-shared-map .mgt2-sector-map-share {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mgt2-shared-map-image {
|
.mgt2-shared-map-image {
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,35 @@
|
|||||||
<form class="mgt2-npc-form mgt2-batch-npc-form">
|
<form class="mgt2-npc-form mgt2-batch-npc-form">
|
||||||
<h3><i class="fas fa-users"></i> Création de PNJs par lots</h3>
|
|
||||||
<p class="npc-intro">Crée un PNJ pour chaque image trouvée dans un répertoire.</p>
|
|
||||||
|
|
||||||
<fieldset>
|
<fieldset style="padding:8px 10px;margin:0 0 6px">
|
||||||
<legend>Répertoire source</legend>
|
<div class="form-group-row" style="gap:8px">
|
||||||
<div class="form-group-row">
|
<div class="form-group" style="flex:1;min-width:0">
|
||||||
<div class="form-group">
|
<label for="batch-directory" style="font-size:0.88em">Répertoire des images</label>
|
||||||
<label for="batch-directory">Chemin du répertoire</label>
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="batch-directory" name="batch.directory" type="text" value="{{batch.directory}}" placeholder="worlds/mon-monde/images/pnjs">
|
<input id="batch-directory" name="batch.directory" type="text" value="{{batch.directory}}" placeholder="worlds/mon-monde/images">
|
||||||
<button type="button" class="btn-small" data-action="browse-directory" title="Parcourir">
|
<button type="button" class="btn-small" data-action="browse-directory" title="Parcourir" style="flex:0 0 28px;padding:2px 4px"><i class="fas fa-folder-open"></i></button>
|
||||||
<i class="fas fa-folder-open"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="hint">Dans vos données Foundry (Data), ex: <code>worlds/mon-monde/images/pnjs</code></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
<div class="form-group" style="flex:0 0 180px">
|
||||||
|
<label for="batch-folderName" style="font-size:0.88em">Dossier Acteur</label>
|
||||||
<fieldset>
|
|
||||||
<legend>Organisation</legend>
|
|
||||||
<div class="form-group-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="batch-folderName">Nom du dossier Acteur</label>
|
|
||||||
<input id="batch-folderName" name="batch.folderName" type="text" value="{{batch.folderName}}" placeholder="PNJs importés">
|
<input id="batch-folderName" name="batch.folderName" type="text" value="{{batch.folderName}}" placeholder="PNJs importés">
|
||||||
<div class="hint">Les PNJs créés seront rangés dans ce dossier.</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div class="form-group checkbox-group">
|
<div class="form-group checkbox-group" style="margin:0 0 6px;padding:0 10px">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" name="batch.openSheets" {{#if batch.openSheets}}checked{{/if}}>
|
<input type="checkbox" name="batch.openSheets" {{#if batch.openSheets}}checked{{/if}}>
|
||||||
Ouvrir la dernière fiche créée
|
Ouvrir la dernière fiche
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="batch-progress" class="batch-progress" style="display:none">
|
<div id="batch-progress" class="batch-progress" style="display:none;margin:0 10px 6px">
|
||||||
<div class="batch-progress-bar"><div class="batch-progress-fill" id="batch-progress-fill"></div></div>
|
<div class="batch-progress-bar"><div class="batch-progress-fill" id="batch-progress-fill"></div></div>
|
||||||
<div class="batch-progress-text" id="batch-progress-text">Initialisation...</div>
|
<div class="batch-progress-text" id="batch-progress-text" style="font-size:0.78em">Initialisation...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-footer">
|
<div class="form-footer" style="padding:6px 10px">
|
||||||
<button type="button" class="btn-calculate" data-action="batch-create">
|
<button type="button" class="btn-calculate" data-action="batch-create" style="padding:4px 12px;font-size:0.85em">
|
||||||
<i class="fas fa-dice-d6"></i> Créer les PNJs
|
<i class="fas fa-dice-d6"></i> Créer les PNJs
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div style="display:flex;flex-direction:column;height:100%;padding:0;overflow:hidden">
|
||||||
|
<iframe src="{{url}}" width="100%" height="100%" style="border:none" allow="clipboard-write" referrerpolicy="no-referrer"></iframe>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user