Auto-import PNJ3
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
|
||||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
const IMAGE_EXTENSIONS = new Set(['png', 'webp', 'jpg', 'jpeg', 'gif', 'svg', 'avif', 'bmp', 'tiff', 'tif']);
|
||||
|
||||
class BatchNpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
static DEFAULT_OPTIONS = {
|
||||
id: 'mgt2-batch-npc',
|
||||
classes: ['mgt2-npc-dialog'],
|
||||
position: { width: 540, height: 'auto' },
|
||||
window: { title: 'Création de PNJs par lots', resizable: true },
|
||||
form: { submitOnClose: false, closeOnSubmit: false },
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: `modules/${MODULE_ID}/templates/batch-npc-dialog.hbs`,
|
||||
root: true,
|
||||
},
|
||||
};
|
||||
|
||||
constructor(options = {}) {
|
||||
super(options);
|
||||
this._formData = {
|
||||
batch: {
|
||||
directory: '',
|
||||
folderName: 'PNJs importés',
|
||||
openSheets: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
return { ...this._formData };
|
||||
}
|
||||
|
||||
async _onRender(context, options) {
|
||||
await super._onRender(context, options);
|
||||
const html = $('#' + this.id).find('.window-content');
|
||||
|
||||
this._applyTheme(html);
|
||||
|
||||
html.find('[data-action="browse-directory"]').on('click', async () => {
|
||||
const fp = new FilePicker({
|
||||
type: 'image',
|
||||
current: this._formData.batch.directory || '',
|
||||
callback: (path) => {
|
||||
const dir = path.split('/').slice(0, -1).join('/');
|
||||
this._formData.batch.directory = dir;
|
||||
html.find('[name="batch.directory"]').val(dir);
|
||||
},
|
||||
});
|
||||
fp.render(true);
|
||||
});
|
||||
|
||||
html.find('[data-action="batch-create"]').on('click', async () => {
|
||||
this._readForm(html);
|
||||
await this._executeBatch(html);
|
||||
});
|
||||
}
|
||||
|
||||
_readForm(html) {
|
||||
this._formData.batch.directory = (html.find('[name="batch.directory"]').val()?.trim() || '').replace(/\/+$/, '');
|
||||
this._formData.batch.folderName = html.find('[name="batch.folderName"]').val()?.trim() || 'PNJs importés';
|
||||
this._formData.batch.openSheets = html.find('[name="batch.openSheets"]').is(':checked');
|
||||
}
|
||||
|
||||
_applyTheme(html) {
|
||||
html.find('h3').css({ color: '#5f4300', 'border-bottom-color': '#b78f26', 'text-shadow': 'none' });
|
||||
html.find('legend').css({ color: '#7a5c00', 'text-shadow': 'none' });
|
||||
}
|
||||
|
||||
async _executeBatch(html) {
|
||||
const { directory, folderName, openSheets } = this._formData.batch;
|
||||
|
||||
if (!directory) {
|
||||
ui.notifications.warn('Veuillez spécifier un répertoire source.');
|
||||
return;
|
||||
}
|
||||
|
||||
const $progress = html.find('#batch-progress');
|
||||
const $fill = html.find('#batch-progress-fill');
|
||||
const $text = html.find('#batch-progress-text');
|
||||
$progress.show();
|
||||
|
||||
try {
|
||||
$text.text('Analyse du répertoire...');
|
||||
|
||||
let files;
|
||||
try {
|
||||
const result = await FilePicker.browse('data', directory);
|
||||
files = result.files || [];
|
||||
} catch (e) {
|
||||
$progress.hide();
|
||||
ui.notifications.error(`Impossible de lire le répertoire « ${directory} » : ${e.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const imageFiles = files.filter(f => {
|
||||
const ext = f.split('.').pop()?.toLowerCase();
|
||||
return ext && IMAGE_EXTENSIONS.has(ext);
|
||||
});
|
||||
|
||||
if (imageFiles.length === 0) {
|
||||
$progress.hide();
|
||||
ui.notifications.warn(`Aucune image trouvée dans « ${directory} ».`);
|
||||
return;
|
||||
}
|
||||
|
||||
$text.text(`${imageFiles.length} image(s) trouvée(s). Vérification des doublons...`);
|
||||
|
||||
const normalizePath = (p) => p.replace(/^[\\/]+/, '').replace(/[\\/]+/g, '/').toLowerCase();
|
||||
|
||||
const existingImages = new Set();
|
||||
for (const actor of game.actors) {
|
||||
if (actor.img && actor.img !== 'icons/svg/mystery-man.svg') {
|
||||
existingImages.add(normalizePath(actor.img));
|
||||
}
|
||||
}
|
||||
|
||||
const toCreate = imageFiles.filter(f => !existingImages.has(normalizePath(f)));
|
||||
|
||||
if (toCreate.length === 0) {
|
||||
$progress.hide();
|
||||
ui.notifications.info('Tous les PNJs existent déjà (aucune nouvelle image à traiter).');
|
||||
return;
|
||||
}
|
||||
|
||||
let folder = null;
|
||||
if (folderName) {
|
||||
folder = game.folders.find(f => f.name === folderName && f.type === 'Actor');
|
||||
if (!folder) {
|
||||
folder = await Folder.create({ name: folderName, type: 'Actor', parent: null });
|
||||
}
|
||||
}
|
||||
|
||||
$text.text(`Création de ${toCreate.length} PNJ(s)...`);
|
||||
|
||||
const chunkSize = 10;
|
||||
let created = 0;
|
||||
const allCreated = [];
|
||||
|
||||
for (let i = 0; i < toCreate.length; i += chunkSize) {
|
||||
const chunk = toCreate.slice(i, i + chunkSize);
|
||||
const actorsData = chunk.map(path => {
|
||||
const name = path.split('/').pop().replace(/\.[^/.]+$/, '');
|
||||
return {
|
||||
name,
|
||||
type: 'npc',
|
||||
img: path,
|
||||
folder: folder?.id || null,
|
||||
token: {
|
||||
name,
|
||||
img: path,
|
||||
actorLink: true,
|
||||
disposition: 0,
|
||||
sight: { enabled: false },
|
||||
},
|
||||
system: {
|
||||
settings: { hideUntrained: true, lockCharacteristics: true },
|
||||
characteristics: {},
|
||||
hits: {},
|
||||
skills: {},
|
||||
},
|
||||
flags: {
|
||||
[MODULE_ID]: { batchImported: true, sourceImage: path },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const actors = await Actor.create(actorsData, { renderSheet: false });
|
||||
allCreated.push(...actors);
|
||||
created += actors.length;
|
||||
|
||||
const pct = Math.min(100, Math.round((i + chunkSize) / toCreate.length * 100));
|
||||
$fill.css('width', pct + '%');
|
||||
$text.text(`${created}/${toCreate.length} PNJ(s) créé(s)...`);
|
||||
}
|
||||
|
||||
ui.actors?.render(false);
|
||||
|
||||
$fill.css('width', '100%');
|
||||
$text.text(`${created} PNJ(s) créé(s) avec succès dans le dossier « ${folderName} » !`);
|
||||
ui.notifications.info(`${created} PNJ(s) créé(s) à partir des images.`);
|
||||
|
||||
if (openSheets && allCreated.length > 0) {
|
||||
const lastActor = allCreated[allCreated.length - 1];
|
||||
if (lastActor) lastActor.sheet?.render(true);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`${MODULE_ID} | Batch NPC creation error:`, error);
|
||||
$text.text(`Erreur : ${error.message}`);
|
||||
ui.notifications.error(`Erreur lors de la création : ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Hooks.on('renderActorDirectory', (app, html, data) => {
|
||||
if (!game.user?.isGM) return;
|
||||
if (html.find('.mgt2-batch-npc-btn').length) return;
|
||||
|
||||
const btn = $(`<button class="mgt2-batch-npc-btn" style="margin:4px 8px;flex:0;background:#2c2c3e;color:#d9b24c;border:1px solid #c9a227;">
|
||||
<i class="fas fa-users"></i> Importer PNJs par lots
|
||||
</button>`);
|
||||
|
||||
const footer = html.find('.directory-footer');
|
||||
if (footer.length) {
|
||||
footer.append(btn);
|
||||
} else {
|
||||
html.find('.directory-header').after(btn);
|
||||
}
|
||||
|
||||
btn.on('click', () => {
|
||||
new BatchNpcDialog().render({ force: true });
|
||||
});
|
||||
});
|
||||
@@ -38,6 +38,7 @@ Hooks.once('init', () => {
|
||||
`modules/${MODULE_ID}/templates/traveller-npc-dialog.hbs`,
|
||||
`modules/${MODULE_ID}/templates/traveller-npc-result.hbs`,
|
||||
`modules/${MODULE_ID}/templates/ally-enemy-result.hbs`,
|
||||
`modules/${MODULE_ID}/templates/batch-npc-dialog.hbs`,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user