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",
|
||||
"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.",
|
||||
"esmodules": [
|
||||
"scripts/batchNpcCreator.js",
|
||||
"scripts/commerce.js",
|
||||
"scripts/npc.js",
|
||||
"scripts/sector.js",
|
||||
"scripts/sectorSocket.js",
|
||||
"scripts/utils/travellerNpcUtils.js",
|
||||
"scripts/data/travellerNpcGenerator.js",
|
||||
"scripts/travellerNpcGenerator.js",
|
||||
|
||||
+113
-39
@@ -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>`;
|
||||
|
||||
ChatMessage.create({ content: html, rollMode: 'public' });
|
||||
ui.notifications.info(`Carte partagée avec les joueurs`);
|
||||
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;
|
||||
}
|
||||
|
||||
_syncAll() {
|
||||
game.socket.emit(`module.${MODULE_ID}`, {
|
||||
type: 'sectorMapSync',
|
||||
const payload = {
|
||||
_type: 'sectorMapSync',
|
||||
sector: this._sector,
|
||||
subsector: this._subsector,
|
||||
});
|
||||
ui.notifications.info(`Carte synchronisée chez tous les joueurs`);
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@ Hooks.once('init', () => {
|
||||
`modules/${MODULE_ID}/templates/traveller-npc-result.hbs`,
|
||||
`modules/${MODULE_ID}/templates/ally-enemy-result.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 ───── */
|
||||
|
||||
async function openMap(sector, subsector) {
|
||||
const app = new SectorMapApp(sector, subsector);
|
||||
async function openMap(sector, subsector, { readOnly = false } = {}) {
|
||||
const app = new SectorMapApp(sector, subsector, { readOnly });
|
||||
await app.render({ force: true });
|
||||
return app;
|
||||
}
|
||||
|
||||
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', () => {
|
||||
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) => {
|
||||
const link = event.target.closest('.mgt2-world-link');
|
||||
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;">
|
||||
<strong>MgT2e – outils</strong>
|
||||
<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;">
|
||||
par <a href="https://www.uberwald.me" target="_blank" rel="noopener noreferrer">LeRatierBretonnien / Uberwald</a>
|
||||
</div>
|
||||
|
||||
+8
-6
@@ -414,6 +414,14 @@ button.btn-calculate:hover,
|
||||
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 {
|
||||
color: #d9b24c;
|
||||
font-weight: bold;
|
||||
@@ -508,7 +516,6 @@ button.btn-calculate:hover,
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.mgt2-sector-map-share,
|
||||
.mgt2-sector-map-sync,
|
||||
.mgt2-sector-map-travel {
|
||||
background: #c9a227;
|
||||
@@ -522,7 +529,6 @@ button.btn-calculate:hover,
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mgt2-sector-map-share:hover,
|
||||
.mgt2-sector-map-sync:hover,
|
||||
.mgt2-sector-map-travel:hover {
|
||||
background: #d9b24c;
|
||||
@@ -766,10 +772,6 @@ button.btn-calculate:hover,
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.mgt2-shared-map .mgt2-sector-map-share {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mgt2-shared-map-image {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
@@ -1,48 +1,35 @@
|
||||
<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>
|
||||
<legend>Répertoire source</legend>
|
||||
<div class="form-group-row">
|
||||
<div class="form-group">
|
||||
<label for="batch-directory">Chemin du répertoire</label>
|
||||
<fieldset style="padding:8px 10px;margin:0 0 6px">
|
||||
<div class="form-group-row" style="gap:8px">
|
||||
<div class="form-group" style="flex:1;min-width:0">
|
||||
<label for="batch-directory" style="font-size:0.88em">Répertoire des images</label>
|
||||
<div class="input-group">
|
||||
<input id="batch-directory" name="batch.directory" type="text" value="{{batch.directory}}" placeholder="worlds/mon-monde/images/pnjs">
|
||||
<button type="button" class="btn-small" data-action="browse-directory" title="Parcourir">
|
||||
<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>
|
||||
<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" style="flex:0 0 28px;padding:2px 4px"><i class="fas fa-folder-open"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Organisation</legend>
|
||||
<div class="form-group-row">
|
||||
<div class="form-group">
|
||||
<label for="batch-folderName">Nom du dossier Acteur</label>
|
||||
<div class="form-group" style="flex:0 0 180px">
|
||||
<label for="batch-folderName" style="font-size:0.88em">Dossier Acteur</label>
|
||||
<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>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<div class="form-group checkbox-group" style="margin:0 0 6px;padding:0 10px">
|
||||
<label>
|
||||
<input type="checkbox" name="batch.openSheets" {{#if batch.openSheets}}checked{{/if}}>
|
||||
Ouvrir la dernière fiche créée
|
||||
Ouvrir la dernière fiche
|
||||
</label>
|
||||
</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-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 class="form-footer">
|
||||
<button type="button" class="btn-calculate" data-action="batch-create">
|
||||
<div class="form-footer" style="padding:6px 10px">
|
||||
<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
|
||||
</button>
|
||||
</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