diff --git a/module.json b/module.json index 1109d12..1a894b5 100644 --- a/module.json +++ b/module.json @@ -21,7 +21,8 @@ "scripts/TravellerNpcDialog.js", "scripts/mgt2eMigration.js", "scripts/npcRollTableSync.js", - "scripts/mgt2eSkills.js" + "scripts/mgt2eSkills.js", + "scripts/imperialCalendar.js" ], "styles": [ "styles/commerce.css", diff --git a/scripts/CalendarApp.js b/scripts/CalendarApp.js new file mode 100644 index 0000000..b07a00c --- /dev/null +++ b/scripts/CalendarApp.js @@ -0,0 +1,62 @@ +import { formatImperialDate } from './calendarDate.js'; + +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; +const MODULE_ID = 'mgt2-compendium-amiral-denisov'; + +export class CalendarApp extends HandlebarsApplicationMixin(ApplicationV2) { + static DEFAULT_OPTIONS = { + id: 'mgt2-calendar', + classes: ['mgt2-calendar'], + position: { width: 260, height: 'auto' }, + window: { + icon: 'fas fa-calendar-alt', + title: 'Calendrier Impérial', + resizable: false, + }, + }; + + static PARTS = { + main: { + template: `modules/${MODULE_ID}/templates/calendar-app.hbs`, + root: true, + }, + }; + + constructor({ onConfig, onClose } = {}) { + super(); + this._onConfig = onConfig; + this._onClose = onClose; + this._data = { year: 1116, day: 1, hour: 12, minute: 0, planet: '' }; + } + + get title() { + return 'Calendrier Impérial'; + } + + async _prepareContext() { + return { + dateLabel: formatImperialDate(this._data), + isGM: game.user?.isGM ?? false, + }; + } + + async _onRender(context, options) { + await super._onRender(context, options); + const html = $(this.element); + html.find('.mgt2-cal-config-btn').on('click', () => { + this._onConfig?.(); + }); + } + + updateDisplay(data) { + this._data = { ...this._data, ...data }; + this.render(); + } + + close() { + this._onConfig = null; + this._onClose?.(); + this._onClose = null; + return super.close(); + } +} diff --git a/scripts/CalendarConfigDialog.js b/scripts/CalendarConfigDialog.js new file mode 100644 index 0000000..b8bf495 --- /dev/null +++ b/scripts/CalendarConfigDialog.js @@ -0,0 +1,116 @@ +import { getDayLabel, computeConversions } from './calendarDate.js'; + +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; +const MODULE_ID = 'mgt2-compendium-amiral-denisov'; + +export class CalendarConfigDialog extends HandlebarsApplicationMixin(ApplicationV2) { + static DEFAULT_OPTIONS = { + id: 'mgt2-calendar-config', + classes: ['mgt2-calendar'], + position: { width: 300, height: 'auto' }, + window: { + icon: 'fas fa-cog', + title: 'Configurer le calendrier', + resizable: false, + }, + }; + + static PARTS = { + main: { + template: `modules/${MODULE_ID}/templates/calendar-config.hbs`, + root: true, + }, + }; + + constructor({ data, onApply } = {}) { + super(); + this._data = { ...data }; + this._onApply = onApply; + } + + get title() { + return 'Configurer le calendrier'; + } + + async _prepareContext() { + return { + year: this._data.year, + day: this._data.day, + hour: this._data.hour, + minute: this._data.minute, + planet: this._data.planet ?? '', + dayLabel: getDayLabel(this._data.day), + conversions: computeConversions(this._data.year), + }; + } + + async _onRender(context, options) { + await super._onRender(context, options); + const html = $(this.element); + + html.find('[name="day"]').on('input', () => { + const day = parseInt(html.find('[name="day"]').val(), 10); + if (day >= 1 && day <= 365) { + html.find('.mgt2-cal-day-label').text(getDayLabel(day)); + } + }); + + html.find('[name="year"]').on('input', () => { + const year = parseInt(html.find('[name="year"]').val(), 10) || 0; + const conv = computeConversions(year); + html.find('.mgt2-cal-conv-val[data-cal="solomani"]').text(conv.solomani); + html.find('.mgt2-cal-conv-val[data-cal="vilani"]').text(conv.vilani); + html.find('.mgt2-cal-conv-val[data-cal="zhodani"]').text(conv.zhodani); + html.find('.mgt2-cal-conv-val[data-cal="aslan"]').text(conv.aslan); + html.find('.mgt2-cal-conv-val[data-cal="kkree"]').text(conv.kkree); + html.find('.mgt2-cal-conv-val[data-cal="hiver"]').text(conv.hiver); + }); + + html.find('[data-action="day-inc"]').on('click', () => { + const input = html.find('[name="day"]'); + let day = parseInt(input.val(), 10) || 1; + day = day >= 365 ? 1 : day + 1; + input.val(day); + html.find('.mgt2-cal-day-label').text(getDayLabel(day)); + }); + + html.find('[data-action="hour-inc"]').on('click', () => { + const dayInput = html.find('[name="day"]'); + const hourInput = html.find('[name="hour"]'); + const minuteInput = html.find('[name="minute"]'); + let day = parseInt(dayInput.val(), 10) || 1; + let hour = parseInt(hourInput.val(), 10) || 0; + let minute = parseInt(minuteInput.val(), 10) || 0; + + hour += 1; + if (hour >= 24) { + hour = 0; + day = day >= 365 ? 1 : day + 1; + } + + hourInput.val(hour); + dayInput.val(day); + html.find('.mgt2-cal-day-label').text(getDayLabel(day)); + }); + + html.find('[data-action="apply"]').on('click', () => { + this._readAndApply(html); + }); + } + + _readAndApply(html) { + const year = parseInt(html.find('[name="year"]').val(), 10) || 1116; + const day = Math.min(365, Math.max(1, parseInt(html.find('[name="day"]').val(), 10) || 1)); + const hour = Math.min(23, Math.max(0, parseInt(html.find('[name="hour"]').val(), 10) || 0)); + const minute = Math.min(59, Math.max(0, parseInt(html.find('[name="minute"]').val(), 10) || 0)); + const planet = html.find('[name="planet"]').val()?.trim() ?? ''; + + const data = { year, day, hour, minute, planet }; + this._onApply?.(data); + } + + close() { + this._onApply = null; + return super.close(); + } +} diff --git a/scripts/calendarDate.js b/scripts/calendarDate.js new file mode 100644 index 0000000..5173245 --- /dev/null +++ b/scripts/calendarDate.js @@ -0,0 +1,95 @@ +const MODULE_ID = 'mgt2-compendium-amiral-denisov'; + +export function imperialToSolomani(impYear) { + return Math.round(impYear * 1.0006644 + 4521); +} + +export function imperialToVilani(impYear) { + return Math.round((impYear + 4045) / 1.312); +} + +export function imperialToZhodani(impYear) { + const raw = (impYear + 6731) / 2.26; + const olympiad = Math.floor(raw); + const frac = raw - olympiad; + const year = Math.min(3, Math.max(1, Math.round(frac * 3))); + return { olympiad, year, label: `${olympiad}.${year}` }; +} + +export function imperialToAslan(impYear) { + return Math.round((impYear + 2083) / 0.877); +} + +export function imperialToKkree(impYear) { + return Math.round((impYear + 8145) / 1.2); +} + +export function imperialToHiver(impYear) { + return Math.round((impYear - 410) * 2); +} + +export function computeConversions(impYear) { + const y = impYear ?? 1116; + const z = imperialToZhodani(y); + return { + solomani: `${imperialToSolomani(y)} AD`, + vilani: `${imperialToVilani(y)} VI`, + zhodani: z.label, + aslan: `${imperialToAslan(y)}`, + kkree: `${imperialToKkree(y)}`, + hiver: `${imperialToHiver(y)} HF`, + }; +} + +export const DAY_NAMES = [ + 'Holiday', + 'Wonday', + 'Tuday', + 'Thirday', + 'Forday', + 'Fiday', + 'Sixday', + 'Senday', +]; + +export const DAY_LABELS_FR = { + Holiday: 'Holiday', + Wonday: 'Wonday', + Tuday: 'Tuday', + Thirday: 'Thirday', + Forday: 'Forday', + Fiday: 'Fiday', + Sixday: 'Sixday', + Senday: 'Senday', +}; + +export function getDayLabel(day) { + if (day < 1 || day > 365) return '?'; + if (day === 1) return DAY_NAMES[0]; + return DAY_NAMES[((day - 2) % 7) + 1]; +} + +export function getWeekday(day) { + if (day < 1 || day > 365) return -1; + if (day === 1) return -1; + return (day - 2) % 7; +} + +export function formatImperialDate({ year, day, hour, minute, planet } = {}) { + const y = year ?? 1116; + const d = day ?? 1; + const dayLabel = getDayLabel(d); + const dayStr = String(d).padStart(3, '0'); + let label = `${dayLabel}, ${dayStr}-${y} IC`; + if (hour != null && minute != null) { + const h = String(Math.round(hour)).padStart(2, '0'); + const m = String(Math.round(minute)).padStart(2, '0'); + label += ` ─ ${h}:${m}`; + if (planet) label += ` (${planet})`; + } + return label; +} + +export function getDefaultCalendar() { + return { year: 1116, day: 1, hour: 12, minute: 0, planet: '' }; +} diff --git a/scripts/imperialCalendar.js b/scripts/imperialCalendar.js new file mode 100644 index 0000000..f41a3cc --- /dev/null +++ b/scripts/imperialCalendar.js @@ -0,0 +1,208 @@ +import { CalendarApp } from './CalendarApp.js'; +import { CalendarConfigDialog } from './CalendarConfigDialog.js'; +import { getDefaultCalendar } from './calendarDate.js'; + +const MODULE_ID = 'mgt2-compendium-amiral-denisov'; +const ChatLogV2 = foundry.applications.sidebar.tabs.ChatLog; + +let _calendarApp = null; + +function readCalendar() { + return { + year: game.settings.get(MODULE_ID, 'calendarYear'), + day: game.settings.get(MODULE_ID, 'calendarDay'), + hour: game.settings.get(MODULE_ID, 'calendarHour'), + minute: game.settings.get(MODULE_ID, 'calendarMinute'), + planet: game.settings.get(MODULE_ID, 'calendarPlanet'), + }; +} + +async function writeCalendar(data) { + await game.settings.set(MODULE_ID, 'calendarYear', data.year); + await game.settings.set(MODULE_ID, 'calendarDay', data.day); + await game.settings.set(MODULE_ID, 'calendarHour', data.hour); + await game.settings.set(MODULE_ID, 'calendarMinute', data.minute); + await game.settings.set(MODULE_ID, 'calendarPlanet', data.planet); +} + +function applyCalendar(data) { + if (_calendarApp) { + _calendarApp.updateDisplay(data); + } +} + +async function broadcastCalendar(data) { + if (!game.socket?.emit) return; + game.socket.emit(`module.${MODULE_ID}`, { + _type: 'calendarSync', + ...data, + }); +} + +function openCalendarApp() { + if (_calendarApp) { + _calendarApp.bringToTop(); + return; + } + const data = readCalendar(); + _calendarApp = new CalendarApp({ + onConfig: () => openCalendarConfig(), + onClose: () => { _calendarApp = null; }, + }); + _calendarApp.updateDisplay(data); + _calendarApp.render({ force: true }); +} + +function openCalendarConfig() { + const data = readCalendar(); + const dialog = new CalendarConfigDialog({ + data, + onApply: async (newData) => { + await writeCalendar(newData); + applyCalendar(newData); + broadcastCalendar(newData); + dialog.close(); + }, + }); + dialog.render({ force: true }); +} + +function registerCalCommand() { + if (!ChatLogV2?.CHAT_COMMANDS) return; + ChatLogV2.CHAT_COMMANDS.cal = { + rgx: /^\/cal(?:\s+(.*))?$/i, + fn: () => { + openCalendarApp(); + return false; + }, + }; + console.log(`${MODULE_ID} | Commande /cal enregistrée via ChatLog.CHAT_COMMANDS`); +} + +Hooks.once('init', () => { + const def = getDefaultCalendar(); + + game.settings.register(MODULE_ID, 'calendarYear', { + name: 'Année impériale', + scope: 'world', + config: false, + type: Number, + default: def.year, + }); + game.settings.register(MODULE_ID, 'calendarDay', { + name: 'Jour impérial (1-365)', + scope: 'world', + config: false, + type: Number, + default: def.day, + }); + game.settings.register(MODULE_ID, 'calendarHour', { + name: 'Heure locale', + scope: 'world', + config: false, + type: Number, + default: def.hour, + }); + game.settings.register(MODULE_ID, 'calendarMinute', { + name: 'Minute locale', + scope: 'world', + config: false, + type: Number, + default: def.minute, + }); + game.settings.register(MODULE_ID, 'calendarPlanet', { + name: 'Planète', + scope: 'world', + config: false, + type: String, + default: def.planet, + }); + game.settings.register(MODULE_ID, 'calendarAutoOpen', { + name: 'Calendrier impérial : auto-ouverture', + hint: 'Ouvre automatiquement la fenêtre du calendrier au démarrage', + scope: 'world', + config: true, + type: Boolean, + default: true, + }); + + const loadTemplatesFn = foundry.applications?.handlebars?.loadTemplates || loadTemplates; + if (loadTemplatesFn) { + loadTemplatesFn([ + `modules/${MODULE_ID}/templates/calendar-app.hbs`, + `modules/${MODULE_ID}/templates/calendar-config.hbs`, + ]); + } + + registerCalCommand(); + console.log(`${MODULE_ID} | Calendrier impérial initialisé`); +}); + +Hooks.once('ready', () => { + game.socket.on(`module.${MODULE_ID}`, (data) => { + if (game.user?.isGM) return; + if (data?._type !== 'calendarSync') return; + if (!_calendarApp) { + const { year, day, hour, minute, planet } = data; + _calendarApp = new CalendarApp({ + onClose: () => { _calendarApp = null; }, + }); + _calendarApp.updateDisplay({ year, day, hour, minute, planet }); + _calendarApp.render({ force: true }); + return; + } + _calendarApp.updateDisplay(data); + }); + + if (game.settings.get(MODULE_ID, 'calendarAutoOpen')) { + openCalendarApp(); + } +}); + +Hooks.on('preCreateChatMessage', (message, data, options) => { + const content = message.content?.trim()?.toLowerCase(); + if (content === '/cal' || content?.startsWith('/cal ')) { + openCalendarApp(); + return false; + } +}); + +Hooks.on('chatMessage', (...args) => { + let message; + if (args[0]?.content !== undefined) { + message = args[0].content; + } else if (typeof args[1] === 'string') { + message = args[1]; + } else { + return; + } + const trimmed = message?.trim()?.toLowerCase(); + if (trimmed === '/cal' || trimmed?.startsWith('/cal ')) { + openCalendarApp(); + return false; + } +}); + +Hooks.on('renderChatInput', (app, html, data) => { + let input; + if (html?.element) { + input = $(html.element).find('textarea[name="content"]'); + } else if (html?.find) { + input = html.find('textarea[name="content"]'); + } else { + input = $(html).find('textarea[name="content"]'); + } + if (input.data('mgt2-cal-listener')) return; + input.data('mgt2-cal-listener', true); + input.on('keydown', (event) => { + if (event.key === 'Enter' && !event.shiftKey) { + const content = input.val()?.trim(); + if (content === '/cal' || content?.startsWith('/cal ')) { + event.preventDefault(); + event.stopImmediatePropagation(); + openCalendarApp(); + input.val(''); + } + } + }); +}); diff --git a/scripts/startupNotice.js b/scripts/startupNotice.js index aa96907..b5d38ea 100644 --- a/scripts/startupNotice.js +++ b/scripts/startupNotice.js @@ -12,7 +12,7 @@ function buildStartupNoticeHtml() {
/commerce /pnj /rencontre /mission /sector
+ /cal /commerce /pnj /rencontre /mission /sector
diff --git a/styles/npc.css b/styles/npc.css
index 93623df..340d781 100644
--- a/styles/npc.css
+++ b/styles/npc.css
@@ -1162,3 +1162,193 @@ a.mgt2-world-link:hover {
color: #555;
margin-top: 4px;
}
+
+/* === Calendrier Impérial =================================================== */
+
+.mgt2-calendar .window-header {
+ background: linear-gradient(180deg, rgba(44, 44, 62, 0.96) 0%, rgba(30, 30, 43, 0.96) 100%);
+ border-bottom: 1px solid #c9a227;
+}
+
+.mgt2-calendar .window-title {
+ color: #d9b24c;
+ text-shadow: none;
+}
+
+.mgt2-calendar .window-content {
+ padding: 4px 8px;
+ background: #1a1a2e;
+ overflow: hidden;
+}
+
+.mgt2-cal-window {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ min-height: 28px;
+}
+
+.mgt2-cal-display {
+ flex: 1;
+}
+
+.mgt2-cal-date {
+ color: #d8c79a;
+ font-size: 0.95em;
+ font-family: 'Roboto', sans-serif;
+ white-space: nowrap;
+}
+
+.mgt2-cal-config-btn {
+ flex: 0 0 auto;
+ width: 22px;
+ height: 22px;
+ padding: 0;
+ border: none;
+ background: transparent;
+ color: #8a7a5a;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.85em;
+ border-radius: 3px;
+ transition: color 0.15s, background 0.15s;
+}
+
+.mgt2-cal-config-btn:hover {
+ color: #d9b24c;
+ background: rgba(201, 162, 39, 0.15);
+}
+
+/* Config dialog */
+
+.mgt2-cal-config {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 8px;
+ background: #1a1a2e;
+ color: #d8c79a;
+}
+
+.mgt2-cal-config-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.mgt2-cal-config-row label {
+ flex: 0 0 80px;
+ font-size: 0.85em;
+ color: #b8a87a;
+}
+
+.mgt2-cal-config-row input[type="number"],
+.mgt2-cal-config-row input[type="text"] {
+ flex: 1;
+ padding: 3px 6px;
+ border: 1px solid #3a3a4e;
+ border-radius: 3px;
+ background: #252540;
+ color: #d8c79a;
+ font-size: 0.85em;
+}
+
+.mgt2-cal-config-row input:focus {
+ outline: none;
+ border-color: #c9a227;
+}
+
+.mgt2-cal-day-group {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.mgt2-cal-day-group input {
+ flex: 0 0 60px;
+}
+
+.mgt2-cal-day-label {
+ font-size: 0.85em;
+ color: #d9b24c;
+ white-space: nowrap;
+}
+
+.mgt2-cal-config-actions {
+ display: flex;
+ gap: 6px;
+ justify-content: flex-end;
+ margin-top: 4px;
+}
+
+.mgt2-cal-btn {
+ padding: 4px 10px;
+ border: 1px solid #3a3a4e;
+ border-radius: 3px;
+ background: #252540;
+ color: #d8c79a;
+ font-size: 0.82em;
+ cursor: pointer;
+ transition: background 0.15s, border-color 0.15s;
+}
+
+.mgt2-cal-btn:hover {
+ background: #303050;
+ border-color: #c9a227;
+}
+
+.mgt2-cal-btn-primary {
+ background: linear-gradient(180deg, #c9a227 0%, #a8881e 100%);
+ color: #1a1a2e;
+ border-color: #c9a227;
+ font-weight: 600;
+}
+
+.mgt2-cal-btn-primary:hover {
+ background: linear-gradient(180deg, #d9b24c 0%, #c9a227 100%);
+}
+
+.mgt2-cal-conversions {
+ margin-top: 8px;
+ border: 1px solid #3a3a4e;
+ border-radius: 3px;
+ background: #1e1e32;
+}
+
+.mgt2-cal-conversions summary {
+ padding: 4px 8px;
+ color: #b8a87a;
+ font-size: 0.82em;
+ cursor: pointer;
+ user-select: none;
+}
+
+.mgt2-cal-conversions summary:hover {
+ color: #d9b24c;
+}
+
+.mgt2-cal-conv-note {
+ font-size: 0.85em;
+ color: #6a6a7e;
+ font-style: italic;
+}
+
+.mgt2-cal-conv-row {
+ display: flex;
+ justify-content: space-between;
+ padding: 2px 8px;
+ font-size: 0.82em;
+ color: #d8c79a;
+}
+
+.mgt2-cal-conv-row:nth-child(even) {
+ background: rgba(255,255,255,0.03);
+}
+
+.mgt2-cal-conv-label {
+ color: #8a7a5a;
+}
diff --git a/templates/calendar-app.hbs b/templates/calendar-app.hbs
new file mode 100644
index 0000000..375a4f8
--- /dev/null
+++ b/templates/calendar-app.hbs
@@ -0,0 +1,10 @@
+