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
+2
View File
@@ -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
View File
@@ -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 += `&center=${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() {
+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);
});
+1
View File
@@ -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
View File
@@ -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;
+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;
}
});
});
+1 -1
View File
@@ -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
View File
@@ -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;
}
+14 -27
View File
@@ -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>
+3
View File
@@ -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>