diff --git a/Calendar and Weather/1.0/Calendar and weather.js b/Calendar and Weather/1.0/Calendar and weather.js new file mode 100644 index 000000000..171e86dc7 --- /dev/null +++ b/Calendar and Weather/1.0/Calendar and weather.js @@ -0,0 +1,581 @@ +on('ready', () => { + // Styles + const chatStyle = 'background-color:#926239; border:1px solid #000; border-radius:8px; padding:8px; width:100%; height:fit-content; font-size:1.1em;'; + const hr = '
...or after a marker) + let jsonMatch = notes.match(/
([\s\S]+?)<\/pre>/) || notes.match(/([\s\S]+)$/);
+ let json;
+ if (jsonMatch) {
+ try {
+ json = JSON.parse(jsonMatch[1]);
+ } catch (e) {
+ sendChat('WeatherMod', `/w gm Error: Invalid JSON in handout / JSON invalide dans le handout.`);
+ return;
+ }
+ } else {
+ sendChat('WeatherMod', `/w gm No JSON found in handout / Aucun JSON trouvé dans le handout.`);
+ return;
+ }
+ // Save imported profile
+ state.WeatherMod.profiles[name] = json;
+ sendChat('WeatherMod', `/w gm Profile "${name}" imported from handout / Profil "${name}" importé depuis le handout.`);
+ showGMMainMenu();
+ });
+ };
+
+ // Export profile to handout
+ const exportProfileToHandout = (name) => {
+ const profile = state.WeatherMod.profiles[name];
+ if (!profile) return;
+
+ // Save JSON in for easy import
+ const html = `
+ ${t('profiles')}: ${name}
+ ${t('climate')}: ${tClimate(profile.selectedClimate)}
+ ${t('date')}: ${profile.calendar.day} ${CalendarConfig.months[profile.calendar.month].name} ${profile.calendar.year}
+ ${t('language')}: ${profile.language}
+ ${t('manualMode')}: ${profile.settings.useManualWeather ? t('yes') : t('no')}
+ ${profile.settings.useManualWeather ? `
+ ${t('type')}: ${t(profile.settings.manualWeather.type)}
+ ${t('temp')}: ${profile.settings.manualWeather.temperature}°C
+ ${t('windSpeed')}: ${profile.settings.manualWeather.windSpeed} km/h ${t('windFrom')} ${tWindDir(profile.settings.manualWeather.windDirection)}
+ ${t('humidityShort')}: ${profile.settings.manualWeather.humidity !== undefined ? profile.settings.manualWeather.humidity : 50}%
+ ` : ''}
+
+ JSON:
+ ${JSON.stringify(profile, null, 2)}
+ ${JSON.stringify(profile)}
+ `;
+
+ let handout = findObjs({ type: "handout", name: `WeatherProfile_${name}` })[0];
+ if (!handout) {
+ handout = createObj("handout", { name: `WeatherProfile_${name}` });
+ }
+ handout.set({ notes: html });
+ };
+
+ // Advance one day
+ const advanceDay = () => {
+ const c = state.WeatherMod.calendar;
+ c.day++;
+ c.totalDays++;
+ const max = CalendarConfig.months[c.month].length;
+ if (c.day > max) {
+ c.day = 1;
+ c.month++;
+ if (c.month >= CalendarConfig.months.length) {
+ c.month = 0;
+ c.year++;
+ }
+ }
+ };
+
+ // State initialization
+ if (!state.WeatherMod) {
+ state.WeatherMod = {
+ language: 'fr',
+ selectedClimate: 'temperate',
+ calendar: { day: 1, month: 0, year: 1000, totalDays: 0 },
+ settings: {
+ useManualWeather: false,
+ manualWeather: { type: "clear", windDirection: "north", temperature: 20, windSpeed: 10, humidity: 50 }
+ },
+ profiles: {}
+ };
+ }
+
+ // Chat commands
+ on('chat:message', (msg) => {
+ if (msg.type !== 'api' || !playerIsGM(msg.playerid)) return;
+
+ const args = msg.content.trim().split(" ");
+ const command = args[0];
+ const subcommand = args[1];
+ const value = args.slice(2).join(" ");
+
+ if (command !== '!weather') return;
+
+ switch (subcommand) {
+ case 'report': displayFullReport(); break;
+ case 'showplayers': showWeatherToPlayers(); break;
+ case 'menu': showGMMainMenu(); break;
+ case 'menu-date': showDateMenu(); break;
+ case 'menu-manual': showManualWeatherMenu(); break;
+ case 'menu-profiles': showProfilesMenu(); break;
+
+ case 'next':
+ case 'next-day':
+ advanceDay();
+ displayFullReport();
+ break;
+
+ case 'lang':
+ if (['en', 'fr'].includes(args[2])) {
+ state.WeatherMod.language = args[2];
+ sendChat("WeatherMod", `/w gm ${t('language')} : ${args[2].toUpperCase()}`);
+ } else {
+ sendChat("WeatherMod", `/w gm ${t('language')}: en, fr`);
+ }
+ break;
+
+ case 'setgm': {
+ const param = args[2];
+ const val = args.slice(3).join(" ");
+ const s = state.WeatherMod;
+ const manual = s.settings.manualWeather;
+
+ switch (param) {
+ case 'climate':
+ if (val in WeatherConfig.climates) s.selectedClimate = val;
+ break;
+ case 'manual':
+ s.settings.useManualWeather = (val === 'on');
+ break;
+ case 'weathertype':
+ manual.type = val;
+ break;
+ case 'winddir':
+ manual.windDirection = val;
+ break;
+ case 'temp':
+ const tval = parseInt(val, 10);
+ if (!isNaN(tval)) manual.temperature = tval;
+ break;
+ case 'windspeed':
+ const wval = parseInt(val, 10);
+ if (!isNaN(wval)) manual.windSpeed = wval;
+ break;
+ case 'humidity':
+ const hval = parseInt(val, 10);
+ if (!isNaN(hval)) manual.humidity = hval;
+ break;
+ case 'day':
+ const d = parseInt(val, 10);
+ if (!isNaN(d)) s.calendar.day = d;
+ break;
+ case 'month':
+ const m = parseInt(val, 10);
+ if (!isNaN(m)) s.calendar.month = m;
+ break;
+ case 'year':
+ const y = parseInt(val, 10);
+ if (!isNaN(y)) s.calendar.year = y;
+ break;
+ }
+ showGMMainMenu();
+ break;
+ }
+
+ case 'save':
+ if (!value.trim()) {
+ sendChat('WeatherMod', `/w gm ${t('saveProfile')} : !weather save MonProfil`);
+ } else {
+ saveWeatherProfile(value.trim());
+ sendChat('WeatherMod', `/w gm ${t('saveProfile')}: ${value.trim()}`);
+ }
+ break;
+
+ case 'load':
+ if (!value.trim()) {
+ sendChat('WeatherMod', `/w gm ${t('loadProfile')} : !weather load MonProfil`);
+ } else {
+ loadWeatherProfile(value.trim());
+ sendChat('WeatherMod', `/w gm ${t('loadProfile')}: ${value.trim()}`);
+ showGMMainMenu();
+ }
+ break;
+
+ case 'export':
+ if (!value.trim()) {
+ sendChat('WeatherMod', `/w gm ${t('exportProfile')} : !weather export MonProfil`);
+ } else {
+ exportProfileToHandout(value.trim());
+ sendChat('WeatherMod', `/w gm ${t('exportProfile')}: WeatherProfile_${value.trim()}`);
+ }
+ break;
+
+ case 'import':
+ if (!value.trim()) {
+ sendChat('WeatherMod', `/w gm ${t('importProfile')} : !weather import MonProfil`);
+ } else {
+ importProfileFromHandout(value.trim());
+ showGMMainMenu();
+ }
+ break;
+ }
+ });
+});
diff --git a/Calendar and Weather/Calendar and Weather.js b/Calendar and Weather/Calendar and Weather.js
new file mode 100644
index 000000000..885d292b0
--- /dev/null
+++ b/Calendar and Weather/Calendar and Weather.js
@@ -0,0 +1,581 @@
+on('ready', () => {
+ // Styles
+ const chatStyle = 'background-color:#926239; border:1px solid #000; border-radius:8px; padding:8px; width:100%; height:fit-content; font-size:1.1em;';
+ const hr = '
';
+ const styleTitle = 'text-align:center; font-size:1.5em; font-weight:bold;';
+ const styleSection = 'font-size:1.3em; font-weight:bold; margin-top:8px;';
+ const styleCenter = 'text-align:center;';
+ const btnStyle = 'background-color:#574530; border:1px solid #352716; border-radius:4px; padding:2px 8px; font-size:0.9em; color:#fff; text-decoration:none; margin:2px; display:inline-block;';
+ const btnGroup = (html) => `${html}`;
+
+ // Configurations
+ const WeatherConfig = {
+ windForce: {
+ "1": { speed: [0, 11], chance: 55, name: "Slight Breeze" },
+ "2": { speed: [12, 38], chance: 25, name: "Nice Breeze" },
+ "3": { speed: [39, 88], chance: 12, name: "Strong Wind" },
+ "4": { speed: [89, 102], chance: 5, name: "Storm" },
+ "5": { speed: [103, 117], chance: 2, name: "Violent Storm" },
+ "6": { speed: [118, 200], chance: 1, name: "Hurricane" }
+ },
+ precipitationStrength: {
+ rain: { light: 55, moderate: 25, heavy: 15, torrential: 5 },
+ snow: { light: 65, moderate: 25, snowstorm: 10 },
+ thunderstorm: { slight: 55, moderate: 25, strong: 15, severe: 5 }
+ },
+ climates: {
+ temperate: {
+ humidity: [40, 60],
+ windChances: { north: 10, west: 20, east: 65, south: 5 },
+ temperature: {
+ spring: [[5,10],[10,15],[15,20],[20,30]],
+ summer: [[10,15],[15,20],[20,30],[30,40]],
+ fall: [[0,5],[5,10],[10,15],[15,20]],
+ winter: [[-5,0],[0,5],[5,10],[10,15]]
+ },
+ precipitation: {
+ spring: { clear: 40, rain: 53, thunderstorm: 7 },
+ summer: { clear: 42, rain: 46, thunderstorm: 12 },
+ fall: { clear: 43, rain: 53, thunderstorm: 4 },
+ winter: { clear: 35, rain: 60, thunderstorm: 5 }
+ }
+ },
+ desert: {
+ humidity: [5, 15],
+ windChances: { north: 5, west: 10, east: 20, south: 65 },
+ temperature: {
+ spring: [[15,20],[20,25],[25,35],[35,45]],
+ summer: [[20,25],[25,35],[35,45],[45,55]],
+ fall: [[10,15],[15,20],[20,25],[25,35]],
+ winter: [[-5,0],[0,5],[5,10],[10,15]]
+ },
+ precipitation: {
+ spring: { clear: 66, rain: 13, thunderstorm: 21 },
+ summer: { clear: 75, rain: 0, thunderstorm: 25 },
+ fall: { clear: 73, rain: 11, thunderstorm: 16 },
+ winter: { clear: 81, rain: 18, thunderstorm: 1 }
+ }
+ },
+ jungle: {
+ humidity: [70, 90],
+ windChances: { north: 5, west: 10, east: 65, south: 20 },
+ temperature: {
+ spring: [[10,15],[15,20],[20,30],[30,40]],
+ summer: [[15,20],[20,30],[30,40],[40,50]],
+ fall: [[5,10],[10,15],[15,20],[20,30]],
+ winter: [[0,5],[5,10],[10,15],[15,20]]
+ },
+ precipitation: {
+ spring: { clear: 21, rain: 51, thunderstorm: 28 },
+ summer: { clear: 74, rain: 15, thunderstorm: 11 },
+ fall: { clear: 12, rain: 44, thunderstorm: 44 },
+ winter: { clear: 15, rain: 48, thunderstorm: 37 }
+ }
+ },
+ cold: {
+ humidity: [35, 55],
+ windChances: { north: 65, west: 20, east: 10, south: 5 },
+ temperature: {
+ spring: [[-5,0],[0,5],[5,10],[10,15]],
+ summer: [[0,5],[5,10],[10,15],[15,20]],
+ fall: [[-10,-5],[-5,0],[0,5],[5,10]],
+ winter: [[-20,-10],[-10,-5],[-5,0],[0,5]]
+ },
+ precipitation: {
+ spring: { clear: 75, rain: 22, thunderstorm: 3 },
+ summer: { clear: 44, rain: 49, thunderstorm: 7 },
+ fall: { clear: 35, rain: 63, thunderstorm: 2 },
+ winter: { clear: 87, rain: 12, thunderstorm: 1 }
+ }
+ }
+ }
+ };
+
+ const CalendarConfig = {
+ days: ["Rilmor", "Eretor", "Nauri", "Neldir", "Veltor", "Eltor", "Mernach"],
+ months: [
+ { name: "Juras", length: 31 }, { name: "Fevnir", length: 28 }, { name: "Morsir", length: 31 },
+ { name: "Avalis", length: 30 }, { name: "Maï", length: 31 }, { name: "Jurn", length: 30 },
+ { name: "Jullirq", length: 31 }, { name: "Aors", length: 31 }, { name: "Septibir", length: 30 },
+ { name: "Octors", length: 31 }, { name: "Noval", length: 30 }, { name: "Devenir", length: 31 }
+ ],
+ seasons: [
+ { name: "spring", months: [2, 3, 4] },
+ { name: "summer", months: [5, 6, 7] },
+ { name: "fall", months: [8, 9, 10] },
+ { name: "winter", months: [11, 0, 1] }
+ ]
+ };
+
+ const MoonConfig = {
+ moons: [
+ { name: "Lunara", cycle: 28, phases: ["New", "Crescent", "First Quarter", "Gibbous", "Full", "Gibbous Waning", "Last Quarter", "Crescent Waning"] },
+ { name: "Virell", cycle: 35, phases: ["New", "First Quarter", "Full", "Last Quarter"] }
+ ]
+ };
+
+ // Translations
+ const i18n = {
+ en: {
+ climateNames: { temperate: "Temperate", desert: "Desert", jungle: "Jungle", cold: "Cold" },
+ moonPhases: {
+ "New": "New", "Crescent": "Crescent", "First Quarter": "First Quarter", "Gibbous": "Gibbous",
+ "Full": "Full", "Gibbous Waning": "Gibbous Waning", "Last Quarter": "Last Quarter", "Crescent Waning": "Crescent Waning"
+ },
+ windDirections: { north: "North", south: "South", east: "East", west: "West" },
+ windForces: {
+ "Slight Breeze": "Slight Breeze", "Nice Breeze": "Nice Breeze", "Strong Wind": "Strong Wind",
+ "Storm": "Storm", "Violent Storm": "Violent Storm", "Hurricane": "Hurricane", "Manual": "Manual Wind"
+ },
+ seasonNames: { spring: "Spring", summer: "Summer", fall: "Autumn", winter: "Winter" },
+ date: "Date", season: "Season", moon: "Moon Phases", weather: "Weather Report", climate: "Climate",
+ temperature: "Temperature", humidity: "Humidity", wind: "Wind", precipitation: "Precipitation",
+ clear: "Clear", rain: "Rain", snow: "Snow", thunderstorm: "Thunderstorm", windFrom: "from",
+ manual: "Manual Weather Mode", generate: "Generate Weather", setDay: "Set Day", setYear: "Set Year",
+ saveProfile: "Save Current", loadProfile: "Load", exportProfile: "Export to handout", importProfile: "Import from handout", month: "Month",
+ language: "Language", profiles: "Profiles", manualMode: "Manual Mode", type: "Type", temp: "Temp",
+ windSpeed: "Wind", back: "Back", yes: "Yes", no: "No", humidityShort: "Humidity"
+ },
+ fr: {
+ climateNames: { temperate: "Tempéré", desert: "Désertique", jungle: "Jungle", cold: "Froid" },
+ moonPhases: {
+ "New": "Nouvelle Lune", "Crescent": "Premier Croissant", "First Quarter": "Premier Quartier",
+ "Gibbous": "Gibbeuse Croissante", "Full": "Pleine Lune", "Gibbous Waning": "Gibbeuse Décroissante",
+ "Last Quarter": "Dernier Quartier", "Crescent Waning": "Dernier Croissant"
+ },
+ windDirections: { north: "Nord", south: "Sud", east: "Est", west: "Ouest" },
+ windForces: {
+ "Slight Breeze": "Brise Légère", "Nice Breeze": "Belle Brise", "Strong Wind": "Vent Fort",
+ "Storm": "Tempête", "Violent Storm": "Tempête Violente", "Hurricane": "Ouragan", "Manual": "Vent Manuel"
+ },
+ seasonNames: { spring: "Printemps", summer: "Été", fall: "Automne", winter: "Hiver" },
+ date: "Date", season: "Saison", moon: "Phases Lunaires", weather: "Météo", climate: "Climat",
+ temperature: "Température", humidity: "Humidité", wind: "Vent", precipitation: "Précipitations",
+ clear: "Clair", rain: "Pluie", snow: "Neige", thunderstorm: "Orage", windFrom: "depuis le",
+ manual: "Mode Météo Manuel", generate: "Générer la Météo", setDay: "Définir le Jour", setYear: "Définir l'Année",
+ saveProfile: "Sauvegarder", loadProfile: "Charger", exportProfile: "Exporter vers un handout", importProfile: "Importer depuis un handout", month: "Mois",
+ language: "Langue", profiles: "Profils", manualMode: "Mode Manuel", type: "Type", temp: "Temp",
+ windSpeed: "Vent", back: "Retour", yes: "Oui", no: "Non", humidityShort: "Humidité"
+ }
+ };
+
+ // Translation functions
+ const lang = () => state.WeatherMod?.language || 'en';
+ const t = (key) => i18n[lang()]?.[key] || key;
+ const tClimate = (key) => i18n[lang()].climateNames?.[key] || key;
+ const tPhase = (key) => i18n[lang()].moonPhases?.[key] || key;
+ const tWindDir = (key) => i18n[lang()].windDirections?.[key] || key;
+ const tWindForce = (key) => i18n[lang()].windForces?.[key] || key;
+ const tSeason = (key) => i18n[lang()].seasonNames?.[key] || key;
+
+ // Icons
+ const climateIcons = { temperate: "🌳", desert: "🏜️", jungle: "🌴", cold: "❄️" };
+ const seasonIcons = { spring: "🌼", summer: "☀️", fall: "🍂", winter: "❄️" };
+ const moonIcons = {
+ "New": "🌑", "Crescent": "🌒", "First Quarter": "🌓", "Gibbous": "🌔", "Full": "🌕",
+ "Gibbous Waning": "🌖", "Last Quarter": "🌗", "Crescent Waning": "🌘"
+ };
+ const skyIcons = { clear: "☀️", rain: "🌧️", snow: "❄️", thunderstorm: "⛈️" };
+
+ const tempIcon = (t) => t < -10 ? "🧊" : t < 0 ? "🥶" : t < 10 ? "❄️" : t < 20 ? "🌤️" : t < 30 ? "☀️" : t < 40 ? "🔥" : "🌋";
+ const humidityIcon = (h) => h < 20 ? "🌵" : h < 40 ? "💨" : h < 60 ? "🌤️" : h < 80 ? "💧" : "🌫️";
+ const windSpeedIcon = (s) => s <= 5 ? "🌬️" : s <= 20 ? "🍃" : s <= 40 ? "💨" : s <= 70 ? "🌪️" : s <= 100 ? "🌬️🌩️" : "🌀";
+
+ // Utility functions
+ const getSeason = () => {
+ const m = state.WeatherMod.calendar.month;
+ return CalendarConfig.seasons.find(s => s.months.includes(m))?.name || "spring";
+ };
+
+ const getMoonPhases = (day) => MoonConfig.moons.map(m => {
+ const idx = Math.floor((day % m.cycle) / m.cycle * m.phases.length);
+ return `${m.name}: ${m.phases[idx]}`;
+ });
+
+ const randomWeighted = (table) => {
+ const total = Object.values(table).reduce((a, b) => a + b, 0);
+ let roll = randomInteger(total);
+ for (const key in table) {
+ roll -= table[key];
+ if (roll <= 0) return key;
+ }
+ };
+
+ const randomRangeFromList = (ranges) => {
+ const [min, max] = ranges[Math.floor(Math.random() * ranges.length)];
+ return randomInteger(max - min + 1) + min - 1;
+ };
+
+ const clearOldWeatherMessages = () => {
+ sendChat("WeatherMod", ``);
+ };
+
+ // Build the full weather report HTML
+ const buildFullWeatherReportHTML = () => {
+ const c = state.WeatherMod.calendar;
+ const dayName = CalendarConfig.days[(c.day - 1) % CalendarConfig.days.length];
+ const monthName = CalendarConfig.months[c.month].name;
+ const season = getSeason();
+ const s = state.WeatherMod.settings;
+ const climate = WeatherConfig.climates[state.WeatherMod.selectedClimate];
+
+ let temp, humidity, windSpeed, windOrigin, windForce, precipType, precipStrength;
+
+ if (s.useManualWeather) {
+ temp = s.manualWeather.temperature;
+ windSpeed = s.manualWeather.windSpeed;
+ windOrigin = s.manualWeather.windDirection;
+ precipType = s.manualWeather.type;
+ humidity = s.manualWeather.humidity !== undefined ? s.manualWeather.humidity : "-";
+ windForce = { name: tWindForce("Manual") };
+ precipStrength = `${skyIcons[precipType] || ""} ${t(precipType)}`;
+ } else {
+ humidity = randomInteger(climate.humidity[1] - climate.humidity[0]) + climate.humidity[0];
+ windOrigin = randomWeighted(climate.windChances);
+ const forceKey = randomWeighted(Object.fromEntries(Object.entries(WeatherConfig.windForce).map(([k, v]) => [k, v.chance])));
+ windForce = WeatherConfig.windForce[forceKey];
+ windSpeed = randomInteger(windForce.speed[1] - windForce.speed[0]) + windForce.speed[0];
+ temp = randomRangeFromList(climate.temperature[season]);
+ precipType = randomWeighted(climate.precipitation[season]);
+
+ if (precipType === 'rain') {
+ precipStrength = (temp <= 0)
+ ? `❄️ ${t('snow')} (${randomWeighted(WeatherConfig.precipitationStrength.snow)})`
+ : `🌧️ ${t('rain')} (${randomWeighted(WeatherConfig.precipitationStrength.rain)})`;
+ } else if (precipType === 'thunderstorm') {
+ precipStrength = `⛈️ ${t('thunderstorm')} (${randomWeighted(WeatherConfig.precipitationStrength.thunderstorm)})`;
+ } else {
+ precipStrength = `☀️ ${t('clear')}`;
+ }
+ }
+
+ const moon = getMoonPhases(c.totalDays).map(m => {
+ const [name, phase] = m.split(": ");
+ return `${moonIcons[phase] || "🌑"} ${name}: ${tPhase(phase)}`;
+ }).join("
");
+
+ let html = ``;
+ html += `${t('weather')}`;
+ html += `${t('date')}: ${lang() === 'fr' ? `${dayName} ${c.day} ${monthName} ${c.year}` : `${monthName} ${c.day}, ${c.year} (${dayName})`}`;
+ html += `${t('season')}: ${seasonIcons[season]} ${tSeason(season)}`;
+ html += `${hr}${t('moon')}:${moon}${hr}`;
+ html += `${t('climate')}: ${climateIcons[state.WeatherMod.selectedClimate]} ${tClimate(state.WeatherMod.selectedClimate)}`;
+ html += `${t('temperature')}: ${temp}°C ${tempIcon(temp)}`;
+ html += `${t('humidity')}: ${humidity}${humidity !== "-" ? "%" : ""} ${humidity !== "-" ? humidityIcon(humidity) : ""}`;
+ html += `${t('wind')}: ${tWindForce(windForce.name)} (${windSpeed} km/h) ${t('windFrom')} ${tWindDir(windOrigin)} ${windSpeedIcon(windSpeed)}`;
+ html += `${t('precipitation')}: ${precipStrength}`;
+ html += ``;
+ return html;
+ };
+
+ const displayFullReport = () => {
+ clearOldWeatherMessages();
+ sendChat("WeatherMod", `/w gm ${buildFullWeatherReportHTML()}`);
+ };
+
+ const showWeatherToPlayers = () => {
+ sendChat("WeatherMod", buildFullWeatherReportHTML());
+ };
+
+ // Styled menus
+ const showGMMainMenu = () => {
+ const s = state.WeatherMod;
+ const climates = Object.keys(WeatherConfig.climates).map(climate =>
+ `${climateIcons[climate]} ${tClimate(climate)}`
+ ).join(" ");
+
+ const html = `
+ ${t('weather')}${hr}
+ ${t('climate')}: ${climateIcons[s.selectedClimate]} ${tClimate(s.selectedClimate)}
+ ${btnGroup(climates)}${hr}
+ ${t('language')}: ${s.language.toUpperCase()}
+ ${btnGroup(`EN FR`)}${hr}
+ ${btnGroup(`📅 ${t('date')}`)}
+ ${btnGroup(`🛠 ${t('manual')}`)}
+ ${btnGroup(`💾 ${t('profiles')}`)}
+ ${btnGroup(`🌦 ${t('generate')}`)}
+ ${btnGroup(`📣 ${t('weather')} → Players`)}
+ `;
+ sendChat("WeatherMod", `/w gm ${html}`);
+ };
+
+ const showDateMenu = () => {
+ const c = state.WeatherMod.calendar;
+ const months = CalendarConfig.months.map((m, i) =>
+ `${m.name}`
+ ).join(" ");
+ const html = `
+ ${t('date')}${hr}
+ ${btnGroup(`${t('setDay')} ${t('setYear')}`)}
+ ${hr}${btnGroup(months)}${hr}
+ ${btnGroup(`⬅️ ${t('back')}`)}
+ `;
+ sendChat("WeatherMod", `/w gm ${html}`);
+ };
+
+ const showManualWeatherMenu = () => {
+ const manual = state.WeatherMod.settings.manualWeather;
+ const weatherTypes = ['clear', 'rain', 'snow', 'thunderstorm'].map(type =>
+ `${skyIcons[type]} ${t(type)}`
+ ).join(" ");
+ const windDirs = ['north', 'east', 'south', 'west'].map(dir =>
+ `${tWindDir(dir)}`
+ ).join(" ");
+
+ const html = `
+ ${t('manual')}${hr}
+ ${t('manualMode')}: ${state.WeatherMod.settings.useManualWeather ? "🟢" : "🔴"} ${state.WeatherMod.settings.useManualWeather ? t('yes') : t('no')}
+ ${t('precipitation')}:${btnGroup(weatherTypes)}
+ ${t('temperature')}: ${manual.temperature}°C
+ ${t('windSpeed')}: ${manual.windSpeed} km/h
+ ${t('windFrom')}: ${btnGroup(windDirs)}
+ ${t('humidityShort')}: ${manual.humidity !== undefined ? manual.humidity : 50}%
+ ${hr}${btnGroup(`⬅️ ${t('back')}`)}
+ `;
+ sendChat("WeatherMod", `/w gm ${html}`);
+ };
+
+ const showProfilesMenu = () => {
+ const html = `
+ ${t('profiles')}${hr}
+ ${btnGroup(`
+ ${t('saveProfile')}
+ ${t('loadProfile')}
+ ${t('exportProfile')}
+ ${t('importProfile')}
`)}
+ ${hr}${btnGroup(`⬅️ ${t('back')}`)}
+ `;
+ sendChat("WeatherMod", `/w gm ${html}`);
+ };
+
+ // Weather profiles
+ const saveWeatherProfile = (name) => {
+ if (!name) return;
+ state.WeatherMod.profiles[name] = {
+ language: state.WeatherMod.language,
+ selectedClimate: state.WeatherMod.selectedClimate,
+ calendar: { ...state.WeatherMod.calendar },
+ settings: JSON.parse(JSON.stringify(state.WeatherMod.settings))
+ };
+ };
+
+ const loadWeatherProfile = (name) => {
+ if (!name || !state.WeatherMod.profiles[name]) return;
+ const data = state.WeatherMod.profiles[name];
+ state.WeatherMod.language = data.language;
+ state.WeatherMod.selectedClimate = data.selectedClimate;
+ state.WeatherMod.calendar = { ...data.calendar };
+ state.WeatherMod.settings = JSON.parse(JSON.stringify(data.settings));
+ };
+
+ // Import a weather profile from a handout
+ const importProfileFromHandout = (name) => {
+ const handout = findObjs({ type: "handout", name: `WeatherProfile_${name}` })[0];
+ if (!handout) {
+ sendChat('WeatherMod', `/w gm [${name}] Handout not found / Handout introuvable.`);
+ return;
+ }
+ handout.get('notes', (notes) => {
+ // Try to extract JSON from the handout notes (between ...
or after a marker)
+ let jsonMatch = notes.match(/([\s\S]+?)<\/pre>/) || notes.match(/([\s\S]+)$/);
+ let json;
+ if (jsonMatch) {
+ try {
+ json = JSON.parse(jsonMatch[1]);
+ } catch (e) {
+ sendChat('WeatherMod', `/w gm Error: Invalid JSON in handout / JSON invalide dans le handout.`);
+ return;
+ }
+ } else {
+ sendChat('WeatherMod', `/w gm No JSON found in handout / Aucun JSON trouvé dans le handout.`);
+ return;
+ }
+ // Save imported profile
+ state.WeatherMod.profiles[name] = json;
+ sendChat('WeatherMod', `/w gm Profile "${name}" imported from handout / Profil "${name}" importé depuis le handout.`);
+ showGMMainMenu();
+ });
+ };
+
+ // Export profile to handout
+ const exportProfileToHandout = (name) => {
+ const profile = state.WeatherMod.profiles[name];
+ if (!profile) return;
+
+ // Save JSON in for easy import
+ const html = `
+ ${t('profiles')}: ${name}
+ ${t('climate')}: ${tClimate(profile.selectedClimate)}
+ ${t('date')}: ${profile.calendar.day} ${CalendarConfig.months[profile.calendar.month].name} ${profile.calendar.year}
+ ${t('language')}: ${profile.language}
+ ${t('manualMode')}: ${profile.settings.useManualWeather ? t('yes') : t('no')}
+ ${profile.settings.useManualWeather ? `
+ ${t('type')}: ${t(profile.settings.manualWeather.type)}
+ ${t('temp')}: ${profile.settings.manualWeather.temperature}°C
+ ${t('windSpeed')}: ${profile.settings.manualWeather.windSpeed} km/h ${t('windFrom')} ${tWindDir(profile.settings.manualWeather.windDirection)}
+ ${t('humidityShort')}: ${profile.settings.manualWeather.humidity !== undefined ? profile.settings.manualWeather.humidity : 50}%
+ ` : ''}
+
+ JSON:
+ ${JSON.stringify(profile, null, 2)}
+ ${JSON.stringify(profile)}
+ `;
+
+ let handout = findObjs({ type: "handout", name: `WeatherProfile_${name}` })[0];
+ if (!handout) {
+ handout = createObj("handout", { name: `WeatherProfile_${name}` });
+ }
+ handout.set({ notes: html });
+ };
+
+ // Advance one day
+ const advanceDay = () => {
+ const c = state.WeatherMod.calendar;
+ c.day++;
+ c.totalDays++;
+ const max = CalendarConfig.months[c.month].length;
+ if (c.day > max) {
+ c.day = 1;
+ c.month++;
+ if (c.month >= CalendarConfig.months.length) {
+ c.month = 0;
+ c.year++;
+ }
+ }
+ };
+
+ // State initialization
+ if (!state.WeatherMod) {
+ state.WeatherMod = {
+ language: 'fr',
+ selectedClimate: 'temperate',
+ calendar: { day: 1, month: 0, year: 1000, totalDays: 0 },
+ settings: {
+ useManualWeather: false,
+ manualWeather: { type: "clear", windDirection: "north", temperature: 20, windSpeed: 10, humidity: 50 }
+ },
+ profiles: {}
+ };
+ }
+
+ // Chat commands
+ on('chat:message', (msg) => {
+ if (msg.type !== 'api' || !playerIsGM(msg.playerid)) return;
+
+ const args = msg.content.trim().split(" ");
+ const command = args[0];
+ const subcommand = args[1];
+ const value = args.slice(2).join(" ");
+
+ if (command !== '!weather') return;
+
+ switch (subcommand) {
+ case 'report': displayFullReport(); break;
+ case 'showplayers': showWeatherToPlayers(); break;
+ case 'menu': showGMMainMenu(); break;
+ case 'menu-date': showDateMenu(); break;
+ case 'menu-manual': showManualWeatherMenu(); break;
+ case 'menu-profiles': showProfilesMenu(); break;
+
+ case 'next':
+ case 'next-day':
+ advanceDay();
+ displayFullReport();
+ break;
+
+ case 'lang':
+ if (['en', 'fr'].includes(args[2])) {
+ state.WeatherMod.language = args[2];
+ sendChat("WeatherMod", `/w gm ${t('language')} : ${args[2].toUpperCase()}`);
+ } else {
+ sendChat("WeatherMod", `/w gm ${t('language')}: en, fr`);
+ }
+ break;
+
+ case 'setgm': {
+ const param = args[2];
+ const val = args.slice(3).join(" ");
+ const s = state.WeatherMod;
+ const manual = s.settings.manualWeather;
+
+ switch (param) {
+ case 'climate':
+ if (val in WeatherConfig.climates) s.selectedClimate = val;
+ break;
+ case 'manual':
+ s.settings.useManualWeather = (val === 'on');
+ break;
+ case 'weathertype':
+ manual.type = val;
+ break;
+ case 'winddir':
+ manual.windDirection = val;
+ break;
+ case 'temp':
+ const tval = parseInt(val, 10);
+ if (!isNaN(tval)) manual.temperature = tval;
+ break;
+ case 'windspeed':
+ const wval = parseInt(val, 10);
+ if (!isNaN(wval)) manual.windSpeed = wval;
+ break;
+ case 'humidity':
+ const hval = parseInt(val, 10);
+ if (!isNaN(hval)) manual.humidity = hval;
+ break;
+ case 'day':
+ const d = parseInt(val, 10);
+ if (!isNaN(d)) s.calendar.day = d;
+ break;
+ case 'month':
+ const m = parseInt(val, 10);
+ if (!isNaN(m)) s.calendar.month = m;
+ break;
+ case 'year':
+ const y = parseInt(val, 10);
+ if (!isNaN(y)) s.calendar.year = y;
+ break;
+ }
+ showGMMainMenu();
+ break;
+ }
+
+ case 'save':
+ if (!value.trim()) {
+ sendChat('WeatherMod', `/w gm ${t('saveProfile')} : !weather save MonProfil`);
+ } else {
+ saveWeatherProfile(value.trim());
+ sendChat('WeatherMod', `/w gm ${t('saveProfile')}: ${value.trim()}`);
+ }
+ break;
+
+ case 'load':
+ if (!value.trim()) {
+ sendChat('WeatherMod', `/w gm ${t('loadProfile')} : !weather load MonProfil`);
+ } else {
+ loadWeatherProfile(value.trim());
+ sendChat('WeatherMod', `/w gm ${t('loadProfile')}: ${value.trim()}`);
+ showGMMainMenu();
+ }
+ break;
+
+ case 'export':
+ if (!value.trim()) {
+ sendChat('WeatherMod', `/w gm ${t('exportProfile')} : !weather export MonProfil`);
+ } else {
+ exportProfileToHandout(value.trim());
+ sendChat('WeatherMod', `/w gm ${t('exportProfile')}: WeatherProfile_${value.trim()}`);
+ }
+ break;
+
+ case 'import':
+ if (!value.trim()) {
+ sendChat('WeatherMod', `/w gm ${t('importProfile')} : !weather import MonProfil`);
+ } else {
+ importProfileFromHandout(value.trim());
+ showGMMainMenu();
+ }
+ break;
+ }
+ });
+});
diff --git a/Calendar and Weather/README.md b/Calendar and Weather/README.md
new file mode 100644
index 000000000..ecc038756
--- /dev/null
+++ b/Calendar and Weather/README.md
@@ -0,0 +1,102 @@
+# Calendar and Weather Mod
+
+## English
+
+This mod is fully customizable. You can generate a report in chat with the current date, season, weather, and moon phases.
+
+### Configuration
+
+#### Weather Parameters
+
+To change the weather settings, edit the `WeatherConfig` object in the script:
+
+- **Wind strength**: Change the values in `windForce`.
+- **Precipitation strength**: Change the values in `precipitationStrength`.
+- For each climate (`climates`), you can modify:
+ - **Humidity**: `humidity`
+ - **Wind direction probability**: `windChances`
+ - **Temperature ranges for each season**: `temperature`
+ - **Weather probabilities**: `precipitation`
+
+#### Calendar Parameters
+
+To customize the calendar, edit the `CalendarConfig` object:
+
+- **Day names**: Change or add/remove days in `days`.
+- **Month names and lengths**: Change or add/remove months in `months`.
+- **Season months**: Change which months belong to each season in `seasons` (months are zero-indexed: first month = 0).
+
+#### Moon Parameters
+
+Edit the `MoonConfig` object to change:
+
+- **Moon names**
+- **Cycle length**
+- **Phase names** (in order)
+- You can add or remove moons, following the format.
+
+---
+
+## Français
+
+Ce mod est entièrement personnalisable. Il permet de générer un rapport dans le chat avec la date, la saison, la météo et les phases de la lune.
+
+### Configuration
+
+#### Paramètres de la météo
+
+Pour modifier la météo, éditez l'objet `WeatherConfig` dans le script :
+
+- **Force du vent** : Modifiez les valeurs dans `windForce`.
+- **Force des précipitations** : Modifiez les valeurs dans `precipitationStrength`.
+- Pour chaque climat (`climates`), vous pouvez modifier :
+ - **Humidité** : `humidity`
+ - **Probabilité de direction du vent** : `windChances`
+ - **Plages de températures par saison** : `temperature`
+ - **Probabilités de météo** : `precipitation`
+
+#### Paramètres du calendrier
+
+Pour personnaliser le calendrier, éditez l'objet `CalendarConfig` :
+
+- **Noms des jours** : Modifiez, ajoutez ou retirez des jours dans `days`.
+- **Noms et durées des mois** : Modifiez, ajoutez ou retirez des mois dans `months`.
+- **Mois des saisons** : Modifiez les mois associés à chaque saison dans `seasons` (les mois commencent à 0).
+
+#### Paramètres de la lune
+
+Modifiez l'objet `MoonConfig` pour changer :
+
+- **Noms des lunes**
+- **Durée du cycle**
+- **Noms des phases** (dans l'ordre)
+- Vous pouvez ajouter ou retirer des lunes en respectant le format.
+
+---
+
+## Command List / Liste des commandes
+
+**EN:** All commands are accessible from the GM menu.
+
+**FR :** Toutes les commandes sont accessibles depuis le menu MJ.
+
+| Commande / Command | Description (EN) | Description (FR) |
+|---------------------------|------------------------------------------|-----------------------------------------|
+| `!weather menu` | Show the GM main menu | Affiche le menu principal MJ |
+| `!weather report` | Show the full weather report to the GM | Affiche le rapport météo au MJ |
+| `!weather showplayers` | Show the weather report to all players | Affiche la météo à tous les joueurs |
+| `!weather menu-date` | Show the date settings menu | Affiche le menu de réglage de la date |
+| `!weather menu-manual` | Show the manual weather menu | Affiche le menu météo manuel |
+| `!weather menu-profiles` | Show the profiles menu | Affiche le menu des profils |
+| `!weather next` | Advance the calendar by one day | Avance le calendrier d'un jour |
+| `!weather lang en/fr` | Change the language | Change la langue |
+| `!weather save ` | Save the current weather profile | Sauvegarde le profil météo actuel |
+| `!weather load ` | Load a saved weather profile | Charge un profil météo |
+| `!weather export ` | Export a profile to a handout | Exporte un profil dans un handout |
+| `!weather import ` | Import a profile from a handout | Importe un profil depuis un handout |
+
+**EN:** To import a profile, use only the profile name you chose (for example: `weather1`).
+**Do not** include the `WeatherProfile_` prefix from the handout name.
+
+**FR :** Pour importer un profil, indiquez uniquement le nom du profil que vous avez choisi (par exemple : `meteo1`).
+**N’ajoutez pas** le préfixe `WeatherProfile_` du nom du handout.
diff --git a/Calendar and Weather/script.json b/Calendar and Weather/script.json
new file mode 100644
index 000000000..3b4f6c37b
--- /dev/null
+++ b/Calendar and Weather/script.json
@@ -0,0 +1,11 @@
+{
+ "name": "Calendar and Weather",
+ "script": "Calendar and Weather.js",
+ "version": "1.0",
+ "description": "An entirely customizable weather and calendar mod in english and french / Un mod pour le calendrier et la météo entièrement personnalisable en anglais et en français.",
+ "authors": "Maïlare",
+ "roll20userid": "3234089",
+ "dependencies": [],
+ "modifies":[],
+ "conflicts": []
+}