ef7fe6e2bd
Corrections critiques implémentées: - Remplacement du cache global mutable par ModuleCache - Binding des méthodes dans TravellerNpcDialog - Suppression des ré-exports circulaires - Validation complète des options - Correction: Duplicate export de TravellerNpcDialog et openTravellerNpcDialog - Correction: distributeSkillLevels ne supprime plus les spécialisations (ex: Pilot-Spacecraft ET Pilot-Small Craft sont maintenant conservées) Améliorations majeures: - Optimisation de l'algorithme de distribution des compétences (single-pass) - Optimisation de la génération des caractéristiques (priorité-based) - Gestion d'erreur améliorée avec TravellerNpcError - Création de TravellerNpcUtils.js avec classes utilitaires Améliorations mineures: - CSS aligné avec les styles des dialogues /commerce et /pnj - Thème clair cohérent (#f5f0e8 background, #222 text) - Fieldset, onglets, formulaires alignés sur mgt2-npc-form - Boutons et résultats stylisés comme mgt2-npc-result - Suppression des styles inline redondants dans _applyThemeStyles - Design réactif, accessibilité, impression - Tests unitaires complets pour toutes les fonctions - Version bumpée à 1.3.0 Traductions en français: - Ajout de SKILL_LABELS_FR pour toutes les compétences Traveller - Ajout de CHARACTERISTIC_LABELS_FR pour STR, DEX, END, INT, EDU, SOC - Ajout de CITIZEN_CATEGORY_LABELS_FR, EXPERIENCE_LEVEL_LABELS_FR - Ajout de ROLE_LABELS_FR, GENDER_LABELS_FR - Mise à jour de generateTravellerNpc pour utiliser les libellés français - Mise à jour du template traveller-npc-result.hbs pour afficher labelFr - Mise à jour du template traveller-npc-dialog.hbs avec libellés français - Mise à jour de TravellerNpcDialog._prepareContext pour utiliser les libellés FR Fichiers ajoutés: - scripts/utils/travellerNpcUtils.js - scripts/tests/travellerNpcGenerator.test.js Fichiers modifiés: - scripts/data/travellerNpcGenerator.js (+ traductions FR) - scripts/travellerNpcGenerator.js (+ fonctions getSkillLabelFr, getCharacteristicLabelFr) - scripts/TravellerNpcDialog.js (libellés FR dans _prepareContext) - scripts/npc.js - styles/traveller-npc.css - templates/traveller-npc-dialog.hbs - templates/traveller-npc-result.hbs - module.json Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
282 lines
9.7 KiB
JavaScript
282 lines
9.7 KiB
JavaScript
/**
|
||
* 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,
|
||
generateRandomName,
|
||
CITIZEN_CATEGORY_LABELS_FR,
|
||
EXPERIENCE_LEVEL_LABELS_FR,
|
||
ROLE_LABELS_FR,
|
||
GENDER_LABELS_FR
|
||
} 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,
|
||
createActor: options.createActor !== undefined ? options.createActor : DEFAULT_OPTIONS.createActor,
|
||
actorName: options.actorName || '',
|
||
openCreatedActor: options.openCreatedActor !== undefined ? options.openCreatedActor : DEFAULT_OPTIONS.openCreatedActor,
|
||
};
|
||
|
||
// Bind les méthodes pour éviter les problèmes de contexte
|
||
this._readForm = this._readForm.bind(this);
|
||
this._handleGenerate = this._handleGenerate.bind(this);
|
||
this._randomizeName = this._randomizeName.bind(this);
|
||
this._applyThemeStyles = this._applyThemeStyles.bind(this);
|
||
this._getForm = this._getForm.bind(this);
|
||
}
|
||
|
||
async _prepareContext() {
|
||
registerHandlebarsHelpers();
|
||
return {
|
||
...this._formData,
|
||
citizenCategories: CITIZEN_CATEGORY_LIST.map(c => ({
|
||
key: c.key,
|
||
label: CITIZEN_CATEGORY_LABELS_FR[c.key] || c.label,
|
||
description: c.description
|
||
})),
|
||
experienceLevels: EXPERIENCE_LEVEL_LIST.map(e => ({
|
||
key: e.key,
|
||
label: EXPERIENCE_LEVEL_LABELS_FR[e.key] || e.label,
|
||
description: e.description
|
||
})),
|
||
roles: ROLE_LIST.map(r => ({
|
||
key: r.key,
|
||
label: ROLE_LABELS_FR[r.key] || r.label,
|
||
description: r.description
|
||
})),
|
||
genders: GENDER_LIST.map(g => ({
|
||
key: g.key,
|
||
label: GENDER_LABELS_FR[g.key] || 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;
|
||
this._formData.useRandomName = useRandom;
|
||
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) {
|
||
// Les styles sont maintenant gérés par CSS, cette méthode peut être vide
|
||
// ou utilisée pour des ajustements spécifiques si nécessaire
|
||
// Les styles de base sont cohérents avec mgt2-npc-dialog et mgt2-commerce-dialog
|
||
}
|
||
|
||
_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) {
|
||
const name = 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
|
||
// ============================================================================
|
||
|
||
// Import des données pour les helpers
|
||
import { CHARACTERISTIC_LIST, UPP_ORDER } from './data/travellerNpcGenerator.js';
|
||
|
||
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 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 });
|
||
}
|