Files
mgt2-compendium-amiral-denisov/scripts/TravellerNpcDialog.js
T
uberwald ef7fe6e2bd Corrections et améliorations pour /gennpc - v1.3.0
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>
2026-05-27 23:56:21 +02:00

282 lines
9.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 });
}