Ajout de la commande /gennpc pour générer des PNJ Traveller
Implémentation complète du générateur de PNJ Traveller basé sur : https://github.com/carloscasalar/traveller-npc-generator Fonctionnalités : - Génération de caractéristiques selon 4 catégories de citoyens - Distribution des compétences selon 6 niveaux d'expérience - 14 rôles différents avec priorités de caractéristiques spécifiques - Génération de noms aléatoires (masculin/féminin/neutre) - Création de fiche d'acteur mgt2e avec toutes les compétences - Interface utilisateur avec dialogue Handlebars - Commande /gennpc dans le chat Fichiers ajoutés : - scripts/data/travellerNpcGenerator.js (données et constantes) - scripts/travellerNpcGenerator.js (logique métier) - scripts/TravellerNpcDialog.js (interface utilisateur) - templates/traveller-npc-dialog.hbs (template dialogue) - templates/traveller-npc-result.hbs (template résultat) - styles/traveller-npc.css (styles spécifiques) Fichiers modifiés : - scripts/npc.js (intégration de la commande) - module.json (ajout des nouveaux scripts et styles) Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* Traveller NPC Generator - Dialogue de génération
|
||||
*
|
||||
* Ce fichier contient le dialogue pour générer des PNJ Traveller.
|
||||
*/
|
||||
|
||||
import { generateAndCreateTravellerNpc } from './travellerNpcGenerator.js';
|
||||
import {
|
||||
CITIZEN_CATEGORY_LIST,
|
||||
EXPERIENCE_LEVEL_LIST,
|
||||
ROLE_LIST,
|
||||
GENDER_LIST,
|
||||
DEFAULT_OPTIONS,
|
||||
CHARACTERISTIC_LIST,
|
||||
UPP_ORDER
|
||||
} from './data/travellerNpcGenerator.js';
|
||||
|
||||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
|
||||
|
||||
export class TravellerNpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
static DEFAULT_OPTIONS = {
|
||||
id: 'mgt2-traveller-npc',
|
||||
classes: ['mgt2-npc-dialog', 'mgt2-traveller-npc-dialog'],
|
||||
position: {
|
||||
width: 700,
|
||||
height: 'auto',
|
||||
},
|
||||
window: {
|
||||
title: 'Générateur de PNJ Traveller – MgT2e',
|
||||
resizable: true,
|
||||
},
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: `modules/${MODULE_ID}/templates/traveller-npc-dialog.hbs`,
|
||||
root: true,
|
||||
},
|
||||
};
|
||||
|
||||
constructor(options = {}) {
|
||||
super(options);
|
||||
|
||||
// Form data avec valeurs par défaut
|
||||
this._formData = {
|
||||
citizenCategory: options.citizenCategory || DEFAULT_OPTIONS.citizenCategory,
|
||||
experience: options.experience || DEFAULT_OPTIONS.experience,
|
||||
role: options.role || DEFAULT_OPTIONS.role,
|
||||
gender: options.gender || DEFAULT_OPTIONS.gender,
|
||||
firstName: options.firstName || '',
|
||||
surname: options.surname || '',
|
||||
useRandomName: options.useRandomName !== false, // Par défaut, on utilise des noms aléatoires
|
||||
createActor: options.createActor !== undefined ? options.createActor : DEFAULT_OPTIONS.createActor,
|
||||
actorName: options.actorName || '',
|
||||
openCreatedActor: options.openCreatedActor !== undefined ? options.openCreatedActor : DEFAULT_OPTIONS.openCreatedActor,
|
||||
};
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
registerHandlebarsHelpers();
|
||||
return {
|
||||
...this._formData,
|
||||
citizenCategories: CITIZEN_CATEGORY_LIST.map(c => ({
|
||||
key: c.key,
|
||||
label: c.label,
|
||||
description: c.description
|
||||
})),
|
||||
experienceLevels: EXPERIENCE_LEVEL_LIST.map(e => ({
|
||||
key: e.key,
|
||||
label: e.label,
|
||||
description: e.description
|
||||
})),
|
||||
roles: ROLE_LIST.map(r => ({
|
||||
key: r.key,
|
||||
label: r.label,
|
||||
description: r.description
|
||||
})),
|
||||
genders: GENDER_LIST.map(g => ({
|
||||
key: g.key,
|
||||
label: g.label
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
async _onRender(context, options) {
|
||||
await super._onRender(context, options);
|
||||
const html = this._getForm();
|
||||
if (!html?.length) return;
|
||||
|
||||
html.addClass('mgt2-traveller-npc-form');
|
||||
this._applyThemeStyles(html);
|
||||
|
||||
// Gestion des événements
|
||||
html.find('[data-action="generate-traveller-npc"]').on('click', async (event) => {
|
||||
event.preventDefault();
|
||||
this._readForm(html);
|
||||
await this._handleGenerate();
|
||||
});
|
||||
|
||||
html.find('[data-action="randomize-name"]').on('click', (event) => {
|
||||
event.preventDefault();
|
||||
this._randomizeName(html);
|
||||
});
|
||||
|
||||
// Gestion du basculement entre nom aléatoire et nom personnalisé
|
||||
html.find('[name="useRandomName"]').on('change', (event) => {
|
||||
const useRandom = event.target.checked;
|
||||
html.find('.name-fields').toggleClass('hidden', useRandom);
|
||||
});
|
||||
|
||||
// Initialiser l'affichage des champs de nom
|
||||
html.find('.name-fields').toggleClass('hidden', this._formData.useRandomName);
|
||||
}
|
||||
|
||||
_getForm() {
|
||||
return $(this.element).find('.window-content');
|
||||
}
|
||||
|
||||
_applyThemeStyles(html) {
|
||||
// Appliquer les styles de thème cohérents avec le dialogue existant
|
||||
html.find('.tabs .item').css({
|
||||
color: '#d8c79a',
|
||||
'text-shadow': 'none',
|
||||
'background-color': '',
|
||||
'border-bottom-color': 'transparent'
|
||||
});
|
||||
|
||||
html.find('.tabs .item.active').css({
|
||||
color: '#d9b24c',
|
||||
'text-shadow': 'none',
|
||||
'background-color': 'rgba(201, 162, 39, 0.18)',
|
||||
'border-bottom-color': '#c9a227'
|
||||
});
|
||||
|
||||
html.find('h3').css({
|
||||
color: '#5f4300',
|
||||
'border-bottom-color': '#b78f26',
|
||||
'text-shadow': 'none'
|
||||
});
|
||||
|
||||
html.find('legend').css({
|
||||
color: '#7a5c00',
|
||||
'text-shadow': 'none'
|
||||
});
|
||||
}
|
||||
|
||||
_readForm(html) {
|
||||
this._formData.citizenCategory = html.find('[name="citizenCategory"]').val();
|
||||
this._formData.experience = html.find('[name="experience"]').val();
|
||||
this._formData.role = html.find('[name="role"]').val();
|
||||
this._formData.gender = html.find('[name="gender"]').val();
|
||||
this._formData.firstName = html.find('[name="firstName"]').val();
|
||||
this._formData.surname = html.find('[name="surname"]').val();
|
||||
this._formData.useRandomName = html.find('[name="useRandomName"]').is(':checked');
|
||||
this._formData.createActor = html.find('[name="createActor"]').is(':checked');
|
||||
this._formData.actorName = html.find('[name="actorName"]').val();
|
||||
this._formData.openCreatedActor = html.find('[name="openCreatedActor"]').is(':checked');
|
||||
}
|
||||
|
||||
_randomizeName(html) {
|
||||
// Importer dynamiquement pour éviter les dépendances circulaires
|
||||
import('./data/travellerNpcGenerator.js').then(module => {
|
||||
const name = module.generateRandomName(this._formData.gender);
|
||||
html.find('[name="firstName"]').val(name.firstName);
|
||||
html.find('[name="surname"]').val(name.surname);
|
||||
this._formData.firstName = name.firstName;
|
||||
this._formData.surname = name.surname;
|
||||
this._formData.useRandomName = false;
|
||||
html.find('[name="useRandomName"]').prop('checked', false);
|
||||
html.find('.name-fields').removeClass('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
async _handleGenerate() {
|
||||
const button = $(this.element).find('[data-action="generate-traveller-npc"]');
|
||||
const originalLabel = button.html();
|
||||
|
||||
try {
|
||||
// Désactiver le bouton pendant la génération
|
||||
button.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Génération...');
|
||||
|
||||
// Préparer les options de génération
|
||||
const generateOptions = {
|
||||
citizenCategory: this._formData.citizenCategory,
|
||||
experience: this._formData.experience,
|
||||
role: this._formData.role,
|
||||
gender: this._formData.gender,
|
||||
createActor: this._formData.createActor,
|
||||
actorName: this._formData.actorName,
|
||||
openCreatedActor: this._formData.openCreatedActor
|
||||
};
|
||||
|
||||
// Si on n'utilise pas de nom aléatoire, passer le nom personnalisé
|
||||
if (!this._formData.useRandomName && this._formData.firstName && this._formData.surname) {
|
||||
generateOptions.firstName = this._formData.firstName;
|
||||
generateOptions.surname = this._formData.surname;
|
||||
}
|
||||
|
||||
// Générer le PNJ
|
||||
const result = await generateAndCreateTravellerNpc(generateOptions);
|
||||
|
||||
if (result.success) {
|
||||
// Afficher le résultat dans le chat
|
||||
await this._postToChatResult(result);
|
||||
|
||||
if (result.createdActor) {
|
||||
ui.notifications.info(`Fiche PNJ Traveller créée : ${result.createdActor.name}`);
|
||||
}
|
||||
} else {
|
||||
ui.notifications.error('Erreur lors de la génération du PNJ Traveller');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${MODULE_ID} | Erreur lors de la génération du PNJ Traveller:`, error);
|
||||
ui.notifications.error(`Erreur: ${error.message}`);
|
||||
} finally {
|
||||
// Réactiver le bouton
|
||||
button.prop('disabled', false).html(originalLabel);
|
||||
}
|
||||
}
|
||||
|
||||
async _postToChatResult(data) {
|
||||
registerHandlebarsHelpers();
|
||||
const html = await foundry.applications.handlebars.renderTemplate(
|
||||
`modules/${MODULE_ID}/templates/traveller-npc-result.hbs`,
|
||||
data
|
||||
);
|
||||
|
||||
await ChatMessage.create({
|
||||
content: html,
|
||||
speaker: ChatMessage.getSpeaker(),
|
||||
flags: { [MODULE_ID]: { type: 'traveller-npc-result' } },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper functions
|
||||
// ============================================================================
|
||||
|
||||
let helpersRegistered = false;
|
||||
|
||||
function registerHandlebarsHelpers() {
|
||||
if (helpersRegistered) return;
|
||||
helpersRegistered = true;
|
||||
|
||||
// Helper pour comparer deux valeurs
|
||||
Handlebars.registerHelper('eq', (a, b) => a === b);
|
||||
|
||||
// Helper pour rejoindre un tableau
|
||||
Handlebars.registerHelper('join', (arr, sep) => (Array.isArray(arr) ? arr.join(sep) : ''));
|
||||
|
||||
// Helper pour vérifier si une valeur contient du texte
|
||||
Handlebars.registerHelper('contains', (text, search) => String(text ?? '').includes(search));
|
||||
|
||||
// Helper pour vérifier si a > b
|
||||
Handlebars.registerHelper('gt', (a, b) => a > b);
|
||||
|
||||
// Helper pour afficher le niveau de compétence avec un symbole
|
||||
Handlebars.registerHelper('skillLevelSymbol', (level) => {
|
||||
if (level === 0) return '';
|
||||
if (level === 1) return '★';
|
||||
if (level === 2) return '★★';
|
||||
if (level === 3) return '★★★';
|
||||
return `+${level}`;
|
||||
});
|
||||
|
||||
// Helper pour formater le DM
|
||||
Handlebars.registerHelper('formatDm', (value) => {
|
||||
const dm = Math.floor((value - 6) / 3);
|
||||
return dm >= 0 ? `+${dm}` : `${dm}`;
|
||||
});
|
||||
|
||||
// Helper pour obtenir la classe CSS du niveau de compétence
|
||||
Handlebars.registerHelper('skillLevelClass', (level) => {
|
||||
if (level === 3) return 'skill-level-3';
|
||||
if (level === 2) return 'skill-level-2';
|
||||
if (level === 1) return 'skill-level-1';
|
||||
return 'skill-level-0';
|
||||
});
|
||||
|
||||
// Helper pour formater une compétence avec son niveau
|
||||
Handlebars.registerHelper('formatSkillForDisplay', (name, level) => {
|
||||
if (level === 0) {
|
||||
return name;
|
||||
}
|
||||
return `${name}-${level}`;
|
||||
});
|
||||
|
||||
// Helper pour créer un objet de libellés de caractéristiques
|
||||
Handlebars.registerHelper('createCharacteristicLabels', () => {
|
||||
const labels = {};
|
||||
CHARACTERISTIC_LIST.forEach(char => {
|
||||
labels[char.key] = char.label;
|
||||
});
|
||||
return labels;
|
||||
});
|
||||
|
||||
// Helper pour lookup dans un objet
|
||||
Handlebars.registerHelper('lookup', (obj, key) => {
|
||||
if (!obj || !key) return '';
|
||||
return obj[key] !== undefined ? obj[key] : '';
|
||||
});
|
||||
}
|
||||
|
||||
// Exporter pour pouvoir l'ouvrir depuis d'autres modules
|
||||
export function openTravellerNpcDialog(options = {}) {
|
||||
new TravellerNpcDialog(options).render({ force: true });
|
||||
}
|
||||
Reference in New Issue
Block a user