MAp management and helpers

This commit is contained in:
2026-06-02 00:16:08 +02:00
parent 49423f40f5
commit efe37b8a96
22 changed files with 1163 additions and 71 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+70
View File
@@ -32,3 +32,73 @@
2026/06/01-22:51:23.050617 7f52c4bfb6c0 Level-0 table #188: started
2026/06/01-22:51:23.083717 7f52c4bfb6c0 Level-0 table #188: 3847811 bytes OK
2026/06/01-22:51:23.090754 7f52c4bfb6c0 Delete type=0 #185
2026/06/01-22:54:31.041731 7f52c4bfb6c0 Level-0 table #190: started
2026/06/01-22:54:31.075783 7f52c4bfb6c0 Level-0 table #190: 3898158 bytes OK
2026/06/01-22:54:31.085709 7f52c4bfb6c0 Delete type=0 #187
2026/06/01-22:56:43.017283 7f52c4bfb6c0 Level-0 table #192: started
2026/06/01-22:56:43.056579 7f52c4bfb6c0 Level-0 table #192: 3942180 bytes OK
2026/06/01-22:56:43.067120 7f52c4bfb6c0 Delete type=0 #189
2026/06/01-22:56:43.067709 7f52c4bfb6c0 Compacting 4@0 + 2@1 files
2026/06/01-22:56:43.096331 7f52c4bfb6c0 Generated table #193@0: 12110 keys, 2146786 bytes
2026/06/01-22:56:43.123233 7f52c4bfb6c0 Generated table #194@0: 13796 keys, 1794145 bytes
2026/06/01-22:56:43.123251 7f52c4bfb6c0 Compacted 4@0 + 2@1 files => 3940931 bytes
2026/06/01-22:56:43.135664 7f52c4bfb6c0 compacted to: files[ 0 2 2 0 0 0 0 ]
2026/06/01-22:56:43.136005 7f52c4bfb6c0 Delete type=2 #183
2026/06/01-22:56:43.136216 7f52c4bfb6c0 Delete type=2 #184
2026/06/01-22:56:43.136413 7f52c4bfb6c0 Delete type=2 #186
2026/06/01-22:56:43.136721 7f52c4bfb6c0 Delete type=2 #188
2026/06/01-22:56:43.137001 7f52c4bfb6c0 Delete type=2 #190
2026/06/01-22:56:43.137270 7f52c4bfb6c0 Delete type=2 #192
2026/06/01-23:02:49.561038 7f52c4bfb6c0 Level-0 table #196: started
2026/06/01-23:02:49.590283 7f52c4bfb6c0 Level-0 table #196: 3996082 bytes OK
2026/06/01-23:02:49.596507 7f52c4bfb6c0 Delete type=0 #191
2026/06/01-23:05:28.996035 7f52c4bfb6c0 Level-0 table #198: started
2026/06/01-23:05:29.028796 7f52c4bfb6c0 Level-0 table #198: 4042529 bytes OK
2026/06/01-23:05:29.035710 7f52c4bfb6c0 Delete type=0 #195
2026/06/01-23:11:22.779385 7f52c4bfb6c0 Level-0 table #200: started
2026/06/01-23:11:22.817516 7f52c4bfb6c0 Level-0 table #200: 4095432 bytes OK
2026/06/01-23:11:22.823424 7f52c4bfb6c0 Delete type=0 #197
2026/06/01-23:19:54.339957 7f52c4bfb6c0 Level-0 table #202: started
2026/06/01-23:19:54.367799 7f52c4bfb6c0 Level-0 table #202: 4146311 bytes OK
2026/06/01-23:19:54.373753 7f52c4bfb6c0 Delete type=0 #199
2026/06/01-23:19:54.374306 7f52c4bfb6c0 Compacting 4@0 + 2@1 files
2026/06/01-23:19:54.395646 7f52c4bfb6c0 Generated table #203@0: 11893 keys, 2146843 bytes
2026/06/01-23:19:54.419845 7f52c4bfb6c0 Generated table #204@0: 15341 keys, 1997992 bytes
2026/06/01-23:19:54.419877 7f52c4bfb6c0 Compacted 4@0 + 2@1 files => 4144835 bytes
2026/06/01-23:19:54.426071 7f52c4bfb6c0 compacted to: files[ 0 2 2 0 0 0 0 ]
2026/06/01-23:19:54.426414 7f52c4bfb6c0 Delete type=2 #193
2026/06/01-23:19:54.426875 7f52c4bfb6c0 Delete type=2 #194
2026/06/01-23:19:54.427165 7f52c4bfb6c0 Delete type=2 #196
2026/06/01-23:19:54.427644 7f52c4bfb6c0 Delete type=2 #198
2026/06/01-23:19:54.428125 7f52c4bfb6c0 Delete type=2 #200
2026/06/01-23:19:54.428618 7f52c4bfb6c0 Delete type=2 #202
2026/06/01-23:23:22.212670 7f52c4bfb6c0 Level-0 table #206: started
2026/06/01-23:23:22.246031 7f52c4bfb6c0 Level-0 table #206: 4199267 bytes OK
2026/06/01-23:23:22.252324 7f52c4bfb6c0 Delete type=0 #201
2026/06/01-23:32:01.046035 7f52c4bfb6c0 Level-0 table #208: started
2026/06/01-23:32:01.083523 7f52c4bfb6c0 Level-0 table #208: 4245727 bytes OK
2026/06/01-23:32:01.090732 7f52c4bfb6c0 Delete type=0 #205
2026/06/01-23:34:50.675178 7f52c4bfb6c0 Level-0 table #210: started
2026/06/01-23:34:50.708986 7f52c4bfb6c0 Level-0 table #210: 4300427 bytes OK
2026/06/01-23:34:50.715366 7f52c4bfb6c0 Delete type=0 #207
2026/06/01-23:37:05.691865 7f52c4bfb6c0 Level-0 table #212: started
2026/06/01-23:37:05.728371 7f52c4bfb6c0 Level-0 table #212: 4351044 bytes OK
2026/06/01-23:37:05.735021 7f52c4bfb6c0 Delete type=0 #209
2026/06/01-23:37:05.735982 7f52c4bfb6c0 Compacting 4@0 + 2@1 files
2026/06/01-23:37:05.756894 7f52c4bfb6c0 Generated table #213@0: 11588 keys, 2145171 bytes
2026/06/01-23:37:05.783035 7f52c4bfb6c0 Generated table #214@0: 16666 keys, 2163321 bytes
2026/06/01-23:37:05.787317 7f52c4bfb6c0 Generated table #215@0: 308 keys, 41171 bytes
2026/06/01-23:37:05.787329 7f52c4bfb6c0 Compacted 4@0 + 2@1 files => 4349663 bytes
2026/06/01-23:37:05.793772 7f52c4bfb6c0 compacted to: files[ 0 3 2 0 0 0 0 ]
2026/06/01-23:37:05.793970 7f52c4bfb6c0 Delete type=2 #203
2026/06/01-23:37:05.794138 7f52c4bfb6c0 Delete type=2 #204
2026/06/01-23:37:05.794271 7f52c4bfb6c0 Delete type=2 #206
2026/06/01-23:37:05.794472 7f52c4bfb6c0 Delete type=2 #208
2026/06/01-23:37:05.794734 7f52c4bfb6c0 Delete type=2 #210
2026/06/01-23:37:05.794942 7f52c4bfb6c0 Delete type=2 #212
2026/06/01-23:54:42.760778 7f52c4bfb6c0 Level-0 table #217: started
2026/06/01-23:54:42.788274 7f52c4bfb6c0 Level-0 table #217: 4406061 bytes OK
2026/06/01-23:54:42.794452 7f52c4bfb6c0 Delete type=0 #211
2026/06/02-00:07:09.406466 7f52c4bfb6c0 Level-0 table #219: started
2026/06/02-00:07:09.441912 7f52c4bfb6c0 Level-0 table #219: 4455911 bytes OK
2026/06/02-00:07:09.447798 7f52c4bfb6c0 Delete type=0 #216
Binary file not shown.
+36 -1
View File
@@ -51,15 +51,36 @@ export class CommerceDialog extends HandlebarsApplicationMixin(ApplicationV2) {
},
};
this._tradeGoods = null;
this._worldNames = {};
if (options.defaultWorld) {
const w = options.defaultWorld;
this._defaultWorld = w;
this._worldNames['pax.uwpDep'] = w.name || '';
this._worldNames['cargo.uwpDep'] = w.name || '';
this._worldNames['trade.uwp'] = w.name || '';
if (w.uwp) this._formData.pax.uwpDep = w.uwp;
if (w.zone) this._formData.pax.zoneDep = w.zone;
if (w.uwp) this._formData.cargo.uwpDep = w.uwp;
if (w.zone) this._formData.cargo.zoneDep = w.zone;
if (w.uwp) this._formData.trade.uwp = w.uwp;
if (w.zone) this._formData.trade.zone = w.zone;
this._activeTab = 'trade';
}
}
async _prepareContext() {
_registerHandlebarsHelpers();
return {
const ctx = {
...this._formData,
activeActor: buildActiveActorContext(),
activeTab: this._activeTab,
};
if (this._defaultWorld) {
ctx.defaultWorldName = this._defaultWorld.name;
ctx.defaultWorldLoc = `${this._defaultWorld.sector || ''} ${this._defaultWorld.hex || ''}`.trim();
}
return ctx;
}
async _onRender(context, options) {
@@ -128,6 +149,12 @@ export class CommerceDialog extends HandlebarsApplicationMixin(ApplicationV2) {
this._bindWorldSearch(html);
// Pré-remplir les champs recherche avec le nom du monde
if (this._defaultWorld?.name) {
html.find('.world-search-widget[data-role="dep"] .world-search-input, .world-block-full .world-search-input')
.val(this._defaultWorld.name);
}
html.on('click', (ev) => {
if (!$(ev.target).closest('.world-search-widget').length) {
html.find('.world-search-results').empty();
@@ -204,6 +231,7 @@ export class CommerceDialog extends HandlebarsApplicationMixin(ApplicationV2) {
$li.on('click', async () => {
$results.empty();
$input.val(w.name);
if (uwpTarget) this._worldNames[uwpTarget] = w.name;
const [detail, coords] = await Promise.all([
fetchWorldDetail(w.sector, w.hex).catch(() => null),
fetchWorldCoordinates(w.sector, w.hex).catch(() => null),
@@ -222,11 +250,13 @@ export class CommerceDialog extends HandlebarsApplicationMixin(ApplicationV2) {
html.find('[name="cargo.uwpDep"]').val(resolvedUwp);
html.find('[name="cargo.zoneDep"]').val(resolvedZone);
$cargoDep.data('coords', coords);
this._worldNames['cargo.uwpDep'] = w.name;
const $tradeWorld = html.find('.world-search-widget[data-uwp-target="trade.uwp"]');
$tradeWorld.find('.world-search-input').val(w.name);
html.find('[name="trade.uwp"]').val(resolvedUwp);
html.find('[name="trade.zone"]').val(resolvedZone);
this._worldNames['trade.uwp'] = w.name;
}
if (parsecsTarget) {
@@ -370,6 +400,8 @@ export class CommerceDialog extends HandlebarsApplicationMixin(ApplicationV2) {
});
if (!result.success) return ui.notifications.error(result.errors.join(' | '));
result.dep = { ...result.dep, name: this._worldNames['pax.uwpDep'] || '' };
result.dest = { ...result.dest, name: this._worldNames['pax.uwpDest'] || '' };
await this._postToChatResult(result);
}
@@ -394,6 +426,8 @@ export class CommerceDialog extends HandlebarsApplicationMixin(ApplicationV2) {
if (!result.success) return ui.notifications.error(result.errors.join(' | '));
result.cargoRevenue = result.lots.reduce((s, l) => s + l.revenue, 0);
result.dep = { ...result.dep, name: this._worldNames['cargo.uwpDep'] || '' };
result.dest = { ...result.dest, name: this._worldNames['cargo.uwpDest'] || '' };
await this._postToChatResult(result);
}
@@ -413,6 +447,7 @@ export class CommerceDialog extends HandlebarsApplicationMixin(ApplicationV2) {
if (!result.success) return ui.notifications.error(result.errors.join(' | '));
result.world = { ...result.world, name: this._worldNames['trade.uwp'] || '' };
this._tradeGoods = result;
const goodsDiv = html.find('.trade-goods-result');
const listDiv = html.find('.trade-goods-list');
+129 -52
View File
@@ -5,6 +5,9 @@
* Les clics sur la carte affichent les détails du monde dans le chat.
*/
import { searchWorlds } from './travellerMapApi.js';
import { TravelDialog } from './travelDialog.js';
const { ApplicationV2 } = foundry.applications.api;
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
@@ -21,9 +24,12 @@ export class SectorMapApp extends ApplicationV2 {
this._sector = sector;
this._subsector = subsector;
this._handler = null;
this._mapHex = null;
this._searchTimeout = null;
}
get title() {
if (!this._sector) return 'Carte stellaire — Traveller Map';
return this._subsector
? `Sous-secteur ${this._subsector}${this._sector}`
: `Secteur ${this._sector}`;
@@ -31,10 +37,15 @@ export class SectorMapApp extends ApplicationV2 {
get _mapUrl() {
const base = 'https://travellermap.com';
if (!this._sector) return `${base}/?style=mongoose&hideui=1`;
if (this._subsector) {
return `${base}/?sector=${encodeURIComponent(this._sector)}&subsector=${encodeURIComponent(this._subsector)}&style=mongoose&hideui=1`;
}
return `${base}/go/${encodeURIComponent(this._sector)}?style=mongoose`;
let url = `${base}/go/${encodeURIComponent(this._sector)}?style=mongoose`;
if (this._mapHex) {
url += `&hex=${this._mapHex}&scale=512`;
}
return url;
}
/* ───── Rendu ───── */
@@ -49,10 +60,15 @@ export class SectorMapApp extends ApplicationV2 {
_renderHTML() {
return `<div class="mgt2-sector-map-outer">
<div class="mgt2-sector-map-toolbar">
<span class="mgt2-sector-map-label">${this._sector}${this._subsector ? ` — sous-secteur ${this._subsector}` : ''}</span>
<div class="mgt2-sector-map-search">
<input type="text" class="mgt2-sector-map-input" placeholder="Rechercher un monde…" autocomplete="off">
<ul class="mgt2-sector-map-results"></ul>
</div>
<span class="mgt2-sector-map-label">${this._sector ? `${this._sector}${this._subsector ? ` — ss.${this._subsector}` : ''}` : 'Toute la carte'}</span>
<span class="mgt2-sector-map-hint">Cliquez sur un hex pour voir les détails du monde</span>
<button type="button" class="mgt2-sector-map-share" title="Partager une image fixe dans le chat"><i class="fas fa-image"></i> Partager</button>
<button type="button" class="mgt2-sector-map-sync" title="Ouvrir la carte interactive chez tous les joueurs"><i class="fas fa-users"></i> Synchroniser</button>
<button type="button" class="mgt2-sector-map-travel" title="Planifier un voyage"><i class="fas fa-route"></i> Voyage</button>
</div>
<iframe
src="${this._mapUrl}"
@@ -71,6 +87,24 @@ export class SectorMapApp extends ApplicationV2 {
this.element?.querySelector('.mgt2-sector-map-sync')?.addEventListener('click', () => {
this._syncAll();
});
this.element?.querySelector('.mgt2-sector-map-travel')?.addEventListener('click', () => {
this._openTravelDialog();
});
const input = this.element?.querySelector('.mgt2-sector-map-input');
const results = this.element?.querySelector('.mgt2-sector-map-results');
if (input && results) {
input.addEventListener('input', () => {
if (this._searchTimeout) clearTimeout(this._searchTimeout);
this._searchTimeout = setTimeout(() => this._doSearch(input, results), 300);
});
input.addEventListener('blur', () => {
setTimeout(() => { results.innerHTML = ''; }, 200);
});
input.addEventListener('focus', () => {
if (input.value.trim().length >= 2) this._doSearch(input, results);
});
}
}
/* ───── Écoute des clics IFRAME ───── */
@@ -131,7 +165,7 @@ export class SectorMapApp extends ApplicationV2 {
/* ───── Carte de chat ───── */
static _STARPORT = { A:'Excellent', B:'Bon', C:'Routinier', D:'Médiocre', E:'Frontière', X:'Aucun' };
static _SIZE = ['Aucun (Astéroïde)','1 600 km','3 200 km','4 800 km','6 400 km','8 000 km','9 600 km','11 200 km','12 800 km','14 400 km','16 000 km']; // A=10, F=15=Gaz géant (géré à part)
static _SIZE = ['Aucun (Astéroïde)','1 600 km','3 200 km','4 800 km','6 400 km','8 000 km','9 600 km','11 200 km','12 800 km','14 400 km','16 000 km'];
static _ATMO = [
'Aucune (vide)','Trace','Très ténue (polluée)','Très ténue','Ténue (polluée)','Ténue','Standard','Standard (polluée)','Dense','Dense (polluée)',
'Exotique','Corrosive','Insidieuse','','',''];
@@ -164,7 +198,7 @@ export class SectorMapApp extends ApplicationV2 {
return `<span class="uwp-dig" title="${desc}">${val}</span>`;
}
_uwpBreakdown(uwp) {
static _uwpBreakdown(uwp) {
if (!uwp || uwp.length < 2) return '';
const d = SectorMapApp._hexVal;
const s = d(uwp[0]), sz = d(uwp[1]), a = d(uwp[2]), h = d(uwp[3]);
@@ -237,22 +271,12 @@ export class SectorMapApp extends ApplicationV2 {
static _STAR_TYPES = { O:'Bleu (hypergéante)', B:'Bleu-blanc', A:'Blanc', F:'Blanc-jaune', G:'Jaune (naine)', K:'Orange (naine)', M:'Rouge (naine)', L:'Brune', T:'Brune', Y:'Brune' };
static _STAR_CLASS = { 'I':'Supergéante', 'II':'Géante brillante', 'III':'Géante', 'IV':'Sous-géante', 'V':'Naine (séquence principale)', 'VI':'Sous-naine', 'VII':'Naine blanche' };
static _BASES_HELP = {
N:'Base navale', S:'Base scout', W:'Relais', D:'Dépôt naval',
T:'Base TAS', C:'Consulat', P:'Base pirate', R:'Base de réparation',
K:'Base navale (K.)', X:'Relais Xboat',
};
_tooltipHelp(map, codes) {
if (!codes) return '';
return codes.split(/[\s,;]+/).map(c => {
const desc = map[c.toUpperCase()];
return `${c}${desc ? ' — ' + desc : ''}`;
}).join(' | ');
}
static _NOBILITY = {
B:'Chevalier (Baronet)', C:'Baron', D:'Marquis', E:'Comte', F:'Duc', G:'Archiduc', H:'Empereur',
};
@@ -263,9 +287,7 @@ export class SectorMapApp extends ApplicationV2 {
'5':'Majeure', '6':'Capitale',
};
/* ───── Lignes dépliables (details/summary) ───── */
_foldRow(label, value, detail, titleAttr) {
static _foldRow(label, value, detail, titleAttr) {
const valAttr = titleAttr ? ` title="${titleAttr}"` : '';
return `<tr><td colspan="2">
<details>
@@ -275,21 +297,21 @@ export class SectorMapApp extends ApplicationV2 {
</td></tr>`;
}
_decodeImportance(ix) {
static _decodeImportance(ix) {
if (!ix) return '';
const m = String(ix).match(/\{?\s*(-?\d+)\s*\}?/);
if (!m) return this._foldRow('Importance', ix, '');
if (!m) return SectorMapApp._foldRow('Importance', ix, '');
const val = m[1];
const desc = SectorMapApp._IMPORTANCE[val] || '—';
const detail = `<div class="fold-desc">Valeur dimportance économique et stratégique du monde.<br>${val} = ${desc}</div>`;
return this._foldRow('Importance', `{ ${val} } ${desc}`, detail);
return SectorMapApp._foldRow('Importance', `{ ${val} } ${desc}`, detail);
}
_decodeEconomics(ex) {
static _decodeEconomics(ex) {
if (!ex) return '';
let s = String(ex).replace(/[()\s]/g, '');
const m = s.match(/^([\dA-F])([\dA-F])([\dA-F])([+-]\d+)$/i);
if (!m) return this._foldRow('Économie', ex, '');
if (!m) return SectorMapApp._foldRow('Économie', ex, '');
const res = m[1], lab = m[2], inf = m[3], eff = m[4];
const detail = `<table class="fold-subtable">
<tr><td>Ressources</td><td>${res}</td></tr>
@@ -297,13 +319,13 @@ export class SectorMapApp extends ApplicationV2 {
<tr><td>Infrastructure</td><td>${inf}</td></tr>
<tr><td>Efficacité</td><td>${eff}</td></tr>
</table>`;
return this._foldRow('Économie', `( ${res} ${lab} ${inf} ${eff} )`, detail);
return SectorMapApp._foldRow('Économie', `( ${res} ${lab} ${inf} ${eff} )`, detail);
}
_decodeCulture(cx) {
static _decodeCulture(cx) {
if (!cx) return '';
const s = String(cx).replace(/[\[\]\s]/g, '');
if (s.length < 4) return this._foldRow('Culture', cx, '');
if (s.length < 4) return SectorMapApp._foldRow('Culture', cx, '');
const h = s[0].toUpperCase(), t = s[1].toUpperCase(), p = s[2].toUpperCase(), a = s[3].toUpperCase();
const detail = `<table class="fold-subtable">
<tr><td>Hétérogénéité</td><td>${h}</td></tr>
@@ -311,10 +333,10 @@ export class SectorMapApp extends ApplicationV2 {
<tr><td>Progressisme</td><td>${p}</td></tr>
<tr><td>Agressivité</td><td>${a}</td></tr>
</table>`;
return this._foldRow('Culture', `[ ${h} ${t} ${p} ${a} ]`, detail);
return SectorMapApp._foldRow('Culture', `[ ${h} ${t} ${p} ${a} ]`, detail);
}
_decodePopulation(uwp, pbg) {
static _decodePopulation(uwp, pbg) {
if (!uwp || uwp.length < 5) return '';
const popUwp = SectorMapApp._hexVal(uwp[4]);
if (popUwp < 0) return '';
@@ -332,22 +354,22 @@ export class SectorMapApp extends ApplicationV2 {
const gas = pbg ? parseInt(pbg[2], 10) : null;
const detail = `<div class="fold-desc">Population = multiplicateur (PBG: <b>${popPbg}</b>) × 10<sup>chiffre UWP (${uwp[4]})</sup><br>
Ceintures dastéroïdes : <b>${belts ?? '?'}</b> &nbsp;|&nbsp; Géantes gazeuses : <b>${gas ?? '?'}</b></div>`;
return this._foldRow('Population', `${fmtMult} × ${fmtBase} = ${fmtTotal}`, detail);
return SectorMapApp._foldRow('Population', `${fmtMult} × ${fmtBase} = ${fmtTotal}`, detail);
}
_decodeNobility(nob) {
static _decodeNobility(nob) {
if (!nob) return '';
const titles = [];
for (const ch of nob) {
const desc = SectorMapApp._NOBILITY[ch.toUpperCase()];
if (desc) titles.push(`${ch} (${desc})`);
}
if (!titles.length) return this._foldRow('Noblesse', nob, '');
if (!titles.length) return SectorMapApp._foldRow('Noblesse', nob, '');
const detail = `<div class="fold-desc">Titres de noblesse impériale présents sur ce monde.</div>`;
return this._foldRow('Noblesse', titles.join(', '), detail);
return SectorMapApp._foldRow('Noblesse', titles.join(', '), detail);
}
_postWorldCard(w) {
static _buildWorldCardHTML(w) {
const sector = w.Sector || '';
const hex = w.Hex || '';
const name = w.Name || '—';
@@ -363,26 +385,22 @@ export class SectorMapApp extends ApplicationV2 {
: zone === 'A' ? 'Ambre'
: 'Verte';
// Construction des lignes pliables
const lines = [];
// UWP
const uwpDetail = this._uwpBreakdown(uwp);
lines.push(this._foldRow('UWP', `<span class="mono">${uwp}</span>`, uwpDetail));
const uwpDetail = SectorMapApp._uwpBreakdown(uwp);
lines.push(SectorMapApp._foldRow('UWP', `<span class="mono">${uwp}</span>`, uwpDetail));
// Champs étendus
const ixRow = this._decodeImportance(w.Ix);
const ixRow = SectorMapApp._decodeImportance(w.Ix);
if (ixRow) lines.push(ixRow);
const exRow = this._decodeEconomics(w.Ex);
const exRow = SectorMapApp._decodeEconomics(w.Ex);
if (exRow) lines.push(exRow);
const cxRow = this._decodeCulture(w.Cx);
const cxRow = SectorMapApp._decodeCulture(w.Cx);
if (cxRow) lines.push(cxRow);
const popRow = this._decodePopulation(uwp, pbg);
const popRow = SectorMapApp._decodePopulation(uwp, pbg);
if (popRow) lines.push(popRow);
const nobRow = this._decodeNobility(w.Nobility);
const nobRow = SectorMapApp._decodeNobility(w.Nobility);
if (nobRow) lines.push(nobRow);
// Bases
if (bases) {
const bCodes = bases.split(/[\s,;]+/);
const bList = bCodes.map(c => {
@@ -390,10 +408,9 @@ export class SectorMapApp extends ApplicationV2 {
return `<tr><td class="mono">${c}</td><td>${desc || '—'}</td></tr>`;
}).join('');
const bDetail = `<table class="fold-subtable"><tbody>${bList}</tbody></table>`;
lines.push(this._foldRow('Bases', bases, bDetail));
lines.push(SectorMapApp._foldRow('Bases', bases, bDetail));
}
// Remarques
if (remarks) {
const rCodes = remarks.split(/[\s,;]+/);
const rList = rCodes.map(c => {
@@ -401,17 +418,15 @@ export class SectorMapApp extends ApplicationV2 {
return `<tr><td class="mono">${c}</td><td>${desc || '—'}</td></tr>`;
}).join('');
const rDetail = `<table class="fold-subtable"><tbody>${rList}</tbody></table>`;
lines.push(this._foldRow('Remarques', remarks, rDetail));
lines.push(SectorMapApp._foldRow('Remarques', remarks, rDetail));
}
// Allégeance
if (allegiance) {
const allegFull = w.AllegianceName || '';
const aDetail = `<div class="fold-desc">${allegFull || allegiance}</div>`;
lines.push(this._foldRow('Allégeance', allegFull ? `${allegiance} (${allegFull})` : allegiance, aDetail));
lines.push(SectorMapApp._foldRow('Allégeance', allegFull ? `${allegiance} (${allegFull})` : allegiance, aDetail));
}
// Étoile (les notations comme "G3 V" contiennent un espace entre sous-classe et luminosité)
if (stellar) {
const sList = [];
let remaining = stellar.trim();
@@ -424,34 +439,89 @@ export class SectorMapApp extends ApplicationV2 {
sList.push(`<tr><td class="mono">${m[1]}${m[2]} ${m[3]}</td><td>${type} · ${cls}</td></tr>`);
remaining = remaining.slice(m[0].length).trimStart();
} else {
// Saute les tokens non reconnus (BD, compagnon, etc.)
const next = remaining.indexOf(' ');
if (next < 0) break;
remaining = remaining.slice(next + 1).trimStart();
}
}
const sDetail = sList.length ? `<table class="fold-subtable"><tbody>${sList.join('')}</tbody></table>` : `<div class="fold-desc">${stellar}</div>`;
lines.push(this._foldRow('Étoile', `<span class="mono">${stellar}</span>`, sDetail));
lines.push(SectorMapApp._foldRow('Étoile', `<span class="mono">${stellar}</span>`, sDetail));
}
const html = `<section class="mgt2-world-card">
return `<section class="mgt2-world-card">
<div class="mgt2-world-card-header">
<span class="mgt2-world-name">${name}</span>
<span class="mgt2-world-hex">${sector} ${hex}</span>
<span class="mgt2-world-zone zone-${zone.toLowerCase() || 'g'}">${zoneLabel}</span>
</div>
<table class="mgt2-world-card-body"><tbody>${lines.join('')}</tbody></table>
<div class="mgt2-world-card-actions">
<a class="mgt2-world-commerce" data-sector="${this._escapeAttr(sector)}" data-hex="${hex}" data-uwp="${uwp}" data-zone="${zone || 'normal'}" data-name="${this._escapeAttr(name)}">
<i class="fas fa-balance-scale"></i> Commerce
</a>
</div>
</section>`;
}
static _escapeAttr(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
_postWorldCard(w) {
const html = SectorMapApp._buildWorldCardHTML(w);
ChatMessage.create({
content: html,
whisper: [game.user.id],
});
}
/* ───── Recherche de monde ───── */
async _doSearch(input, results) {
const query = input.value.trim();
if (query.length < 2) { results.innerHTML = ''; return; }
const worlds = await searchWorlds(query);
if (!worlds.length) {
results.innerHTML = '<li class="no-result">Aucun monde trouvé</li>';
return;
}
results.innerHTML = worlds.slice(0, 12).map(w =>
`<li data-sector="${w.sector}" data-hex="${w.hex}" data-name="${w.name}">
<span class="world-name">${w.name}</span>
<span class="world-uwp">${w.uwp}</span>
<span class="world-sector">${w.sector}</span>
</li>`
).join('');
results.querySelectorAll('li[data-sector]').forEach(li => {
li.addEventListener('click', () => this._selectWorld(li, input, results));
});
}
async _selectWorld(li, input, results) {
const sector = li.dataset.sector;
const hex = li.dataset.hex;
const name = li.dataset.name;
results.innerHTML = '';
input.value = name;
this._sector = sector;
this._subsector = null;
this._mapHex = hex;
const iframe = this.element?.querySelector('.mgt2-sector-map-frame');
if (iframe) iframe.src = this._mapUrl;
ui.notifications.info(`Carte centrée sur ${name} (${sector} ${hex})`);
}
/* ───── Partage ───── */
_shareMap() {
if (!this._sector) {
ui.notifications.warn('Aucun secteur sélectionné — utilisez la recherche pour centrer sur un secteur');
return;
}
const posterUrl = `https://travellermap.com/api/poster?sector=${encodeURIComponent(this._sector)}${this._subsector ? `&subsector=${encodeURIComponent(this._subsector)}` : ''}&style=mongoose&scale=128&dpr=2`;
const label = this._subsector
? `Sous-secteur ${this._subsector}${this._sector}`
@@ -484,6 +554,13 @@ export class SectorMapApp extends ApplicationV2 {
ui.notifications.info(`Carte synchronisée chez tous les joueurs`);
}
_openTravelDialog() {
const existing = Object.values(ui.windows).find(w => w.id === 'mgt2-travel-dialog');
if (existing) { existing.bringToTop(); return; }
const dialog = new TravelDialog();
dialog.render({ force: true });
}
/* ───── Nettoyage ───── */
close() {
+72 -10
View File
@@ -6,6 +6,8 @@
*/
import { SectorMapApp } from './SectorMapApp.js';
import { postWorldCardToChat } from './worldCard.js';
import { CommerceDialog } from './CommerceDialog.js';
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
const ChatLogV2 = foundry.applications?.sidebar?.tabs?.ChatLog;
@@ -23,15 +25,11 @@ async function handleSectorCommand(sector, subsector) {
if (_pendingHandle) return;
_pendingHandle = true;
try {
if (!sector?.trim()) {
ui.notifications.warn('Usage : /sector <nom du secteur> (ex: /sector "Spinward Marches")');
return;
}
if (!game.user?.isGM) {
ui.notifications.error('Seul le MJ peut utiliser cette commande');
return;
}
await openMap(sector.trim());
await openMap(sector?.trim());
} catch (err) {
console.error(`${MODULE_ID} | Erreur /sector :`, err);
ui.notifications.error(`Erreur : ${err.message}`);
@@ -40,6 +38,14 @@ async function handleSectorCommand(sector, subsector) {
}
}
async function handleSystemCommand(sector, hex) {
if (!sector || !hex) {
ui.notifications.warn('Usage : /system <secteur> <hex> (ex: /system "Spinward Marches" 1910)');
return;
}
await postWorldCardToChat(sector, hex);
}
async function handleSubsectorCommand(raw) {
if (_pendingHandle) return;
_pendingHandle = true;
@@ -75,11 +81,8 @@ if (ChatLogV2?.CHAT_COMMANDS) {
rgx: /^\/sector(?:\s+(.*))?$/i,
fn: function() {
const raw = arguments[1]?.[1]?.trim?.();
if (raw) {
handleSectorCommand(raw);
return false;
}
return true;
handleSectorCommand(raw || undefined);
return false;
},
};
console.log(`${MODULE_ID} | Commande /sector enregistrée`);
@@ -96,6 +99,20 @@ if (ChatLogV2?.CHAT_COMMANDS) {
},
};
console.log(`${MODULE_ID} | Commande /subsector enregistrée`);
ChatLogV2.CHAT_COMMANDS['system'] = {
rgx: /^\/system\s+(.+?)\s+(\d{4})\s*$/i,
fn: function() {
const sector = arguments[1]?.[1]?.trim?.();
const hex = arguments[1]?.[2]?.trim?.();
if (sector && hex) {
handleSystemCommand(sector, hex);
return false;
}
return true;
},
};
console.log(`${MODULE_ID} | Commande /system enregistrée`);
}
/* ───── Hooks de secours (v13 / fallback v14) ───── */
@@ -114,6 +131,12 @@ Hooks.on('preCreateChatMessage', (message, data, options) => {
handleSubsectorCommand(m[1]?.trim());
return false;
}
m = c?.match(/^\/system\s+(.+?)\s+(\d{4})\s*$/i);
if (m) {
handleSystemCommand(m[1]?.trim(), m[2]?.trim());
return false;
}
});
/* ───── Socket (synchronisation MJ → joueurs) ───── */
@@ -124,6 +147,39 @@ Hooks.once('ready', () => {
if (game.user?.isGM) return;
openMap(data.sector, data.subsector);
});
// Clics sur les liens de monde dans les journals de trajet
document.addEventListener('click', (event) => {
const link = event.target.closest('.mgt2-world-link');
if (!link) return;
event.preventDefault();
const sector = link.dataset.sector;
const hex = link.dataset.hex;
if (sector && hex) {
handleSystemCommand(sector, hex);
}
});
// Clics sur le bouton Commerce dans les cartes de monde
document.addEventListener('click', (event) => {
const btn = event.target.closest('.mgt2-world-commerce');
if (!btn) return;
event.preventDefault();
const uwp = btn.dataset.uwp;
const zone = btn.dataset.zone;
const name = btn.dataset.name;
const sector = btn.dataset.sector;
const hex = btn.dataset.hex;
if (uwp) {
const existing = Object.values(ui.windows).find(w => w.id === 'mgt2-commerce');
if (existing) { existing.bringToTop(); return; }
const dialog = new CommerceDialog({
defaultWorld: { uwp, zone, name, sector, hex },
initialTab: 'trade',
});
dialog.render({ force: true });
}
});
});
Hooks.on('chatMessage', (...args) => {
@@ -143,4 +199,10 @@ Hooks.on('chatMessage', (...args) => {
handleSubsectorCommand(m[1]?.trim());
return false;
}
m = msg?.trim()?.match(/^\/system\s+(.+?)\s+(\d{4})\s*$/i);
if (m) {
handleSystemCommand(m[1]?.trim(), m[2]?.trim());
return false;
}
});
+274
View File
@@ -0,0 +1,274 @@
import { searchWorlds, calcParsecs } from './travellerMapApi.js';
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
function cleanSectorName(sector) {
return sector?.replace(/\s*\([^)]*\)\s*$/, '').trim() || sector;
}
function worldCoord(sx, sy, hx, hy) {
return { x: (sx - 0) * 32 + (hx - 1), y: (sy - 0) * 40 + (hy - 40) };
}
export class TravelDialog extends HandlebarsApplicationMixin(ApplicationV2) {
static DEFAULT_OPTIONS = {
id: 'mgt2-travel-dialog',
classes: ['mgt2-travel-dialog'],
position: { width: 600, height: 500 },
window: { icon: 'fas fa-route', title: 'Planificateur de voyage', resizable: true, controls: [] },
};
static PARTS = {
main: {
template: `modules/${MODULE_ID}/templates/travel-dialog.hbs`,
},
};
constructor() {
super();
this._fromWorld = null;
this._toWorld = null;
this._lastWorlds = null;
this._lastSegments = null;
}
_onRender(context, options) {
this._wireSearch('from');
this._wireSearch('to');
this.element?.querySelector('[data-action="calculate"]')?.addEventListener('click', () => {
this._calculateRoute();
});
this.element?.querySelector('[data-action="create-journal"]')?.addEventListener('click', () => {
this._createJournal();
});
}
_wireSearch(prefix) {
const input = this.element.querySelector(`[name="travel-${prefix}"]`);
const results = this.element.querySelector(`.travel-${prefix}-results`);
if (!input || !results) return;
let timeout = null;
input.addEventListener('input', () => {
clearTimeout(timeout);
const q = input.value.trim();
if (q.length < 2) { results.innerHTML = ''; return; }
timeout = setTimeout(async () => {
const worlds = await searchWorlds(q);
if (!worlds?.length) {
results.innerHTML = '<li class="no-result">Aucun résultat</li>';
return;
}
results.innerHTML = worlds.slice(0, 10).map(w =>
`<li data-sector="${w.sector}" data-hex="${w.hex}" data-name="${w.name}">
<span class="world-name">${w.name}</span>
<span class="world-sector">${w.sector}</span>
<span class="world-hex">${w.hex}</span>
</li>`
).join('');
}, 300);
});
results.addEventListener('click', (e) => {
const li = e.target.closest('[data-sector]');
if (!li) return;
const data = { sector: li.dataset.sector, hex: li.dataset.hex, name: li.dataset.name };
if (prefix === 'from') this._fromWorld = data;
else this._toWorld = data;
input.value = `${data.name} (${data.sector} ${data.hex})`;
results.innerHTML = '';
});
input.addEventListener('blur', () => {
setTimeout(() => { results.innerHTML = ''; }, 200);
});
}
async _calculateRoute() {
if (!this._fromWorld || !this._toWorld) {
ui.notifications.warn('Veuillez sélectionner un monde de départ et un monde d\'arrivée');
return;
}
const jumpEl = this.element.querySelector('[name="travel-jump"]');
const jump = parseInt(jumpEl?.value, 10) || 2;
const resultsEl = this.element.querySelector('.travel-results');
if (!resultsEl) return;
resultsEl.innerHTML = '<div class="travel-loading"><i class="fas fa-spinner fa-spin"></i> Calcul de l\'itinéraire…</div>';
const startSector = cleanSectorName(this._fromWorld.sector);
const endSector = cleanSectorName(this._toWorld.sector);
const startLoc = `${startSector} ${this._fromWorld.hex}`;
const endLoc = `${endSector} ${this._toWorld.hex}`;
try {
const resp = await fetch(
`https://travellermap.com/api/route?start=${encodeURIComponent(startLoc)}&end=${encodeURIComponent(endLoc)}&jump=${jump}`
);
if (!resp.ok) {
if (resp.status === 404) {
const text = await resp.text().catch(() => 'Aucun itinéraire trouvé');
resultsEl.innerHTML = `<div class="travel-error">${this._escapeHtml(text)}</div>`;
} else {
resultsEl.innerHTML = `<div class="travel-error">Erreur API (${resp.status})</div>`;
}
return;
}
const data = await resp.json();
if (!Array.isArray(data) || data.length < 2) {
resultsEl.innerHTML = '<div class="travel-error">Aucun itinéraire trouvé</div>';
return;
}
this._displayRoute(data, resultsEl);
} catch (err) {
console.error('TravelDialog | Erreur:', err);
resultsEl.innerHTML = `<div class="travel-error">Erreur : ${this._escapeHtml(err.message)}</div>`;
}
}
_displayRoute(worlds, resultsEl) {
this._lastWorlds = worlds;
const segments = [];
let totalParsecs = 0;
for (let i = 0; i < worlds.length - 1; i++) {
const a = worlds[i];
const b = worlds[i + 1];
const fromC = worldCoord(a.SectorX, a.SectorY, a.HexX, a.HexY);
const toC = worldCoord(b.SectorX, b.SectorY, b.HexX, b.HexY);
const dist = calcParsecs(fromC, toC);
totalParsecs += dist;
segments.push({ from: a, to: b, dist });
}
this._lastSegments = segments;
let html = `<div class="travel-route-summary">
<i class="fas fa-route"></i>
<strong>${segments.length}</strong> saut${segments.length > 1 ? 's' : ''}
· <strong>${totalParsecs}</strong> parsecs
</div>`;
html += `<div class="travel-route-duration">
<i class="fas fa-clock"></i> Durée estimée : <strong>${segments.length}</strong> semaine${segments.length > 1 ? 's' : ''}
</div>`;
html += '<ol class="travel-jump-list">';
segments.forEach((seg) => {
const f = seg.from;
const t = seg.to;
html += `<li>
<div class="jump-segment">
<div class="jump-world jump-from">
<span class="jump-world-name">${f.Name || '?'}</span>
<span class="jump-world-detail">${f.Sector} ${f.Hex || ''}</span>
</div>
<div class="jump-arrow"><i class="fas fa-long-arrow-alt-right"></i></div>
<div class="jump-world jump-to">
<span class="jump-world-name">${t.Name || '?'}</span>
<span class="jump-world-detail">${t.Sector} ${t.Hex || ''}</span>
</div>
<div class="jump-distance">Saut-${seg.dist}</div>
</div>
</li>`;
});
html += '</ol>';
resultsEl.innerHTML = html;
const journalBtn = this.element?.querySelector('.travel-journal-actions');
if (journalBtn) journalBtn.style.display = 'block';
}
async _createJournal() {
const worlds = this._lastWorlds;
const segments = this._lastSegments;
if (!worlds?.length || !segments?.length) {
ui.notifications.warn('Calculez d\'abord un itinéraire');
return;
}
const from = worlds[0];
const to = worlds[worlds.length - 1];
const totalParsecs = segments.reduce((s, seg) => s + seg.dist, 0);
const totalJumps = segments.length;
const jumpRating = this.element?.querySelector('[name="travel-jump"]')?.value || '?';
const lines = [];
lines.push(`<h2>Journal de voyage</h2>`);
lines.push(`<p><strong>Départ :</strong> ${this._worldLink(from)}</p>`);
lines.push(`<p><strong>Destination :</strong> ${this._worldLink(to)}</p>`);
lines.push(`<p><strong>Moteur :</strong> J-${jumpRating}</p>`);
lines.push(`<hr>`);
lines.push(`<h3>Itinéraire (${totalJumps} saut${totalJumps > 1 ? 's' : ''}, ${totalParsecs} pc)</h3>`);
lines.push(`<table><thead><tr><th>#</th><th>Départ</th><th>→</th><th>Arrivée</th><th>Distance</th></tr></thead><tbody>`);
segments.forEach((seg, i) => {
const f = seg.from;
const t = seg.to;
lines.push(`<tr>
<td>${i + 1}</td>
<td>${this._worldLink(f)}</td>
<td>→</td>
<td>${this._worldLink(t)}</td>
<td>${seg.dist} pc</td>
</tr>`);
});
lines.push(`</tbody></table>`);
lines.push(`<hr>`);
lines.push(`<h3>Mondes visités</h3>`);
lines.push(`<ul>`);
const seen = new Set();
for (const w of worlds) {
const key = `${w.Sector}|${w.Hex}`;
if (seen.has(key)) continue;
seen.add(key);
lines.push(`<li>${this._worldLink(w)}${w.UWP ? ` — UWP: ${w.UWP}` : ''}</li>`);
}
lines.push(`</ul>`);
const content = lines.join('\n');
try {
const journal = await JournalEntry.create({
name: `Voyage : ${from.Name || '?'}${to.Name || '?'}`,
pages: [{
name: 'Itinéraire',
type: 'text',
text: { content, format: 2 },
}],
});
if (journal) {
ui.notifications.info(`Journal créé : ${journal.name}`);
journal.sheet?.render(true);
}
} catch (err) {
console.error('TravelDialog | Erreur création journal:', err);
ui.notifications.error('Erreur lors de la création du journal');
}
}
_worldLink(w) {
const name = w.Name || '?';
const sector = w.Sector || '';
const hex = w.Hex || '';
return `<a class="mgt2-world-link" data-sector="${this._escapeAttr(sector)}" data-hex="${this._escapeAttr(hex)}">${name}</a> <em>(${sector} ${hex})</em>`;
}
_escapeAttr(str) {
if (!str) return '';
return str.replace(/"/g, '&quot;').replace(/&/g, '&amp;');
}
_escapeHtml(str) {
if (!str) return '';
const d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
}
+46
View File
@@ -0,0 +1,46 @@
/**
* Fonctions réutilisables pour afficher une carte de monde dans le chat.
*/
import { SectorMapApp } from './SectorMapApp.js';
const BASE_URL = 'https://travellermap.com';
/**
* Récupère les données d'un monde via l'API et poste une carte détaillée dans le chat.
* @param {string} sector Nom du secteur
* @param {string} hex Code hex sur 4 chiffres
* @param {object} [options] { whisper: bool }
*/
export async function postWorldCardToChat(sector, hex, options = {}) {
if (!sector || !hex) {
ui.notifications.error('Secteur et hex requis');
return;
}
const url = `${BASE_URL}/data/${encodeURIComponent(sector)}/${encodeURIComponent(hex)}`;
let resp;
try {
resp = await fetch(url);
} catch (err) {
console.error('worldCard | fetch error:', err);
ui.notifications.error('Erreur réseau');
return;
}
if (!resp.ok) {
ui.notifications.error(`Monde introuvable : ${sector} ${hex}`);
return;
}
const data = await resp.json();
const world = data.Worlds?.[0];
if (!world) {
ui.notifications.error(`Aucune donnée pour ${sector} ${hex}`);
return;
}
const html = SectorMapApp._buildWorldCardHTML(world);
const msgData = { content: html };
if (options.whisper !== false) msgData.whisper = [game.user.id];
await ChatMessage.create(msgData);
}
+23
View File
@@ -734,3 +734,26 @@ button.btn-calculate:hover,
vertical-align: super;
margin-left: 2px;
}
/* Bandeau monde sélectionné (depuis carte de chat) */
.world-info-banner {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #e8e0d0;
border: 1px solid #c9a227;
border-radius: 4px;
margin-bottom: 10px;
font-size: 0.95em;
}
.world-info-banner i {
color: #c9a227;
}
.world-info-banner .world-info-loc {
color: #888;
font-size: 0.85em;
}
+453 -3
View File
@@ -366,7 +366,92 @@ button.btn-calculate:hover,
font-size: 0.78em;
}
.mgt2-sector-map-share {
.mgt2-sector-map-search {
position: relative;
flex-shrink: 0;
}
.mgt2-sector-map-input {
width: 180px;
padding: 4px 8px;
font-size: 0.8em;
border: 1px solid #555;
border-radius: 3px;
background: #2c2c3e;
color: #d9b24c;
outline: none;
}
.mgt2-sector-map-input::placeholder {
color: #7a755a;
}
.mgt2-sector-map-input:focus {
border-color: #c9a227;
}
.mgt2-sector-map-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
max-height: 280px;
overflow-y: auto;
background: #2c2c3e;
border: 1px solid #c9a227;
border-top: none;
border-radius: 0 0 4px 4px;
z-index: 999;
list-style: none;
margin: 0;
padding: 0;
}
.mgt2-sector-map-results li {
padding: 5px 8px;
cursor: pointer;
font-size: 0.78em;
color: #d8c79a;
border-bottom: 1px solid #3a3a50;
display: flex;
gap: 6px;
align-items: baseline;
}
.mgt2-sector-map-results li:last-child {
border-bottom: none;
}
.mgt2-sector-map-results li:hover {
background: #3a3a50;
}
.mgt2-sector-map-results .no-result {
color: #7a755a;
font-style: italic;
cursor: default;
}
.mgt2-sector-map-results .world-name {
font-weight: bold;
color: #d9b24c;
}
.mgt2-sector-map-results .world-uwp {
font-family: 'Courier New', monospace;
font-size: 0.9em;
color: #a99c7a;
}
.mgt2-sector-map-results .world-sector {
margin-left: auto;
color: #7a755a;
font-size: 0.9em;
}
.mgt2-sector-map-share,
.mgt2-sector-map-sync,
.mgt2-sector-map-travel {
background: #c9a227;
color: #1a1a2e;
border: none;
@@ -378,7 +463,9 @@ button.btn-calculate:hover,
flex-shrink: 0;
}
.mgt2-sector-map-share:hover {
.mgt2-sector-map-share:hover,
.mgt2-sector-map-sync:hover,
.mgt2-sector-map-travel:hover {
background: #d9b24c;
}
@@ -492,7 +579,36 @@ button.btn-calculate:hover,
}
.mgt2-world-card-body summary:hover {
background: rgba(201, 162, 39, 0.08);
background: #eae4d4;
}
.mgt2-world-card-actions {
padding: 6px 12px 8px;
text-align: right;
border-top: 1px solid #ddd0bc;
}
.mgt2-world-commerce {
display: inline-block;
padding: 4px 14px;
background: #c9a227;
color: #fff;
border-radius: 3px;
font-size: 0.85em;
font-weight: 600;
cursor: pointer;
text-decoration: none;
transition: background 0.15s;
}
.mgt2-world-commerce:hover {
background: #b89020;
color: #fff;
text-decoration: none;
}
.mgt2-world-commerce i {
margin-right: 4px;
}
.fold-label {
@@ -604,3 +720,337 @@ button.btn-calculate:hover,
max-width: 100%;
height: auto;
}
/* ══════════════════════════════════════════════════════
Travel Dialog (planificateur de voyage)
══════════════════════════════════════════════════════ */
#mgt2-travel-dialog .window-content {
background: #f5f0e8;
font-family: 'Signika', sans-serif;
color: #222;
padding: 12px;
}
.travel-form {
display: flex;
flex-direction: column;
gap: 12px;
height: 100%;
}
.travel-worlds {
display: flex;
align-items: flex-end;
gap: 10px;
}
.travel-world-block {
flex: 1;
min-width: 0;
}
.travel-world-block label {
display: block;
font-weight: 600;
margin-bottom: 4px;
font-size: 0.9em;
color: #444;
}
.travel-world-block label i {
margin-right: 4px;
color: #c9a227;
}
.travel-search-widget input {
width: 100%;
padding: 6px 8px;
border: 1px solid #b5a68b;
border-radius: 3px;
background: #fff;
color: #222;
font-size: 0.9em;
box-sizing: border-box;
}
.travel-search-widget input:focus {
outline: none;
border-color: #c9a227;
box-shadow: 0 0 4px rgba(201, 162, 39, 0.4);
}
.travel-search-widget {
position: relative;
}
.travel-from-results,
.travel-to-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 100;
background: #fff;
border: 1px solid #b5a68b;
border-top: none;
max-height: 200px;
overflow-y: auto;
list-style: none;
margin: 0;
padding: 0;
border-radius: 0 0 3px 3px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.travel-from-results li,
.travel-to-results li {
padding: 6px 8px;
cursor: pointer;
display: flex;
gap: 8px;
align-items: baseline;
font-size: 0.85em;
color: #222;
border-bottom: 1px solid #eee;
}
.travel-from-results li:last-child,
.travel-to-results li:last-child {
border-bottom: none;
}
.travel-from-results li:hover,
.travel-to-results li:hover {
background: #e8e0d0;
}
.travel-from-results .no-result,
.travel-to-results .no-result {
color: #888;
cursor: default;
font-style: italic;
}
.travel-from-results .world-name,
.travel-to-results .world-name {
font-weight: 600;
white-space: nowrap;
}
.travel-from-results .world-sector,
.travel-to-results .world-sector {
color: #666;
font-size: 0.85em;
white-space: nowrap;
}
.travel-from-results .world-hex,
.travel-to-results .world-hex {
color: #888;
font-size: 0.8em;
font-family: monospace;
margin-left: auto;
}
.travel-jump-selector {
flex: 0 0 80px;
text-align: center;
}
.travel-jump-selector label {
display: block;
font-weight: 600;
margin-bottom: 4px;
font-size: 0.9em;
color: #444;
}
.travel-jump-selector select {
width: 100%;
padding: 6px;
border: 1px solid #b5a68b;
border-radius: 3px;
background: #fff;
color: #222;
font-size: 0.9em;
text-align: center;
}
.travel-actions {
text-align: center;
}
.travel-actions button {
padding: 8px 24px;
background: #c9a227;
color: #fff;
border: none;
border-radius: 4px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
.travel-actions button:hover {
background: #b89020;
}
.travel-actions button i {
margin-right: 6px;
}
.travel-results {
flex: 1;
overflow-y: auto;
min-height: 60px;
border-top: 1px solid #d4c9b8;
padding-top: 12px;
}
.travel-loading {
text-align: center;
color: #888;
padding: 20px;
font-style: italic;
}
.travel-loading i {
margin-right: 8px;
}
.travel-error {
padding: 12px 16px;
background: #fce4e4;
border: 1px solid #e8b4b4;
border-radius: 4px;
color: #a33;
font-size: 0.9em;
}
.travel-route-summary {
padding: 10px 14px;
background: #e4eed4;
border: 1px solid #b8d498;
border-radius: 4px;
font-size: 1em;
margin-bottom: 8px;
}
.travel-route-summary i {
margin-right: 6px;
color: #5a8a2a;
}
.travel-route-duration {
padding: 6px 14px;
font-size: 0.9em;
color: #666;
margin-bottom: 8px;
}
.travel-route-duration i {
margin-right: 6px;
}
.travel-jump-list {
list-style: none;
margin: 0;
padding: 0;
}
.travel-jump-list li {
padding: 6px 0;
border-bottom: 1px solid #e8e0d0;
}
.travel-jump-list li:last-child {
border-bottom: none;
}
.jump-segment {
display: flex;
align-items: center;
gap: 8px;
}
.jump-world {
flex: 1;
min-width: 0;
}
.jump-world-name {
display: block;
font-weight: 600;
font-size: 0.95em;
}
.jump-world-detail {
display: block;
font-size: 0.8em;
color: #888;
}
.jump-to {
text-align: right;
}
.jump-arrow {
flex: 0 0 24px;
text-align: center;
color: #c9a227;
font-size: 1.1em;
}
.jump-distance {
flex: 0 0 60px;
text-align: center;
font-weight: 700;
font-size: 0.9em;
padding: 2px 8px;
background: #eee8d8;
border-radius: 3px;
color: #555;
}
/* Journal de trajet */
.travel-journal-actions {
text-align: center;
padding-top: 8px;
border-top: 1px solid #d4c9b8;
margin-top: 8px;
}
.travel-journal-actions button {
padding: 8px 24px;
background: #5a7a2a;
color: #fff;
border: none;
border-radius: 4px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
.travel-journal-actions button:hover {
background: #4a6822;
}
.travel-journal-actions button i {
margin-right: 6px;
}
a.mgt2-world-link {
color: #6a3a8a;
font-weight: 600;
cursor: pointer;
text-decoration: underline;
text-decoration-style: dotted;
}
a.mgt2-world-link:hover {
color: #8a4aaa;
}
+8
View File
@@ -273,6 +273,14 @@
<div class="tab {{#if (eq activeTab "trade")}}active{{/if}}" data-tab="trade">
<h3><i class="fas fa-balance-scale"></i> Commerce spéculatif</h3>
{{#if defaultWorldName}}
<div class="world-info-banner">
<i class="fas fa-globe"></i>
<strong>{{defaultWorldName}}</strong>
{{#if defaultWorldLoc}}<span class="world-info-loc">{{defaultWorldLoc}}</span>{{/if}}
</div>
{{/if}}
<div class="world-block world-block-full">
<div class="world-block-title"><i class="fas fa-store"></i> Monde fournisseur</div>
<div class="world-search-widget" data-uwp-target="trade.uwp" data-zone-target="trade.zone">
+7 -5
View File
@@ -24,9 +24,9 @@
<div class="commerce-section">
<p class="route">
<span class="route-uwp">{{dep.uwp}}</span>
<span class="route-world">{{#if dep.name}}{{dep.name}}{{/if}}<span class="route-uwp">{{dep.uwp}}</span></span>
<i class="fas fa-arrow-right route-arrow"></i>
<span class="route-uwp">{{dest.uwp}}</span>
<span class="route-world">{{#if dest.name}}{{dest.name}}{{/if}}<span class="route-uwp">{{dest.uwp}}</span></span>
<span class="route-parsecs">{{parsecs}} parsec{{#if (gt parsecs 1)}}s{{/if}}</span>
</p>
</div>
@@ -67,9 +67,9 @@
<div class="commerce-section">
<p class="route">
<span class="route-uwp">{{dep.uwp}}</span>
<span class="route-world">{{#if dep.name}}{{dep.name}}{{/if}}<span class="route-uwp">{{dep.uwp}}</span></span>
<i class="fas fa-arrow-right route-arrow"></i>
<span class="route-uwp">{{dest.uwp}}</span>
<span class="route-world">{{#if dest.name}}{{dest.name}}{{/if}}<span class="route-uwp">{{dest.uwp}}</span></span>
<span class="route-parsecs">{{parsecs}} parsec{{#if (gt parsecs 1)}}s{{/if}}</span>
</p>
</div>
@@ -131,7 +131,9 @@
<div class="commerce-section">
<p>
<strong>Monde :</strong> <span class="route-uwp">{{world.uwp}}</span>
<strong>Monde :</strong>
{{#if world.name}}<span class="route-world">{{world.name}}</span> — {{/if}}
<span class="route-uwp">{{world.uwp}}</span>
&nbsp;|&nbsp;
<strong>Codes :</strong>
{{#if world.tradeCodes.length}}
+45
View File
@@ -0,0 +1,45 @@
<form class="travel-form">
<div class="travel-worlds">
<div class="travel-world-block">
<label><i class="fas fa-rocket"></i> Monde de départ</label>
<div class="travel-search-widget">
<input type="text" name="travel-from" placeholder="Rechercher un monde…" autocomplete="off">
<ul class="travel-from-results"></ul>
</div>
</div>
<div class="travel-jump-selector">
<label for="travel-jump">Saut</label>
<select name="travel-jump">
<option value="1">J-1</option>
<option value="2" selected>J-2</option>
<option value="3">J-3</option>
<option value="4">J-4</option>
<option value="5">J-5</option>
<option value="6">J-6</option>
</select>
</div>
<div class="travel-world-block">
<label><i class="fas fa-flag-checkered"></i> Monde d'arrivée</label>
<div class="travel-search-widget">
<input type="text" name="travel-to" placeholder="Rechercher un monde…" autocomplete="off">
<ul class="travel-to-results"></ul>
</div>
</div>
</div>
<div class="travel-actions">
<button type="button" data-action="calculate">
<i class="fas fa-route"></i> Calculer l'itinéraire
</button>
</div>
<div class="travel-results"></div>
<div class="travel-journal-actions" style="display:none;">
<button type="button" data-action="create-journal">
<i class="fas fa-book"></i> Créer un journal de trajet
</button>
</div>
</form>