This commit is contained in:
+177
-164
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MGT2 Commerce – CommerceDialog
|
||||
*
|
||||
* Boîte de dialogue principale (FormApplication FoundryVTT).
|
||||
* Boîte de dialogue principale (ApplicationV2 FoundryVTT).
|
||||
* Trois onglets : Passagers / Cargaison / Commerce spéculatif.
|
||||
* Les résultats sont postés dans le chat.
|
||||
*/
|
||||
@@ -10,13 +10,33 @@ import { calculatePassengers, calculateCargo, findAvailableGoods, calculatePrice
|
||||
import { searchWorlds, fetchWorldDetail, fetchWorldCoordinates, calcParsecs } from './travellerMapApi.js';
|
||||
import { buildActiveActorContext, COMMERCE_SKILLS, getActiveTravellerActor, rollActorSkillEffect, getActorSkillSummary } from './mgt2eSkills.js';
|
||||
|
||||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
|
||||
|
||||
export class CommerceDialog extends FormApplication {
|
||||
export class CommerceDialog extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
static DEFAULT_OPTIONS = {
|
||||
id: 'mgt2-commerce',
|
||||
classes: ['mgt2-commerce-dialog'],
|
||||
position: {
|
||||
width: 780,
|
||||
height: 'auto',
|
||||
},
|
||||
window: {
|
||||
title: 'Commerce – MgT2e',
|
||||
resizable: true,
|
||||
},
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: `modules/${MODULE_ID}/templates/commerce-dialog.hbs`,
|
||||
root: true,
|
||||
},
|
||||
};
|
||||
|
||||
constructor(options = {}) {
|
||||
super({}, options);
|
||||
// Valeurs par défaut du formulaire
|
||||
super(options);
|
||||
this._activeTab = options.initialTab ?? 'passengers';
|
||||
this._formData = {
|
||||
pax: {
|
||||
uwpDep: '', uwpDest: '', zoneDep: 'normal', zoneDest: 'normal',
|
||||
@@ -30,61 +50,48 @@ export class CommerceDialog extends FormApplication {
|
||||
uwp: '', zone: 'normal', brokerSkill: 0, previousAttempts: 0, blackMarket: false,
|
||||
},
|
||||
};
|
||||
this._tradeGoods = null; // résultats de findAvailableGoods, conservés pour le calcul des prix
|
||||
this._tradeGoods = null;
|
||||
}
|
||||
|
||||
static get defaultOptions() {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
id: 'mgt2-commerce',
|
||||
title: 'Commerce – MgT2e',
|
||||
template: `modules/${MODULE_ID}/templates/commerce-dialog.hbs`,
|
||||
width: 780,
|
||||
height: 'auto',
|
||||
resizable: true,
|
||||
tabs: [{
|
||||
navSelector: '.tabs',
|
||||
contentSelector: '.tab-content',
|
||||
initial: 'passengers',
|
||||
}],
|
||||
classes: ['mgt2-commerce-dialog'],
|
||||
});
|
||||
}
|
||||
|
||||
getData() {
|
||||
async _prepareContext() {
|
||||
_registerHandlebarsHelpers();
|
||||
return foundry.utils.mergeObject(super.getData(), {
|
||||
return {
|
||||
...this._formData,
|
||||
activeActor: buildActiveActorContext(),
|
||||
});
|
||||
activeTab: this._activeTab,
|
||||
};
|
||||
}
|
||||
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
async _onRender(context, options) {
|
||||
await super._onRender(context, options);
|
||||
|
||||
const html = this._getForm();
|
||||
if (!html?.length) return;
|
||||
html.addClass('mgt2-commerce-form');
|
||||
|
||||
this._applyThemeStyles(html);
|
||||
|
||||
// Bouton "Calculer les passagers"
|
||||
html.find('[data-action="calculate-passengers"]').on('click', async (ev) => {
|
||||
ev.preventDefault();
|
||||
this._readForm(html);
|
||||
await this._handlePassengers();
|
||||
});
|
||||
|
||||
// Bouton "Calculer la cargaison"
|
||||
html.find('[data-action="calculate-cargo"]').on('click', async (ev) => {
|
||||
ev.preventDefault();
|
||||
this._readForm(html);
|
||||
await this._handleCargo();
|
||||
});
|
||||
|
||||
// Bouton "Trouver un fournisseur & marchandises"
|
||||
html.find('[data-action="find-goods"]').on('click', async (ev) => {
|
||||
ev.preventDefault();
|
||||
this._readForm(html);
|
||||
await this._handleFindGoods(html);
|
||||
});
|
||||
|
||||
// Bouton "Calculer les prix d'achat"
|
||||
html.on('click', '[data-action="calculate-buy-prices"]', async (ev) => {
|
||||
ev.preventDefault();
|
||||
this._readForm(html);
|
||||
await this._handleBuyPrices();
|
||||
});
|
||||
|
||||
@@ -113,16 +120,74 @@ export class CommerceDialog extends FormApplication {
|
||||
this._applyTradeActorData(html);
|
||||
});
|
||||
|
||||
// ─── Recherche de monde (Traveller Map API) ───────────────────────────────
|
||||
html.find('.tabs .item').on('click', (ev) => {
|
||||
ev.preventDefault();
|
||||
this._readForm(html);
|
||||
this._activateTab($(ev.currentTarget).data('tab'));
|
||||
});
|
||||
|
||||
this._bindWorldSearch(html);
|
||||
|
||||
html.on('click', (ev) => {
|
||||
if (!$(ev.target).closest('.world-search-widget').length) {
|
||||
html.find('.world-search-results').empty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_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'
|
||||
});
|
||||
}
|
||||
|
||||
_bindWorldSearch(html) {
|
||||
html.find('.world-search-widget').each((_, widget) => {
|
||||
const $widget = $(widget);
|
||||
const $input = $widget.find('.world-search-input');
|
||||
const $btn = $widget.find('.btn-world-search');
|
||||
const $widget = $(widget);
|
||||
const $input = $widget.find('.world-search-input');
|
||||
const $btn = $widget.find('.btn-world-search');
|
||||
const $results = $widget.find('.world-search-results');
|
||||
const uwpTarget = $widget.data('uwp-target');
|
||||
const zoneTarget = $widget.data('zone-target');
|
||||
const uwpTarget = $widget.data('uwp-target');
|
||||
const zoneTarget = $widget.data('zone-target');
|
||||
const parsecsTarget = $widget.data('parsecs-target') || null;
|
||||
const role = $widget.data('role') || null;
|
||||
const role = $widget.data('role') || null;
|
||||
|
||||
const doSearch = async () => {
|
||||
const query = $input.val().trim();
|
||||
@@ -134,27 +199,23 @@ export class CommerceDialog extends FormApplication {
|
||||
if (!worlds.length) {
|
||||
$results.append('<li class="no-result">Aucun monde trouvé</li>');
|
||||
} else {
|
||||
worlds.slice(0, 10).forEach(w => {
|
||||
worlds.slice(0, 10).forEach((w) => {
|
||||
const $li = $(`<li data-sector="${w.sector}" data-hex="${w.hex}"><span class="world-name">${w.name}</span> <span class="world-uwp">${w.uwp}</span> <span class="world-sector">${w.sector}</span></li>`);
|
||||
$li.on('click', async () => {
|
||||
$results.empty();
|
||||
$input.val(w.name);
|
||||
// Récupère les détails (UWP précis + zone) et les coordonnées en parallèle
|
||||
const [detail, coords] = await Promise.all([
|
||||
fetchWorldDetail(w.sector, w.hex).catch(() => null),
|
||||
fetchWorldCoordinates(w.sector, w.hex).catch(() => null),
|
||||
]);
|
||||
|
||||
const resolvedUwp = detail ? detail.uwp : w.uwp;
|
||||
const resolvedUwp = detail ? detail.uwp : w.uwp;
|
||||
const resolvedZone = detail ? detail.zone : 'normal';
|
||||
|
||||
html.find(`[name="${uwpTarget}"]`).val(resolvedUwp);
|
||||
html.find(`[name="${zoneTarget}"]`).val(resolvedZone);
|
||||
|
||||
// Stocker les coordonnées dans le widget courant
|
||||
$widget.data('coords', coords);
|
||||
|
||||
// Synchronisation inter-onglets : départ Passagers → départ Cargaison + monde Commerce
|
||||
if (uwpTarget === 'pax.uwpDep') {
|
||||
const $cargoDep = html.find('.world-search-widget[data-uwp-target="cargo.uwpDep"]');
|
||||
$cargoDep.find('.world-search-input').val(w.name);
|
||||
@@ -168,70 +229,62 @@ export class CommerceDialog extends FormApplication {
|
||||
html.find('[name="trade.zone"]').val(resolvedZone);
|
||||
}
|
||||
|
||||
// Calculer les parsecs si le widget partenaire a aussi ses coordonnées
|
||||
if (parsecsTarget) {
|
||||
const partnerRole = role === 'dep' ? 'dest' : 'dep';
|
||||
const $partner = html.find(
|
||||
`.world-search-widget[data-parsecs-target="${parsecsTarget}"][data-role="${partnerRole}"]`
|
||||
);
|
||||
const $partner = html.find(`.world-search-widget[data-parsecs-target="${parsecsTarget}"][data-role="${partnerRole}"]`);
|
||||
const partnerCoords = $partner.data('coords');
|
||||
if (coords && partnerCoords) {
|
||||
const dist = calcParsecs(coords, partnerCoords);
|
||||
html.find(`[name="${parsecsTarget}"]`).val(dist);
|
||||
}
|
||||
}
|
||||
|
||||
this._readForm(html);
|
||||
});
|
||||
$results.append($li);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
$results.append('<li class="no-result">Erreur de connexion à Traveller Map</li>');
|
||||
}
|
||||
$btn.prop('disabled', false).html('<i class="fas fa-globe"></i> Chercher');
|
||||
};
|
||||
|
||||
$btn.on('click', doSearch);
|
||||
$input.on('keydown', (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); doSearch(); } });
|
||||
});
|
||||
|
||||
// Fermer les listes de résultats en cliquant ailleurs
|
||||
html.on('click', (ev) => {
|
||||
if (!$(ev.target).closest('.world-search-widget').length) {
|
||||
html.find('.world-search-results').empty();
|
||||
}
|
||||
$input.on('keydown', (ev) => {
|
||||
if (ev.key === 'Enter') {
|
||||
ev.preventDefault();
|
||||
doSearch();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Lecture du formulaire ─────────────────────────────────────────────────
|
||||
|
||||
_readForm(html) {
|
||||
// Passagers
|
||||
this._formData.pax.uwpDep = html.find('[name="pax.uwpDep"]').val();
|
||||
this._formData.pax.uwpDest = html.find('[name="pax.uwpDest"]').val();
|
||||
this._formData.pax.zoneDep = html.find('[name="pax.zoneDep"]').val();
|
||||
this._formData.pax.zoneDest = html.find('[name="pax.zoneDest"]').val();
|
||||
this._formData.pax.parsecs = parseInt(html.find('[name="pax.parsecs"]').val()) || 1;
|
||||
this._formData.pax.skillEffect = parseInt(html.find('[name="pax.skillEffect"]').val()) || 0;
|
||||
this._formData.pax.stewardLevel = parseInt(html.find('[name="pax.stewardLevel"]').val()) || 0;
|
||||
this._formData.pax.uwpDep = html.find('[name="pax.uwpDep"]').val();
|
||||
this._formData.pax.uwpDest = html.find('[name="pax.uwpDest"]').val();
|
||||
this._formData.pax.zoneDep = html.find('[name="pax.zoneDep"]').val();
|
||||
this._formData.pax.zoneDest = html.find('[name="pax.zoneDest"]').val();
|
||||
this._formData.pax.parsecs = parseInt(html.find('[name="pax.parsecs"]').val(), 10) || 1;
|
||||
this._formData.pax.skillEffect = parseInt(html.find('[name="pax.skillEffect"]').val(), 10) || 0;
|
||||
this._formData.pax.stewardLevel = parseInt(html.find('[name="pax.stewardLevel"]').val(), 10) || 0;
|
||||
|
||||
// Cargaison
|
||||
this._formData.cargo.uwpDep = html.find('[name="cargo.uwpDep"]').val();
|
||||
this._formData.cargo.uwpDest = html.find('[name="cargo.uwpDest"]').val();
|
||||
this._formData.cargo.zoneDep = html.find('[name="cargo.zoneDep"]').val();
|
||||
this._formData.cargo.zoneDest = html.find('[name="cargo.zoneDest"]').val();
|
||||
this._formData.cargo.parsecs = parseInt(html.find('[name="cargo.parsecs"]').val()) || 1;
|
||||
this._formData.cargo.skillEffect = parseInt(html.find('[name="cargo.skillEffect"]').val()) || 0;
|
||||
this._formData.cargo.navyRank = parseInt(html.find('[name="cargo.navyRank"]').val()) || 0;
|
||||
this._formData.cargo.scoutRank = parseInt(html.find('[name="cargo.scoutRank"]').val()) || 0;
|
||||
this._formData.cargo.socMod = parseInt(html.find('[name="cargo.socMod"]').val()) || 0;
|
||||
this._formData.cargo.armed = html.find('[name="cargo.armed"]').is(':checked');
|
||||
this._formData.cargo.uwpDep = html.find('[name="cargo.uwpDep"]').val();
|
||||
this._formData.cargo.uwpDest = html.find('[name="cargo.uwpDest"]').val();
|
||||
this._formData.cargo.zoneDep = html.find('[name="cargo.zoneDep"]').val();
|
||||
this._formData.cargo.zoneDest = html.find('[name="cargo.zoneDest"]').val();
|
||||
this._formData.cargo.parsecs = parseInt(html.find('[name="cargo.parsecs"]').val(), 10) || 1;
|
||||
this._formData.cargo.skillEffect = parseInt(html.find('[name="cargo.skillEffect"]').val(), 10) || 0;
|
||||
this._formData.cargo.navyRank = parseInt(html.find('[name="cargo.navyRank"]').val(), 10) || 0;
|
||||
this._formData.cargo.scoutRank = parseInt(html.find('[name="cargo.scoutRank"]').val(), 10) || 0;
|
||||
this._formData.cargo.socMod = parseInt(html.find('[name="cargo.socMod"]').val(), 10) || 0;
|
||||
this._formData.cargo.armed = html.find('[name="cargo.armed"]').is(':checked');
|
||||
|
||||
// Commerce spéculatif
|
||||
this._formData.trade.uwp = html.find('[name="trade.uwp"]').val();
|
||||
this._formData.trade.zone = html.find('[name="trade.zone"]').val();
|
||||
this._formData.trade.brokerSkill = parseInt(html.find('[name="trade.brokerSkill"]').val()) || 0;
|
||||
this._formData.trade.previousAttempts = parseInt(html.find('[name="trade.previousAttempts"]').val()) || 0;
|
||||
this._formData.trade.blackMarket = html.find('[name="trade.blackMarket"]').is(':checked');
|
||||
this._formData.trade.uwp = html.find('[name="trade.uwp"]').val();
|
||||
this._formData.trade.zone = html.find('[name="trade.zone"]').val();
|
||||
this._formData.trade.brokerSkill = parseInt(html.find('[name="trade.brokerSkill"]').val(), 10) || 0;
|
||||
this._formData.trade.previousAttempts = parseInt(html.find('[name="trade.previousAttempts"]').val(), 10) || 0;
|
||||
this._formData.trade.blackMarket = html.find('[name="trade.blackMarket"]').is(':checked');
|
||||
}
|
||||
|
||||
_getActiveActorOrWarn() {
|
||||
@@ -297,12 +350,9 @@ export class CommerceDialog extends FormApplication {
|
||||
const label = normalized > 0 ? `+${normalized}` : `${normalized}`;
|
||||
select.append(`<option value="${normalized}">${label}</option>`);
|
||||
}
|
||||
|
||||
select.val(`${normalized}`);
|
||||
}
|
||||
|
||||
// ─── Passagers ─────────────────────────────────────────────────────────────
|
||||
|
||||
async _handlePassengers() {
|
||||
const p = this._formData.pax;
|
||||
if (!p.uwpDep || !p.uwpDest) {
|
||||
@@ -310,24 +360,19 @@ export class CommerceDialog extends FormApplication {
|
||||
}
|
||||
|
||||
const result = await calculatePassengers({
|
||||
uwpDep: p.uwpDep,
|
||||
uwpDest: p.uwpDest,
|
||||
zoneDep: p.zoneDep,
|
||||
zoneDest: p.zoneDest,
|
||||
parsecs: p.parsecs,
|
||||
uwpDep: p.uwpDep,
|
||||
uwpDest: p.uwpDest,
|
||||
zoneDep: p.zoneDep,
|
||||
zoneDest: p.zoneDest,
|
||||
parsecs: p.parsecs,
|
||||
skillEffect: p.skillEffect,
|
||||
stewardLevel: p.stewardLevel,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return ui.notifications.error(result.errors.join(' | '));
|
||||
}
|
||||
|
||||
if (!result.success) return ui.notifications.error(result.errors.join(' | '));
|
||||
await this._postToChatResult(result);
|
||||
}
|
||||
|
||||
// ─── Cargaison ─────────────────────────────────────────────────────────────
|
||||
|
||||
async _handleCargo() {
|
||||
const c = this._formData.cargo;
|
||||
if (!c.uwpDep || !c.uwpDest) {
|
||||
@@ -335,30 +380,23 @@ export class CommerceDialog extends FormApplication {
|
||||
}
|
||||
|
||||
const result = await calculateCargo({
|
||||
uwpDep: c.uwpDep,
|
||||
uwpDest: c.uwpDest,
|
||||
zoneDep: c.zoneDep,
|
||||
zoneDest: c.zoneDest,
|
||||
parsecs: c.parsecs,
|
||||
uwpDep: c.uwpDep,
|
||||
uwpDest: c.uwpDest,
|
||||
zoneDep: c.zoneDep,
|
||||
zoneDest: c.zoneDest,
|
||||
parsecs: c.parsecs,
|
||||
skillEffect: c.skillEffect,
|
||||
navyRank: c.navyRank,
|
||||
scoutRank: c.scoutRank,
|
||||
socMod: c.socMod,
|
||||
armed: c.armed,
|
||||
navyRank: c.navyRank,
|
||||
scoutRank: c.scoutRank,
|
||||
socMod: c.socMod,
|
||||
armed: c.armed,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return ui.notifications.error(result.errors.join(' | '));
|
||||
}
|
||||
|
||||
// Calcule le sous-total cargaison pour le template
|
||||
if (!result.success) return ui.notifications.error(result.errors.join(' | '));
|
||||
result.cargoRevenue = result.lots.reduce((s, l) => s + l.revenue, 0);
|
||||
|
||||
await this._postToChatResult(result);
|
||||
}
|
||||
|
||||
// ─── Commerce spéculatif – trouver les marchandises ────────────────────────
|
||||
|
||||
async _handleFindGoods(html) {
|
||||
const t = this._formData.trade;
|
||||
if (!t.uwp) {
|
||||
@@ -366,27 +404,23 @@ export class CommerceDialog extends FormApplication {
|
||||
}
|
||||
|
||||
const result = await findAvailableGoods({
|
||||
uwp: t.uwp,
|
||||
zone: t.zone,
|
||||
blackMarket: t.blackMarket,
|
||||
brokerSkill: t.brokerSkill,
|
||||
uwp: t.uwp,
|
||||
zone: t.zone,
|
||||
blackMarket: t.blackMarket,
|
||||
brokerSkill: t.brokerSkill,
|
||||
previousAttempts: t.previousAttempts,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return ui.notifications.error(result.errors.join(' | '));
|
||||
}
|
||||
if (!result.success) return ui.notifications.error(result.errors.join(' | '));
|
||||
|
||||
this._tradeGoods = result;
|
||||
|
||||
// Affiche un aperçu inline dans l'onglet
|
||||
const goodsDiv = html.find('.trade-goods-result');
|
||||
const listDiv = html.find('.trade-goods-list');
|
||||
const listDiv = html.find('.trade-goods-list');
|
||||
|
||||
if (result.goods.length === 0) {
|
||||
listDiv.html('<p><em>Aucune marchandise disponible sur ce monde.</em></p>');
|
||||
} else {
|
||||
const rows = result.goods.map(g => `
|
||||
const rows = result.goods.map((g) => `
|
||||
<div class="trade-good-item">
|
||||
<label>
|
||||
<input type="checkbox" class="good-select" data-d66="${g.d66}" checked>
|
||||
@@ -404,8 +438,6 @@ export class CommerceDialog extends FormApplication {
|
||||
await this._postToChatResult(result);
|
||||
}
|
||||
|
||||
// ─── Commerce spéculatif – calculer les prix d'achat ───────────────────────
|
||||
|
||||
async _handleBuyPrices() {
|
||||
if (!this._tradeGoods || !this._tradeGoods.goods.length) {
|
||||
return ui.notifications.warn('Veuillez d\'abord trouver les marchandises disponibles.');
|
||||
@@ -413,57 +445,42 @@ export class CommerceDialog extends FormApplication {
|
||||
|
||||
const brokerSkill = this._formData.trade.brokerSkill;
|
||||
const prices = [];
|
||||
|
||||
for (const g of this._tradeGoods.goods) {
|
||||
const priceResult = await calculatePrice({
|
||||
basePrice: g.basePrice,
|
||||
buyMod: g.buyMod,
|
||||
sellMod: g.sellMod,
|
||||
basePrice: g.basePrice,
|
||||
buyMod: g.buyMod,
|
||||
sellMod: g.sellMod,
|
||||
brokerSkill,
|
||||
mode: 'buy',
|
||||
});
|
||||
prices.push({
|
||||
name: g.name,
|
||||
tons: g.tons,
|
||||
basePrice: g.basePrice,
|
||||
diceResult: priceResult.diceResult,
|
||||
modifier: priceResult.modifier,
|
||||
total: priceResult.total,
|
||||
percent: priceResult.percent,
|
||||
name: g.name,
|
||||
tons: g.tons,
|
||||
basePrice: g.basePrice,
|
||||
diceResult: priceResult.diceResult,
|
||||
modifier: priceResult.modifier,
|
||||
total: priceResult.total,
|
||||
percent: priceResult.percent,
|
||||
actualPrice: priceResult.actualPrice,
|
||||
totalCost: priceResult.actualPrice * g.tons,
|
||||
totalCost: priceResult.actualPrice * g.tons,
|
||||
});
|
||||
}
|
||||
|
||||
const grandTotal = prices.reduce((s, p) => s + p.totalCost, 0);
|
||||
|
||||
await this._postToChatResult({ type: 'buy-prices', prices, grandTotal });
|
||||
}
|
||||
|
||||
// ─── Post en chat ───────────────────────────────────────────────────────────
|
||||
|
||||
async _postToChatResult(data) {
|
||||
// Ajoute les helpers Handlebars nécessaires (si pas déjà enregistrés)
|
||||
_registerHandlebarsHelpers();
|
||||
|
||||
const html = await renderTemplate(
|
||||
`modules/${MODULE_ID}/templates/commerce-result.hbs`,
|
||||
data,
|
||||
);
|
||||
|
||||
const html = await foundry.applications.handlebars.renderTemplate(`modules/${MODULE_ID}/templates/commerce-result.hbs`, data);
|
||||
await ChatMessage.create({
|
||||
content: html,
|
||||
speaker: ChatMessage.getSpeaker(),
|
||||
flags: { [MODULE_ID]: { type: 'commerce-result' } },
|
||||
flags: { [MODULE_ID]: { type: 'commerce-result' } },
|
||||
});
|
||||
}
|
||||
|
||||
/** Nécessaire pour FormApplication : ne soumet rien (tout est piloté par boutons). */
|
||||
async _updateObject(_event, _formData) {}
|
||||
}
|
||||
|
||||
// ─── Helpers Handlebars ───────────────────────────────────────────────────────
|
||||
|
||||
let _helpersRegistered = false;
|
||||
|
||||
function _registerHandlebarsHelpers() {
|
||||
@@ -472,19 +489,15 @@ function _registerHandlebarsHelpers() {
|
||||
|
||||
Handlebars.registerHelper('formatCredits', (amount) => formatCredits(amount));
|
||||
Handlebars.registerHelper('join', (arr, sep) => (Array.isArray(arr) ? arr.join(sep) : ''));
|
||||
Handlebars.registerHelper('gt', (a, b) => a > b);
|
||||
Handlebars.registerHelper('gt', (a, b) => a > b);
|
||||
Handlebars.registerHelper('gte', (a, b) => a >= b);
|
||||
Handlebars.registerHelper('eq', (a, b) => a === b);
|
||||
|
||||
/** Classe CSS selon le signe du modificateur. */
|
||||
Handlebars.registerHelper('modClass', (n) => Number(n) > 0 ? 'mod-pos' : Number(n) < 0 ? 'mod-neg' : 'mod-zero');
|
||||
|
||||
/** Génère un <select> numérique de min à max, avec +N pour les positifs. */
|
||||
Handlebars.registerHelper('eq', (a, b) => a === b);
|
||||
Handlebars.registerHelper('modClass', (n) => (Number(n) > 0 ? 'mod-pos' : Number(n) < 0 ? 'mod-neg' : 'mod-zero'));
|
||||
Handlebars.registerHelper('modSelect', function(id, name, value, min, max) {
|
||||
let opts = '';
|
||||
for (let i = min; i <= max; i++) {
|
||||
const label = i > 0 ? `+${i}` : `${i}`;
|
||||
const sel = value === i ? ' selected' : '';
|
||||
const sel = value === i ? ' selected' : '';
|
||||
opts += `<option value="${i}"${sel}>${label}</option>`;
|
||||
}
|
||||
return new Handlebars.SafeString(`<select id="${id}" name="${name}">${opts}</select>`);
|
||||
|
||||
+86
-35
@@ -2,11 +2,33 @@ import { formatCredits } from './tradeHelper.js';
|
||||
import { createNpcActor, generateClientMission, generateEncounter, generateQuickNpc } from './npcHelper.js';
|
||||
import { NPC_RELATIONS } from './data/npcTables.js';
|
||||
|
||||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
|
||||
|
||||
export class NpcDialog extends FormApplication {
|
||||
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);
|
||||
super(options);
|
||||
this._activeTab = options.initialTab ?? 'npc';
|
||||
this._formData = {
|
||||
npc: {
|
||||
relation: options.relation ?? 'contact',
|
||||
@@ -22,33 +44,22 @@ export class NpcDialog extends FormApplication {
|
||||
};
|
||||
}
|
||||
|
||||
static get defaultOptions() {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
id: 'mgt2-npc',
|
||||
title: 'PNJ & Rencontres – MgT2e',
|
||||
template: `modules/${MODULE_ID}/templates/npc-dialog.hbs`,
|
||||
width: 720,
|
||||
height: 'auto',
|
||||
resizable: true,
|
||||
tabs: [{
|
||||
navSelector: '.tabs',
|
||||
contentSelector: '.tab-content',
|
||||
initial: 'npc',
|
||||
}],
|
||||
classes: ['mgt2-npc-dialog'],
|
||||
});
|
||||
}
|
||||
|
||||
getData() {
|
||||
async _prepareContext() {
|
||||
registerHandlebarsHelpers();
|
||||
return foundry.utils.mergeObject(super.getData(), {
|
||||
return {
|
||||
...this._formData,
|
||||
activeTab: this._activeTab,
|
||||
relations: Object.entries(NPC_RELATIONS).map(([key, value]) => ({ key, label: value.label })),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
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();
|
||||
@@ -64,12 +75,58 @@ export class NpcDialog extends FormApplication {
|
||||
|
||||
html.find('[data-action="generate-mission"]').on('click', async (event) => {
|
||||
event.preventDefault();
|
||||
this._readForm(html);
|
||||
await this._handleMission();
|
||||
});
|
||||
|
||||
if (this.options.initialTab) {
|
||||
this._tabs?.[0]?.activate(this.options.initialTab);
|
||||
}
|
||||
html.find('.tabs .item').on('click', (event) => {
|
||||
event.preventDefault();
|
||||
this._readForm(html);
|
||||
this._activateTab($(event.currentTarget).data('tab'));
|
||||
});
|
||||
}
|
||||
|
||||
_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) {
|
||||
@@ -89,10 +146,7 @@ export class NpcDialog extends FormApplication {
|
||||
name: this._formData.npc.actorName,
|
||||
openSheet: this._formData.npc.openCreatedActor,
|
||||
});
|
||||
result.createdActor = {
|
||||
id: actor.id,
|
||||
name: actor.name,
|
||||
};
|
||||
result.createdActor = { id: actor.id, name: actor.name };
|
||||
ui.notifications.info(`Fiche PNJ créée : ${actor.name}`);
|
||||
}
|
||||
await this._postToChatResult(result);
|
||||
@@ -110,8 +164,7 @@ export class NpcDialog extends FormApplication {
|
||||
|
||||
async _postToChatResult(data) {
|
||||
registerHandlebarsHelpers();
|
||||
const renderHbs = foundry.applications?.handlebars?.renderTemplate ?? renderTemplate;
|
||||
const html = await renderHbs(`modules/${MODULE_ID}/templates/npc-result.hbs`, data);
|
||||
const html = await foundry.applications.handlebars.renderTemplate(`modules/${MODULE_ID}/templates/npc-result.hbs`, data);
|
||||
|
||||
await ChatMessage.create({
|
||||
content: html,
|
||||
@@ -119,8 +172,6 @@ export class NpcDialog extends FormApplication {
|
||||
flags: { [MODULE_ID]: { type: 'npc-result' } },
|
||||
});
|
||||
}
|
||||
|
||||
async _updateObject(_event, _formData) {}
|
||||
}
|
||||
|
||||
let helpersRegistered = false;
|
||||
|
||||
+101
-8
@@ -3,35 +3,128 @@
|
||||
*
|
||||
* Chargé par FoundryVTT via "esmodules" dans module.json.
|
||||
* Enregistre la commande /commerce dans le chat.
|
||||
* Compatible Foundry VTT v13 et v14
|
||||
*/
|
||||
|
||||
import { CommerceDialog } from './CommerceDialog.js';
|
||||
import './mgt2eMigration.js';
|
||||
import './startupNotice.js';
|
||||
|
||||
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
|
||||
const COMMERCE_COMMAND = 'commerce';
|
||||
const ChatLogV2 = foundry.applications.sidebar.tabs.ChatLog;
|
||||
|
||||
function openCommerceDialog() {
|
||||
new CommerceDialog().render({ force: true });
|
||||
}
|
||||
|
||||
function registerCommerceCommand() {
|
||||
if (!ChatLogV2?.CHAT_COMMANDS) {
|
||||
console.warn(`${MODULE_ID} | ChatLog.CHAT_COMMANDS indisponible, commande /${COMMERCE_COMMAND} non enregistrée`);
|
||||
return;
|
||||
}
|
||||
|
||||
ChatLogV2.CHAT_COMMANDS[COMMERCE_COMMAND] = {
|
||||
rgx: /^\/commerce(?:\s+(.*))?$/i,
|
||||
fn: () => {
|
||||
openCommerceDialog();
|
||||
return false;
|
||||
},
|
||||
};
|
||||
console.log(`${MODULE_ID} | Commande /${COMMERCE_COMMAND} enregistrée via ChatLog.CHAT_COMMANDS`);
|
||||
}
|
||||
|
||||
Hooks.once('init', () => {
|
||||
console.log(`${MODULE_ID} | Commerce module initialisé`);
|
||||
|
||||
// Pré-charge les templates Handlebars
|
||||
loadTemplates([
|
||||
`modules/${MODULE_ID}/templates/commerce-dialog.hbs`,
|
||||
`modules/${MODULE_ID}/templates/commerce-result.hbs`,
|
||||
]);
|
||||
// Compatibilité v13 et v14
|
||||
const loadTemplatesFn = foundry.applications?.handlebars?.loadTemplates || loadTemplates;
|
||||
if (loadTemplatesFn) {
|
||||
loadTemplatesFn([
|
||||
`modules/${MODULE_ID}/templates/commerce-dialog.hbs`,
|
||||
`modules/${MODULE_ID}/templates/commerce-result.hbs`,
|
||||
]);
|
||||
}
|
||||
|
||||
registerCommerceCommand();
|
||||
});
|
||||
|
||||
Hooks.once('ready', () => {
|
||||
console.log(`${MODULE_ID} | Commerce module prêt – tapez /commerce dans le chat`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Solution pour Foundry v14 : Intercepte les commandes AVANT la validation
|
||||
* Utilise renderChatInput pour modifier le comportement de l'input
|
||||
* Compatible v13 (jQuery) et v14 (DOM element)
|
||||
*/
|
||||
Hooks.on('renderChatInput', (app, html, data) => {
|
||||
// Foundry v14 passe un objet avec element, v13 passe jQuery
|
||||
let input;
|
||||
|
||||
// Vérifie si html est un objet avec element (v14)
|
||||
if (html?.element) {
|
||||
input = $(html.element).find('textarea[name="content"]');
|
||||
} else if (html?.find) {
|
||||
// v13 ou déjà jQuery
|
||||
input = html.find('textarea[name="content"]');
|
||||
} else {
|
||||
// Dernier recours : suppose que html est l'élément
|
||||
input = $(html).find('textarea[name="content"]');
|
||||
}
|
||||
|
||||
// Évite les doublons d'écouteurs
|
||||
if (input.data('mgt2-commerce-listener')) return;
|
||||
input.data('mgt2-commerce-listener', true);
|
||||
|
||||
input.on('keydown', (event) => {
|
||||
// Intercepte Entrée (13) et vérifie si c'est une de nos commandes
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
const content = input.val()?.trim();
|
||||
if (content?.startsWith('/commerce')) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
openCommerceDialog();
|
||||
input.val('');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Intercepte les messages de chat commençant par /commerce.
|
||||
* Utilise preCreateChatMessage pour Foundry v14+ (avant que le message ne soit validé)
|
||||
* Compatible avec Foundry v13 et v14
|
||||
* Retourne false pour empêcher l'envoi du message brut.
|
||||
*/
|
||||
Hooks.on('chatMessage', (_chatLog, message, _chatData) => {
|
||||
const trimmed = message.trim().toLowerCase();
|
||||
if (trimmed === '/commerce' || trimmed.startsWith('/commerce ')) {
|
||||
new CommerceDialog().render(true);
|
||||
Hooks.on('preCreateChatMessage', (message, data, options) => {
|
||||
const content = message.content?.trim()?.toLowerCase();
|
||||
if (content === '/commerce' || content?.startsWith('/commerce ')) {
|
||||
openCommerceDialog();
|
||||
return false; // Empêche la création du message
|
||||
}
|
||||
});
|
||||
|
||||
// Gardé pour compatibilité v13
|
||||
Hooks.on('chatMessage', (...args) => {
|
||||
// Foundry v14 passe un objet ChatMessage en premier paramètre
|
||||
// Foundry v13 passe (chatLog, message, chatData)
|
||||
let message;
|
||||
|
||||
if (args[0]?.content !== undefined) {
|
||||
// v14: premier argument est ChatMessage
|
||||
message = args[0].content;
|
||||
} else if (typeof args[1] === 'string') {
|
||||
// v13: deuxième argument est la string message
|
||||
message = args[1];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmed = message?.trim()?.toLowerCase();
|
||||
if (trimmed === '/commerce' || trimmed?.startsWith('/commerce ')) {
|
||||
openCommerceDialog();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
+123
-10
@@ -3,18 +3,44 @@ import { syncNpcRollTables } from './npcRollTableSync.js';
|
||||
import './mgt2eMigration.js';
|
||||
|
||||
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
|
||||
const ChatLogV2 = foundry.applications.sidebar.tabs.ChatLog;
|
||||
|
||||
function openNpcDialog(initialTab, options = {}) {
|
||||
new NpcDialog({ initialTab, ...options }).render(true);
|
||||
new NpcDialog({ initialTab, ...options }).render({ force: true });
|
||||
}
|
||||
|
||||
function registerNpcCommand(commandName, initialTab) {
|
||||
if (!ChatLogV2?.CHAT_COMMANDS) {
|
||||
console.warn(`${MODULE_ID} | ChatLog.CHAT_COMMANDS indisponible, commande /${commandName} non enregistrée`);
|
||||
return;
|
||||
}
|
||||
|
||||
ChatLogV2.CHAT_COMMANDS[commandName] = {
|
||||
rgx: new RegExp(`^\\/${commandName}(?:\\s+(.*))?$`, 'i'),
|
||||
fn: () => {
|
||||
openNpcDialog(initialTab);
|
||||
return false;
|
||||
},
|
||||
};
|
||||
console.log(`${MODULE_ID} | Commande /${commandName} enregistrée via ChatLog.CHAT_COMMANDS`);
|
||||
}
|
||||
|
||||
Hooks.once('init', () => {
|
||||
console.log(`${MODULE_ID} | Outils PNJ initialisés`);
|
||||
|
||||
loadTemplates([
|
||||
`modules/${MODULE_ID}/templates/npc-dialog.hbs`,
|
||||
`modules/${MODULE_ID}/templates/npc-result.hbs`,
|
||||
]);
|
||||
// Pré-charge les templates Handlebars
|
||||
// Compatibilité v13 et v14
|
||||
const loadTemplatesFn = foundry.applications?.handlebars?.loadTemplates || loadTemplates;
|
||||
if (loadTemplatesFn) {
|
||||
loadTemplatesFn([
|
||||
`modules/${MODULE_ID}/templates/npc-dialog.hbs`,
|
||||
`modules/${MODULE_ID}/templates/npc-result.hbs`,
|
||||
]);
|
||||
}
|
||||
|
||||
registerNpcCommand('pnj', 'npc');
|
||||
registerNpcCommand('rencontre', 'encounter');
|
||||
registerNpcCommand('mission', 'mission');
|
||||
});
|
||||
|
||||
Hooks.once('ready', async () => {
|
||||
@@ -22,20 +48,107 @@ Hooks.once('ready', async () => {
|
||||
console.log(`${MODULE_ID} | Outils PNJ prêts – tapez /pnj, /rencontre ou /mission dans le chat`);
|
||||
});
|
||||
|
||||
Hooks.on('chatMessage', (_chatLog, message, _chatData) => {
|
||||
const trimmed = message.trim().toLowerCase();
|
||||
/**
|
||||
* Solution pour Foundry v14 : Intercepte les commandes AVANT la validation
|
||||
* Utilise renderChatInput pour modifier le comportement de l'input
|
||||
* Compatible v13 (jQuery) et v14 (DOM element)
|
||||
*/
|
||||
Hooks.on('renderChatInput', (app, html, data) => {
|
||||
// Foundry v14 passe un objet avec element, v13 passe jQuery
|
||||
let input;
|
||||
|
||||
// Vérifie si html est un objet avec element (v14)
|
||||
if (html?.element) {
|
||||
input = $(html.element).find('textarea[name="content"]');
|
||||
} else if (html?.find) {
|
||||
// v13 ou déjà jQuery
|
||||
input = html.find('textarea[name="content"]');
|
||||
} else {
|
||||
// Dernier recours : suppose que html est l'élément
|
||||
input = $(html).find('textarea[name="content"]');
|
||||
}
|
||||
|
||||
// Évite les doublons d'écouteurs
|
||||
if (input.data('mgt2-npc-listener')) return;
|
||||
input.data('mgt2-npc-listener', true);
|
||||
|
||||
input.on('keydown', (event) => {
|
||||
// Intercepte Entrée (13) et vérifie si c'est une de nos commandes
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
const content = input.val()?.trim();
|
||||
if (content?.startsWith('/pnj')) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
openNpcDialog('npc');
|
||||
input.val('');
|
||||
} else if (content?.startsWith('/rencontre')) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
openNpcDialog('encounter');
|
||||
input.val('');
|
||||
} else if (content?.startsWith('/mission')) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
openNpcDialog('mission');
|
||||
input.val('');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (trimmed === '/pnj' || trimmed.startsWith('/pnj ')) {
|
||||
/**
|
||||
* Intercepte les messages de chat pour /pnj, /rencontre, /mission
|
||||
* Utilise preCreateChatMessage pour Foundry v14+ (avant que le message ne soit validé)
|
||||
* Compatible avec Foundry v13 et v14
|
||||
*/
|
||||
Hooks.on('preCreateChatMessage', (message, data, options) => {
|
||||
const content = message.content?.trim()?.toLowerCase();
|
||||
|
||||
if (content === '/pnj' || content?.startsWith('/pnj ')) {
|
||||
openNpcDialog('npc');
|
||||
return false; // Empêche la création du message
|
||||
}
|
||||
|
||||
if (content === '/rencontre' || content?.startsWith('/rencontre ')) {
|
||||
openNpcDialog('encounter');
|
||||
return false; // Empêche la création du message
|
||||
}
|
||||
|
||||
if (content === '/mission' || content?.startsWith('/mission ')) {
|
||||
openNpcDialog('mission');
|
||||
return false; // Empêche la création du message
|
||||
}
|
||||
});
|
||||
|
||||
// Gardé pour compatibilité v13
|
||||
Hooks.on('chatMessage', (...args) => {
|
||||
// Foundry v14 passe un objet ChatMessage en premier paramètre
|
||||
// Foundry v13 passe (chatLog, message, chatData)
|
||||
let message;
|
||||
|
||||
if (args[0]?.content !== undefined) {
|
||||
// v14: premier argument est ChatMessage
|
||||
message = args[0].content;
|
||||
} else if (typeof args[1] === 'string') {
|
||||
// v13: deuxième argument est la string message
|
||||
message = args[1];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmed = message?.trim()?.toLowerCase();
|
||||
|
||||
if (trimmed === '/pnj' || trimmed?.startsWith('/pnj ')) {
|
||||
openNpcDialog('npc');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (trimmed === '/rencontre' || trimmed.startsWith('/rencontre ')) {
|
||||
if (trimmed === '/rencontre' || trimmed?.startsWith('/rencontre ')) {
|
||||
openNpcDialog('encounter');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (trimmed === '/mission' || trimmed.startsWith('/mission ')) {
|
||||
if (trimmed === '/mission' || trimmed?.startsWith('/mission ')) {
|
||||
openNpcDialog('mission');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
|
||||
const STARTUP_NOTICE_FLAG = `${MODULE_ID}.startupNoticePosted`;
|
||||
|
||||
function isNoticeOwner() {
|
||||
if (!game.user?.isGM) return false;
|
||||
const activeGM = game.users?.activeGM;
|
||||
return !activeGM || activeGM.id === game.user.id;
|
||||
}
|
||||
|
||||
function buildStartupNoticeHtml() {
|
||||
return `
|
||||
<div style="padding:6px 8px;border-left:4px solid #c9a227;background:#f5f0e8;color:#2c2c3e;line-height:1.45;">
|
||||
<strong>MgT2e – outils</strong>
|
||||
<span style="color:#6a5422;">·</span>
|
||||
<code>/commerce</code> <code>/pnj</code> <code>/rencontre</code> <code>/mission</code>
|
||||
<div style="margin-top:2px;font-size:0.92em;color:#6a5422;">
|
||||
par <a href="https://www.uberwald.me" target="_blank" rel="noopener noreferrer">LeRatierBretonnien / Uberwald</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
Hooks.once('ready', async () => {
|
||||
if (!isNoticeOwner()) return;
|
||||
if (globalThis[STARTUP_NOTICE_FLAG]) return;
|
||||
|
||||
globalThis[STARTUP_NOTICE_FLAG] = true;
|
||||
|
||||
await ChatMessage.create({
|
||||
speaker: {
|
||||
alias: 'MgT2e - Compendium Amiral Denisov',
|
||||
},
|
||||
content: buildStartupNoticeHtml(),
|
||||
style: CONST.CHAT_MESSAGE_STYLES.OTHER,
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user