import { formatCredits } from './tradeHelper.js'; import { createNpcActor, generateClientMission, generateEncounter, generateQuickNpc, formatSigned } from './npcHelper.js'; import { generateAllyEnemy } from './allyEnemyGenerator.js'; import { NPC_RELATIONS } from './data/npcTables.js'; import { generateAndCreateTravellerNpc } from './travellerNpcGenerator.js'; import { generateRandomName } from './data/travellerNpcGenerator.js'; import { localizeSkill } from './mgt2eSkills.js'; import { CITIZEN_CATEGORY_LIST, EXPERIENCE_LEVEL_LIST, ROLE_LIST, GENDER_LIST, DEFAULT_OPTIONS, 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 NpcDialog extends HandlebarsApplicationMixin(ApplicationV2) { static DEFAULT_OPTIONS = { id: 'mgt2-npc', classes: ['mgt2-npc-dialog'], position: { width: 720, height: 'auto', }, window: { title: 'PNJ & Rencontres – MgT2e', resizable: true, }, }; static PARTS = { main: { template: `modules/${MODULE_ID}/templates/npc-dialog.hbs`, root: true, }, }; constructor(options = {}) { super(options); this._activeTab = options.initialTab ?? 'npc'; this._formData = { npc: { relation: options.relation ?? 'contact', experienceBias: 'random', createActor: false, actorName: '', openCreatedActor: true, }, encounter: { context: options.context ?? 'starport', includeFollowUp: true, }, mission: {}, traveller: { citizenCategory: DEFAULT_OPTIONS.citizenCategory, experience: DEFAULT_OPTIONS.experience, role: DEFAULT_OPTIONS.role, gender: DEFAULT_OPTIONS.gender, firstName: '', surname: '', useRandomName: true, createActor: DEFAULT_OPTIONS.createActor, actorName: '', openCreatedActor: DEFAULT_OPTIONS.openCreatedActor, }, ae: { relation: options.relation ?? 'contact', includeSpecial: true, createActor: false, actorName: '', openCreatedActor: true, }, }; } async _prepareContext() { registerHandlebarsHelpers(); return { ...this._formData, activeTab: this._activeTab, relations: Object.entries(NPC_RELATIONS).map(([key, value]) => ({ key, label: value.label })), 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-npc-form'); this._applyThemeStyles(html); html.find('[data-action="generate-npc"]').on('click', async (event) => { event.preventDefault(); this._readForm(html); await this._handleNpc(); }); html.find('[data-action="generate-encounter"]').on('click', async (event) => { event.preventDefault(); this._readForm(html); await this._handleEncounter(); }); html.find('[data-action="generate-mission"]').on('click', async (event) => { event.preventDefault(); this._readForm(html); await this._handleMission(); }); html.find('.tabs .item').on('click', (event) => { event.preventDefault(); this._readForm(html); this._activateTab($(event.currentTarget).data('tab')); }); // Gestion des événements pour l'onglet PNJ Détaillé (Traveller) html.find('[data-action="generate-traveller-npc"]').on('click', async (event) => { event.preventDefault(); this._readForm(html); await this._handleTravellerNpc(); }); html.find('[data-action="generate-ally-enemy"]').on('click', async (event) => { event.preventDefault(); this._readForm(html); await this._handleAllyEnemy(); }); html.find('[data-action="randomize-name"]').on('click', (event) => { event.preventDefault(); this._randomizeTravellerName(html); }); html.find('[name="traveller.useRandomName"]').on('change', (event) => { const useRandom = event.target.checked; this._formData.traveller.useRandomName = useRandom; html.find('.traveller-name-fields').toggleClass('hidden', useRandom); }); // Initialiser l'affichage des champs de nom pour l'onglet Traveller html.find('.traveller-name-fields').toggleClass('hidden', this._formData.traveller.useRandomName); } _getForm() { return $(this.element).find('.window-content'); } _activateTab(tabId) { const html = this._getForm(); if (!html?.length) return; this._activeTab = tabId; html.find('.tabs .item').removeClass('active'); html.find(`.tabs .item[data-tab="${tabId}"]`).addClass('active'); html.find('.tab-content .tab').removeClass('active'); html.find(`.tab-content .tab[data-tab="${tabId}"]`).addClass('active'); this._applyThemeStyles(html); } _applyThemeStyles(html) { 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.npc.relation = html.find('[name="npc.relation"]').val(); this._formData.npc.experienceBias = html.find('[name="npc.experienceBias"]').val(); this._formData.npc.createActor = html.find('[name="npc.createActor"]').is(':checked'); this._formData.npc.actorName = html.find('[name="npc.actorName"]').val(); this._formData.npc.openCreatedActor = html.find('[name="npc.openCreatedActor"]').is(':checked'); this._formData.encounter.context = html.find('[name="encounter.context"]').val(); this._formData.encounter.includeFollowUp = html.find('[name="encounter.includeFollowUp"]').is(':checked'); // Données pour l'onglet Alliés/Ennemis this._formData.ae.relation = html.find('[name="ae.relation"]').val(); this._formData.ae.includeSpecial = html.find('[name="ae.includeSpecial"]').is(':checked'); this._formData.ae.createActor = html.find('[name="ae.createActor"]').is(':checked'); this._formData.ae.actorName = html.find('[name="ae.actorName"]').val(); this._formData.ae.openCreatedActor = html.find('[name="ae.openCreatedActor"]').is(':checked'); // Données pour l'onglet PNJ Détaillé (Traveller) this._formData.traveller.citizenCategory = html.find('[name="traveller.citizenCategory"]').val(); this._formData.traveller.experience = html.find('[name="traveller.experience"]').val(); this._formData.traveller.role = html.find('[name="traveller.role"]').val(); this._formData.traveller.gender = html.find('[name="traveller.gender"]').val(); this._formData.traveller.firstName = html.find('[name="traveller.firstName"]').val(); this._formData.traveller.surname = html.find('[name="traveller.surname"]').val(); this._formData.traveller.useRandomName = html.find('[name="traveller.useRandomName"]').is(':checked'); this._formData.traveller.createActor = html.find('[name="traveller.createActor"]').is(':checked'); this._formData.traveller.actorName = html.find('[name="traveller.actorName"]').val(); this._formData.traveller.openCreatedActor = html.find('[name="traveller.openCreatedActor"]').is(':checked'); } async _handleNpc() { const result = await generateQuickNpc(this._formData.npc); if (this._formData.npc.createActor) { const actor = await createNpcActor(result, { name: this._formData.npc.actorName, openSheet: this._formData.npc.openCreatedActor, }); result.createdActor = { id: actor.id, name: actor.name }; ui.notifications.info(`Fiche PNJ créée : ${actor.name}`); } await this._postToChatResult(result); } async _handleEncounter() { const result = await generateEncounter(this._formData.encounter); await this._postToChatResult(result); } async _handleMission() { const result = await generateClientMission(); await this._postToChatResult(result); } async _handleTravellerNpc() { const button = $(this.element).find('[data-action="generate-traveller-npc"]'); const originalLabel = button.html(); try { button.prop('disabled', true).html(' Génération...'); const generateOptions = { citizenCategory: this._formData.traveller.citizenCategory, experience: this._formData.traveller.experience, role: this._formData.traveller.role, gender: this._formData.traveller.gender, createActor: this._formData.traveller.createActor, actorName: this._formData.traveller.actorName, openCreatedActor: this._formData.traveller.openCreatedActor }; if (!this._formData.traveller.useRandomName && this._formData.traveller.firstName && this._formData.traveller.surname) { generateOptions.firstName = this._formData.traveller.firstName; generateOptions.surname = this._formData.traveller.surname; } const result = await generateAndCreateTravellerNpc(generateOptions); if (result.success) { 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 { button.prop('disabled', false).html(originalLabel); } } async _handleAllyEnemy() { const button = $(this.element).find('[data-action="generate-ally-enemy"]'); const originalLabel = button.html(); try { button.prop('disabled', true).html(' Génération...'); const result = await generateAllyEnemy(this._formData.ae.relation, { includeSpecial: this._formData.ae.includeSpecial, }); if (result.success) { if (this._formData.ae.createActor) { const ae = this._formData.ae; const actorName = ae.actorName?.trim() || `PNJ — ${result.relation.label}`; const baseActorSystem = game.system?.id === 'mgt2e' ? await (await import('./travellerNpcGenerator.js')).getMgt2eBaseActorSystem() : null; const actorData = { name: actorName, type: 'npc', img: 'systems/mgt2e/icons/cargo/passenger-middle.svg', system: { settings: foundry.utils.mergeObject(foundry.utils.deepClone(baseActorSystem?.settings ?? {}), { hideUntrained: true, lockCharacteristics: true, }), sophont: foundry.utils.mergeObject(foundry.utils.deepClone(baseActorSystem?.sophont ?? {}), { age: 18, homeworld: '', profession: result.relation.label, }), characteristics: foundry.utils.deepClone(baseActorSystem?.characteristics ?? {}), hits: foundry.utils.deepClone(baseActorSystem?.hits ?? {}), skills: foundry.utils.deepClone(baseActorSystem?.skills ?? {}), }, flags: { [MODULE_ID]: { generatedAllyEnemy: { relation: result.relation.key } }, }, }; const actor = await Actor.create(actorData, { renderSheet: false }); result.createdActor = { id: actor.id, name: actor.name }; if (ae.openCreatedActor) actor.sheet?.render(true); ui.notifications.info(`Fiche PNJ créée : ${actor.name}`); } await this._postToChatResult(result); } else { ui.notifications.error('Erreur lors de la génération de la relation'); } } catch (error) { console.error(`${MODULE_ID} | Erreur AE:`, error); ui.notifications.error(`Erreur: ${error.message}`); } finally { button.prop('disabled', false).html(originalLabel); } } _randomizeTravellerName(html) { const name = generateRandomName(this._formData.traveller.gender); html.find('[name="traveller.firstName"]').val(name.firstName); html.find('[name="traveller.surname"]').val(name.surname); this._formData.traveller.firstName = name.firstName; this._formData.traveller.surname = name.surname; this._formData.traveller.useRandomName = false; html.find('[name="traveller.useRandomName"]').prop('checked', false); html.find('.traveller-name-fields').removeClass('hidden'); } async _postToChatResult(data) { registerHandlebarsHelpers(); let template = `modules/${MODULE_ID}/templates/npc-result.hbs`; let resultType = 'npc-result'; if (data.type === 'traveller-npc' || data?.flags?.[MODULE_ID]?.type === 'traveller-npc-result') { template = `modules/${MODULE_ID}/templates/traveller-npc-result.hbs`; resultType = 'traveller-npc-result'; } else if (data.type === 'ally-enemy') { template = `modules/${MODULE_ID}/templates/ally-enemy-result.hbs`; resultType = 'ally-enemy-result'; } const html = await foundry.applications.handlebars.renderTemplate(template, data); await ChatMessage.create({ content: html, speaker: ChatMessage.getSpeaker(), flags: { [MODULE_ID]: { type: resultType } }, }); } } let helpersRegistered = false; function registerHandlebarsHelpers() { if (helpersRegistered) return; helpersRegistered = true; // Helpers existants pour NPC Handlebars.registerHelper('eq', (a, b) => a === b); Handlebars.registerHelper('join', (arr, sep) => (Array.isArray(arr) ? arr.join(sep) : '')); Handlebars.registerHelper('formatCredits', (amount) => formatCredits(amount)); Handlebars.registerHelper('contains', (text, search) => String(text ?? '').includes(search)); // Helper pour localiser une compétence (ex: 'pilot' -> 'Pilote') Handlebars.registerHelper('localizeSkill', (skillFqn) => { if (!skillFqn) return ''; return localizeSkill(String(skillFqn)); }); // Helper pour joindre un tableau de compétences en les localisant Handlebars.registerHelper('joinLocalizedSkills', (arr, sep = ', ') => { if (!Array.isArray(arr)) return ''; return arr.map(skill => localizeSkill(String(skill))).join(sep); }); // Helpers pour Traveller NPC 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 (Difficulté Modificateur) 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] : ''; }); const RELATION_LABELS = Object.entries(NPC_RELATIONS).reduce((acc, [key, val]) => { acc[key] = val.label; return acc; }, {}); Handlebars.registerHelper('lookupRelationKey', (key) => RELATION_LABELS[key] || key); Handlebars.registerHelper('formatSigned', (value) => formatSigned(value)); }