459 lines
17 KiB
JavaScript
459 lines
17 KiB
JavaScript
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('<i class="fas fa-spinner fa-spin"></i> 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('<i class="fas fa-spinner fa-spin"></i> 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));
|
||
}
|