import {PANEL_TYP_INITIATIVE_TRACKER} from "./dmscreen-consts.js"; import {DmScreenUtil} from "./dmscreen-util.js"; import {EncounterBuilderHelpers, ListUtilBestiary} from "../utils-list-bestiary.js"; export class TimerTrackerMoonSpriteLoader { static _TIME_TRACKER_MOON_SPRITE = new Image(); static _TIME_TRACKER_MOON_SPRITE_LOADER = null; static _hasError = false; static async pInit () { this._TIME_TRACKER_MOON_SPRITE_LOADER ||= new Promise(resolve => { this._TIME_TRACKER_MOON_SPRITE.onload = resolve; this._TIME_TRACKER_MOON_SPRITE.onerror = () => { this._hasError = true; resolve(); }; }); this._TIME_TRACKER_MOON_SPRITE.src ||= Renderer.get().getMediaUrl("img", "dmscreen/moon.webp"); await this._TIME_TRACKER_MOON_SPRITE_LOADER; } static hasError () { return this._hasError; } static getImage () { return this._TIME_TRACKER_MOON_SPRITE; } } export class TimeTracker { static $getTracker (board, state) { const $wrpPanel = $(`
`) // root class used to identify for saving .data("getState", () => tracker.getSaveableState()); const tracker = new TimeTrackerRoot(board, $wrpPanel); state = TimeTrackerUtil.getMigratedState(state); tracker.setStateFrom(state); tracker.render($wrpPanel); return $wrpPanel; } } class TimeTrackerUtil { static pGetUserWindBearing (def) { return InputUiUtil.pGetUserDirection({ title: "Wind Bearing (Direction)", default: def, stepButtons: ["N", "NE", "E", "SE", "S", "SW", "W", "NW"], }); } static revSlugToText (it) { return it.split("-").reverse().map(s => s.split("|").join("- ")).join(" ").toTitleCase(); } static getMigratedState (state) { if (!state?.state) return state; // region Migrate legacy sub-objects ["days", "months", "years", "eras", "moons", "seasons"] .forEach(prop => { if (!state.state[prop]) return; if (state.state[prop] instanceof Array) return; if (typeof state.state[prop] !== "object") return; state.state[prop] = Object.values(state.state[prop]) .map(({id, ...rest}) => ({id, data: rest})); }); // endregion return state; } } class TimeTrackerComponent extends BaseComponent { /** * @param board DM Screen board. * @param $wrpPanel Panel wrapper element for us to populate. * @param [opts] Options object. * @param [opts.isTemporary] If this object should not save state to the board. */ constructor (board, $wrpPanel, opts) { super(); opts = opts || {}; this._board = board; this._$wrpPanel = $wrpPanel; if (!opts.isTemporary) this._addHookAll("state", () => this._board.doSaveStateDebounced()); } getPod () { const out = super.getPod(); out.triggerMapUpdate = (prop) => this._triggerMapUpdate(prop); return out; } /** * Trigger an update for a collection, auto-filtering deleted entries. The collection stored * at the prop should be a map of `id:state`. * @param prop The state property. */ _triggerMapUpdate (prop) { this._state[prop] = Object.values(this._state[prop]) .filter(it => !it.isDeleted) .mergeMap(it => ({[it.id]: it})); } } class TimeTrackerBase extends TimeTrackerComponent { /** * @param [opts] Options object. * @param [opts.isBase] True to forcibly use base time, false to let the component decide. * @returns {object} */ _getTimeInfo (opts) { opts = opts || {}; let numSecs; // Discard millis if (opts.numSecs != null) numSecs = opts.numSecs; else if (!opts.isBase && this._state.isBrowseMode && this._state.browseTime != null) numSecs = Math.round(this._state.browseTime / 1000); else numSecs = Math.round(this._state.time / 1000); numSecs = Math.max(0, numSecs); const secsPerMinute = this._state.secondsPerMinute; const secsPerHour = secsPerMinute * this._state.minutesPerHour; const secsPerDay = secsPerHour * this._state.hoursPerDay; const numDays = Math.floor(numSecs / secsPerDay); numSecs = numSecs - (numDays * secsPerDay); const numHours = Math.floor(numSecs / secsPerHour); numSecs = numSecs - (numHours * secsPerHour); const numMinutes = Math.floor(numSecs / secsPerMinute); numSecs = numSecs - (numMinutes * secsPerMinute); const dayInfos = this._state.days .map(it => it.data); const monthInfos = this._state.months .map(it => it.data); const seasonInfos = this._state.seasons .map(it => it.data) .sort((a, b) => SortUtil.ascSort(a.startDay, b.startDay)); const yearInfos = this._state.years .map(it => it.data) .sort((a, b) => SortUtil.ascSort(a.year, b.year)); const eraInfos = this._state.eras .map(it => it.data) .sort((a, b) => SortUtil.ascSort(a.startYear, b.startYear)); const secsPerYear = secsPerDay * monthInfos.map(it => it.days).reduce((a, b) => a + b, 0); const daysPerWeek = dayInfos.length; const secsPerWeek = secsPerDay * daysPerWeek; const dayOfWeek = numDays % daysPerWeek; const daysPerYear = monthInfos.map(it => it.days).reduce((a, b) => a + b, 0); const dayOfYear = numDays % daysPerYear; const out = { // handy stats secsPerMinute, minutesPerHour: this._state.minutesPerHour, hoursPerDay: this._state.hoursPerDay, secsPerHour, secsPerDay, secsPerWeek, secsPerYear, daysPerWeek, daysPerYear, monthsPerYear: monthInfos.length, // clock numSecs, numMinutes, numHours, numDays, timeOfDaySecs: numSecs + (numMinutes * secsPerMinute) + (numHours * secsPerHour), // calendar date: 0, // current day in month, i.e. 0-30 for a 31-day month month: 0, // current month in year, i.e. 0-11 for a 12-month year year: 0, dayOfWeek, dayOfYear, monthStartDay: 0, // day the current month starts on, i.e. 0-6 for a 7-day week; e.g. if the first day of the current month is a Wednesday, this will be set to 2 monthInfo: {...monthInfos[0]}, prevMonthInfo: {...monthInfos.last()}, nextMonthInfo: {...(monthInfos[1] || monthInfos[0])}, dayInfo: {...dayInfos[dayOfWeek]}, monthStartDayOfYear: 0, // day in the current year that the current month starts on, e.g. "31" for the first day of February, or "58" for the first day of March weekOfYear: 0, seasonInfos: [], yearInfos: [], eraInfos: [], }; let tmpDays = numDays; outer: while (tmpDays > 0) { for (let i = 0; i < monthInfos.length; ++i) { const m = monthInfos[i]; for (let j = 0; j < m.days; ++j, --tmpDays) { if (tmpDays === 0) { out.date = j; out.month = i; out.monthInfo = {...m}; if (i > 0) out.prevMonthInfo = monthInfos[i - 1]; if (i < monthInfos.length - 1) out.nextMonthInfo = monthInfos[i + 1]; else out.nextMonthInfo = monthInfos[0]; break outer; } } out.monthStartDayOfYear += m.days; } out.year++; out.monthStartDayOfYear = out.monthStartDayOfYear % daysPerYear; } out.monthStartDay = (numDays - out.date) % daysPerWeek; if (seasonInfos.length) out.seasonInfos = seasonInfos.filter(it => dayOfYear >= it.startDay && dayOfYear <= it.endDay); // offsets out.year += this._state.offsetYears; out.monthStartDay += this._state.offsetMonthStartDay; out.monthStartDay %= daysPerWeek; out.dayInfo = dayInfos[(dayOfWeek + this._state.offsetMonthStartDay) % daysPerWeek]; // track the current week of the year, compensated for offsets out.weekOfYear = (out.year * (daysPerYear % daysPerWeek)) % daysPerWeek; // collect year/era info after offsets, so the user doesn't have to do math if (yearInfos.length) out.yearInfos = yearInfos.filter(it => out.year === it.year); if (eraInfos.length) { out.eraInfos = eraInfos.filter(it => out.year >= it.startYear && out.year <= it.endYear) .map(it => { const cpy = MiscUtil.copy(it); cpy.dayOfEra = out.year - cpy.startYear; return cpy; }); } if (opts.year != null || opts.dayOfYear != null) { const now = Math.round((this._state.isBrowseMode && this._state.browseTime != null ? this._state.browseTime : this._state.time) / 1000); const diffSecsYear = opts.year != null ? (out.year - opts.year) * secsPerYear : 0; const diffSecsDay = opts.dayOfYear != null ? (dayOfYear - opts.dayOfYear) * secsPerDay : 0; return this._getTimeInfo({numSecs: now - (diffSecsYear + diffSecsDay)}); } else return out; } _getEvents (year, dayOfYear) { return this._getEncountersEvents("events", year, dayOfYear); } _getEncounters (year, dayOfYear) { return this._getEncountersEvents("encounters", year, dayOfYear); } _getEncountersEvents (prop, year, dayOfYear) { return Object.values(this._state[prop]) .filter(it => !it.isDeleted) .filter(it => { if (it.when.year != null && it.when.day != null) { return it.when.year === year && it.when.day === dayOfYear; } // TODO consider expanding this in future // - will also require changes to the event creation/management UI // else if (it.when.weekday != null) {...} // else if (it.when.fortnightDay != null) {...} // ... etc }) .sort((a, b) => { if (a.hasTime && !b.hasTime) return 1; if (!a.hasTime && b.hasTime) return -1; if (a.hasTime && b.hasTime) return SortUtil.ascSort(a.timeOfDaySecs, b.timeOfDaySecs) || SortUtil.ascSort(a.pos, b.pos); return SortUtil.ascSort(a.pos, b.pos); }); } _getMoonInfos (numDays) { const moons = this._state.moons .map(it => it.data) .sort((a, b) => SortUtil.ascSort(a.phaseOffset, b.phaseOffset) || SortUtil.ascSort(a.name, b.name)); return moons.map(moon => { // this should be never occur if (moon.period <= 0) throw new Error(`Invalid moon period "${moon.period}", should be greater than zero!`); const offsetNumDays = numDays - moon.phaseOffset; let dayOfPeriod = offsetNumDays % moon.period; while (dayOfPeriod < 0) dayOfPeriod += moon.period; const ixPhase = Math.floor((dayOfPeriod / moon.period) * 8); const phaseNameSlug = TimeTrackerBase._MOON_PHASES[ixPhase === 8 ? 0 : ixPhase]; const phaseFirstDay = (Math.floor(((dayOfPeriod - 1) / moon.period) * 8) === ixPhase - 1); // going back a day would take us to the previous phase return { color: moon.color, name: moon.name, period: moon.period, phaseName: phaseNameSlug.split("-").map(it => it.uppercaseFirst()).join(" "), phaseFirstDay: phaseFirstDay, phaseIndex: ixPhase, dayOfPeriod, }; }); } _getAllDayInfos () { return this._state.days .map(it => it.data); } /** * @param deltaSecs Time modification, in seconds. * @param [opts] Options object. * @param [opts.isBase] True if the base time should be forcibly modified; false if the method should choose. */ _doModTime (deltaSecs, opts) { opts = opts || {}; const prop = !opts.isBase && this._state.isBrowseMode && this._state.browseTime != null ? "browseTime" : "time"; const oldTime = this._state[prop]; this._state[prop] = Math.max(0, oldTime + Math.round(deltaSecs * 1000)); } _getDefaultState () { return MiscUtil.copy(TimeTrackerBase._DEFAULT_STATE); } get _rendered () { return this.__rendered; } getPod () { const pod = super.getPod(); pod.getTimeInfo = this._getTimeInfo.bind(this); pod.getEvents = this._getEvents.bind(this); pod.getEncounters = this._getEncounters.bind(this); pod.getMoonInfos = this._getMoonInfos.bind(this); pod.doModTime = this._doModTime.bind(this); pod.getAllDayInfos = this._getAllDayInfos.bind(this); return pod; } static getGenericDay (i) { return { id: CryptUtil.uid(), data: { ...TimeTrackerBase._DEFAULT_STATE__DAY, name: `${Parser.numberToText(i + 1)}day`.uppercaseFirst(), }, }; } static getGenericMonth (i) { return { id: CryptUtil.uid(), data: { ...TimeTrackerBase._DEFAULT_STATE__MONTH, name: `${Parser.numberToText(i + 1)}uary`.uppercaseFirst(), days: 30, }, }; } static getGenericEvent (pos, year, eventDay, timeOfDaySecs) { const out = { ...MiscUtil.copy(TimeTrackerBase._DEFAULT_STATE__EVENT), id: CryptUtil.uid(), pos, }; if (year != null) out.when.year = year; if (eventDay != null) out.when.day = eventDay; if (timeOfDaySecs != null) { out.timeOfDaySecs = timeOfDaySecs; out.hasTime = true; } return out; } static getGenericEncounter (pos, year, encounterDay, timeOfDaySecs) { const out = { ...MiscUtil.copy(TimeTrackerBase._DEFAULT_STATE__ENCOUNTER), id: CryptUtil.uid(), pos, }; if (year != null) out.when.year = year; if (encounterDay != null) out.when.day = encounterDay; if (timeOfDaySecs != null) { out.timeOfDaySecs = timeOfDaySecs; out.hasTime = true; } return out; } static getGenericSeason (i) { return { id: CryptUtil.uid(), data: { ...TimeTrackerBase._DEFAULT_STATE__SEASON, name: `Season ${i + 1}`, startDay: i * 90, endDay: ((i + 1) * 90) - 1, }, }; } static getGenericYear (i) { return { id: CryptUtil.uid(), data: { ...TimeTrackerBase._DEFAULT_STATE__YEAR, name: `Year of the ${Parser.numberToText(i + 1).uppercaseFirst()}s`, year: i, }, }; } static getGenericEra (i) { const symbol = Parser.ALPHABET[i % Parser.ALPHABET.length]; return { id: CryptUtil.uid(), data: { ...TimeTrackerBase._DEFAULT_STATE__ERA, name: `${Parser.getOrdinalForm(i + 1)} Era`, abbreviation: `${symbol}E`, startYear: i, endYear: i, }, }; } static getGenericMoon (i) { return { id: CryptUtil.uid(), data: { ...TimeTrackerBase._DEFAULT_STATE__MOON, name: `Moon ${i + 1}`, }, }; } static formatDateInfo (dayInfo, date, monthInfo, seasonInfos) { return `${dayInfo.name || "[Nameless day]"} ${Parser.getOrdinalForm(date + 1)} ${monthInfo.name || "[Nameless month]"}${seasonInfos.length ? ` (${seasonInfos.map(it => it.name || "[Nameless season]").join("/")})` : ""}`; } static formatYearInfo (year, yearInfos, eraInfos, abbreviate) { return `Year ${year + 1}${yearInfos.length ? ` (${yearInfos.map(it => it.name.escapeQuotes()).join("/")})` : ""}${eraInfos.length ? `, ${eraInfos.map(it => `${it.dayOfEra + 1} ${(abbreviate ? it.abbreviation : it.name).escapeQuotes()}${abbreviate ? "" : ` (${it.abbreviation.escapeQuotes()})`}`).join("/")}` : ""}`; } static $getCvsMoon (moonInfo) { const $canvas = $(``); const c = $canvas[0]; const ctx = c.getContext("2d"); // draw image if (!TimerTrackerMoonSpriteLoader.hasError()) { ctx.drawImage( TimerTrackerMoonSpriteLoader.getImage(), moonInfo.phaseIndex * TimeTrackerBase._MOON_RENDER_RES, // source x 0, // source y TimeTrackerBase._MOON_RENDER_RES, // source w TimeTrackerBase._MOON_RENDER_RES, // source h 0, // dest x 0, // dest y TimeTrackerBase._MOON_RENDER_RES, // dest w TimeTrackerBase._MOON_RENDER_RES, // dest h ); } // overlay color ctx.globalCompositeOperation = "multiply"; ctx.fillStyle = moonInfo.color; ctx.rect(0, 0, TimeTrackerBase._MOON_RENDER_RES, TimeTrackerBase._MOON_RENDER_RES); ctx.fill(); ctx.closePath(); ctx.globalCompositeOperation = "source-over"; // draw border ctx.beginPath(); ctx.arc(TimeTrackerBase._MOON_RENDER_RES / 2, TimeTrackerBase._MOON_RENDER_RES / 2, TimeTrackerBase._MOON_RENDER_RES / 2, 0, 2 * Math.PI); ctx.lineWidth = 6; ctx.stroke(); ctx.closePath(); return $canvas; } static getClockInputs (timeInfo, vals, fnOnChange) { const getIptNum = ($ipt) => { return Number($ipt.val().trim().replace(/^0+/g, "")); }; let lastTimeSecs = vals.timeOfDaySecs; const doUpdateTime = () => { const curTimeSecs = metas .map(it => getIptNum(it.$ipt) * it.mult) .reduce((a, b) => a + b, 0); if (lastTimeSecs !== curTimeSecs) { lastTimeSecs = curTimeSecs; fnOnChange(curTimeSecs); } }; const metas = []; const $getIpt = (title, propMax, valProp, propMult) => { const $ipt = $(``) .change(() => { const maxVal = timeInfo[propMax] - 1; const nxtRaw = getIptNum($ipt); const nxtVal = Math.max(0, Math.min(maxVal, nxtRaw)); $ipt.val(TimeTrackerBase.getPaddedNum(nxtVal, timeInfo[propMax])); doUpdateTime(); }) .click(() => $ipt.select()) .val(TimeTrackerBase.getPaddedNum(vals[valProp], timeInfo[propMax])); return {$ipt, propMax, mult: propMult ? timeInfo[propMult] : 1}; }; const metaHours = $getIpt("Hours", "hoursPerDay", "hours", "secsPerHour"); const metaMinutes = $getIpt("Minutes", "minutesPerHour", "minutes", "secsPerMinute"); const metaSeconds = $getIpt("Seconds", "secsPerMinute", "seconds"); metas.push(metaHours, metaMinutes, metaSeconds); const out = {$iptHours: metaHours.$ipt, $iptMinutes: metaMinutes.$ipt, $iptSeconds: metaSeconds.$ipt}; doUpdateTime(); return out; } static getHoursMinutesSecondsFromSeconds (secsPerHour, secsPerMinute, numSecs) { const numHours = Math.floor(numSecs / secsPerHour); numSecs = numSecs - (numHours * secsPerHour); const numMinutes = Math.floor(numSecs / secsPerMinute); numSecs = numSecs - (numMinutes * secsPerMinute); return { seconds: numSecs, minutes: numMinutes, hours: numHours, }; } static getPaddedNum (num, max) { return `${num}`.padStart(`${max}`.length, "0"); } } TimeTrackerBase._DEFAULT_STATE__DAY = { name: "Day", }; TimeTrackerBase._DEFAULT_STATE__MONTH = { name: "Month", days: 30, }; TimeTrackerBase._DEFAULT_STATE__EVENT = { name: "Event", entries: [], when: { year: 0, day: 0, }, isDeleted: false, isHidden: false, }; TimeTrackerBase._DEFAULT_STATE__ENCOUNTER = { name: "Encounter", when: { year: 0, day: 0, }, isDeleted: false, countUses: 0, }; TimeTrackerBase._DEFAULT_STATE__SEASON = { name: "Season", startDay: 0, endDay: 0, sunriseHour: 6, sunsetHour: 22, }; TimeTrackerBase._DEFAULT_STATE__YEAR = { name: "Year", year: 0, }; TimeTrackerBase._DEFAULT_STATE__ERA = { name: "Era", abbreviation: "E", startYear: 0, endYear: 0, }; TimeTrackerBase._DEFAULT_STATE__MOON = { name: "Moon", color: "#ffffff", phaseOffset: 0, period: 24, }; TimeTrackerBase._DEFAULT_STATE = { time: 0, // Store these in the base class, even though they are only effectively useful in the subclass browseTime: null, isBrowseMode: false, // clock hoursPerDay: 24, minutesPerHour: 60, secondsPerMinute: 60, // game mechanics hoursPerLongRest: 8, minutesPerShortRest: 60, secondsPerRound: 6, // offsets offsetYears: 0, offsetMonthStartDay: 0, // calendar days: [...new Array(7)] .map((_, i) => TimeTrackerBase.getGenericDay(i)), months: [...new Array(12)] .map((_, i) => TimeTrackerBase.getGenericMonth(i)), events: {}, encounters: {}, seasons: [...new Array(4)] .map((_, i) => TimeTrackerBase.getGenericSeason(i)), years: [], eras: [], moons: [...new Array(1)] .map((_, i) => TimeTrackerBase.getGenericMoon(i)), }; TimeTrackerBase._MOON_PHASES = [ "new-moon", "waxing-crescent", "first-quarter", "waxing-gibbous", "full-moon", "waning-gibbous", "last-quarter", "waning-crescent", ]; TimeTrackerBase._MOON_RENDER_RES = 32; TimeTrackerBase._MIN_TIME = 1; TimeTrackerBase._MAX_TIME = 9999; class TimeTrackerRoot extends TimeTrackerBase { constructor (tracker, $wrpPanel) { super(tracker, $wrpPanel); // components this._compClock = new TimeTrackerRoot_Clock(tracker, $wrpPanel); this._compCalendar = new TimeTrackerRoot_Calendar(tracker, $wrpPanel); this._compSettings = new TimeTrackerRoot_Settings(tracker, $wrpPanel); } getSaveableState () { return { ...this.getBaseSaveableState(), compClockState: this._compClock.getSaveableState(), compCalendarState: this._compCalendar.getSaveableState(), compSettingsState: this._compSettings.getSaveableState(), }; } setStateFrom (toLoad) { this.setBaseSaveableStateFrom(toLoad); if (toLoad.compClockState) this._compClock.setStateFrom(toLoad.compClockState); if (toLoad.compCalendarState) this._compCalendar.setStateFrom(toLoad.compCalendarState); if (toLoad.compSettingsState) this._compSettings.setStateFrom(toLoad.compSettingsState); } render ($parent) { $parent.empty(); const $wrpClock = $(`
`); const $wrpCalendar = $(`
`); const $wrpSettings = $(`
`); const pod = this.getPod(); this._compClock.render($wrpClock, pod); this._compCalendar.render($wrpCalendar, pod); this._compSettings.render($wrpSettings, pod); const $btnShowClock = $(``) .click(() => this._state.tab = 0); const $btnShowCalendar = $(``) .click(() => this._state.tab = 1); const $btnShowSettings = $(``) .click(() => this._state.tab = 2); const hookShowTab = () => { $btnShowClock.toggleClass("active", this._state.tab === 0); $btnShowCalendar.toggleClass("active", this._state.tab === 1); $btnShowSettings.toggleClass("active", this._state.tab === 2); $wrpClock.toggleClass("hidden", this._state.tab !== 0); $wrpCalendar.toggleClass("hidden", this._state.tab !== 1); $wrpSettings.toggleClass("hidden", this._state.tab !== 2); }; this._addHookBase("tab", hookShowTab); hookShowTab(); const $btnReset = $(``) .click(() => confirm("Are you sure?") && Object.assign(this._state, {time: 0, isBrowseMode: false, browseTime: null})); $$`
${$btnShowClock}${$btnShowCalendar}${$btnShowSettings}${$btnReset}

${$wrpClock} ${$wrpCalendar} ${$wrpSettings}
`.appendTo($parent); // Prevent events and encounters from being lost on month changes (i.e. reduced number of days in the year) const _hookSettingsMonths_handleProp = (daysPerYear, prop) => { let isMod = false; Object.values(this._state[prop]).forEach(it => { if (it.when.year != null && it.when.day != null) { if (it.when.day >= daysPerYear) { it.when.day = daysPerYear - 1; isMod = true; } } }); if (isMod) this._triggerMapUpdate(prop); }; const hookSettingsMonths = () => { const {daysPerYear} = this._getTimeInfo({isBase: true}); _hookSettingsMonths_handleProp(daysPerYear, "events"); _hookSettingsMonths_handleProp(daysPerYear, "encounters"); }; this._addHookBase("months", hookSettingsMonths); hookSettingsMonths(); // Prevent event/encounter times from exceeding day bounds on clock setting changes const _hookSettingsClock_handleProp = (secsPerDay, prop) => { let isMod = false; Object.values(this._state[prop]).forEach(it => { if (it.timeOfDaySecs != null) { if (it.timeOfDaySecs >= secsPerDay) { it.timeOfDaySecs = secsPerDay - 1; isMod = true; } } }); if (isMod) this._triggerMapUpdate(prop); }; const hookSettingsClock = () => { const {secsPerDay} = this._getTimeInfo({isBase: true}); _hookSettingsClock_handleProp(secsPerDay, "events"); _hookSettingsClock_handleProp(secsPerDay, "encounters"); }; this._addHookBase("secondsPerMinute", hookSettingsClock); this._addHookBase("minutesPerHour", hookSettingsClock); this._addHookBase("hoursPerDay", hookSettingsClock); hookSettingsClock(); } _getDefaultState () { return { ...MiscUtil.copy(super._getDefaultState()), ...MiscUtil.copy(TimeTrackerRoot._DEFAULT_STATE), }; } } TimeTrackerRoot._DEFAULT_STATE = { tab: 0, isPaused: false, isAutoPaused: false, hasCalendarLabelsColumns: true, hasCalendarLabelsRows: false, unitsWindSpeed: "mph", isClockSectionHidden: false, isCalendarSectionHidden: false, isMechanicsSectionHidden: false, isOffsetsSectionHidden: false, isDaysSectionHidden: false, isMonthsSectionHidden: false, isSeasonsSectionHidden: false, isYearsSectionHidden: false, isErasSectionHidden: false, isMoonsSectionHidden: false, }; class TimeTrackerRoot_Clock extends TimeTrackerComponent { constructor (board, $wrpPanel) { super(board, $wrpPanel); this._compWeather = new TimeTrackerRoot_Clock_Weather(board, $wrpPanel); this._ivTimer = null; } getSaveableState () { return { ...this.getBaseSaveableState(), compWeatherState: this._compWeather.getSaveableState(), }; } setStateFrom (toLoad) { this.setBaseSaveableStateFrom(toLoad); if (toLoad.compWeatherState) this._compWeather.setStateFrom(toLoad.compWeatherState); } render ($parent, parent) { $parent.empty(); this._parent = parent; const {getTimeInfo, getMoonInfos, doModTime, getEvents, getEncounters} = parent; clearInterval(this._ivTimer); let time = Date.now(); this._ivTimer = setInterval(() => { const timeNext = Date.now(); const timeDelta = timeNext - time; time = timeNext; if (this._parent.get("isPaused") || this._parent.get("isAutoPaused")) return; this._parent.set("time", this._parent.get("time") + timeDelta); }, 1000); this._$wrpPanel.data("onDestroy", () => clearInterval(this._ivTimer)); const $dispReadableDate = $(`
`); const $dispReadableYear = $(`
`); const $wrpMoons = $(`
`); const $wrpDayNight = $(`
`); const getSecsToNextDay = (timeInfo) => { const { secsPerMinute, secsPerHour, secsPerDay, numSecs, numMinutes, numHours, } = timeInfo; return secsPerDay - ( numHours * secsPerHour + numMinutes * secsPerMinute + numSecs ); }; const $btnNextSunrise = $(``) .click(() => { const timeInfo = getTimeInfo({isBase: true}); const { seasonInfos, numHours, numMinutes, numSecs, secsPerHour, secsPerMinute, } = timeInfo; const sunriseHour = seasonInfos[0].sunriseHour; if (sunriseHour > this._parent.get("hoursPerDay")) { return JqueryUtil.doToast({content: "Could not skip to next sunrise\u2014sunrise time is greater than the number of hours in a day!", type: "warning"}); } if (numHours < sunriseHour) { // skip to sunrise later today const targetSecs = sunriseHour * secsPerHour; const currentSecs = (secsPerHour * numHours) + (secsPerMinute * numMinutes) + numSecs; const toAdvance = targetSecs - currentSecs; doModTime(toAdvance, {isBase: true}); } else { // skip to sunrise the next day const toNextDay = getSecsToNextDay(timeInfo); const toAdvance = toNextDay + (secsPerHour * sunriseHour); doModTime(toAdvance, {isBase: true}); } }); const $btnNextDay = $(``) .click(() => doModTime(getSecsToNextDay(getTimeInfo({isBase: true})), {isBase: true})); const $getIpt = (propMax, timeProp, multProp) => { const $ipt = $(``) .change(() => { const timeInfo = getTimeInfo({isBase: true}); const multiplier = (multProp ? timeInfo[multProp] : 1); const curSecs = timeInfo[timeProp] * multiplier; const nxtRaw = Number($ipt.val().trim().replace(/^0+/g, "")); const nxtSecs = (isNaN(nxtRaw) ? 0 : nxtRaw) * multiplier; doModTime(nxtSecs - curSecs, {isBase: true}); }) .click(() => $ipt.select()) .focus(() => this._parent.set("isAutoPaused", true)) .blur(() => this._parent.set("isAutoPaused", false)); const hookDisplay = () => { const maxDigits = `${this._parent.get(propMax)}`.length; $ipt.css("width", 20 * maxDigits); }; this._parent.addHook(propMax, hookDisplay); hookDisplay(); return $ipt; }; const doUpdate$Ipt = ($ipt, propMax, num) => { if ($ipt.is(":focus")) return; // freeze selected inputs $ipt.val(TimeTrackerBase.getPaddedNum(num, this._parent.get(propMax))); }; const $iptHours = $getIpt("hoursPerDay", "numHours", "secsPerHour"); const $iptMinutes = $getIpt("minutesPerHour", "numMinutes", "secsPerMinute"); const $iptSeconds = $getIpt("secondsPerMinute", "numSecs"); const $wrpDays = $(`
`); const $wrpHours = $$`
${$iptHours}
`; const $wrpMinutes = $$`
${$iptMinutes}
`; const $wrpSeconds = $$`
${$iptSeconds}
`; const $wrpEventsEncounters = $(`
`); const $hrEventsEncounters = $(`
`); // cache rendering let lastReadableDate = null; let lastReadableYearHtml = null; let lastDay = null; let lastMoonInfo = null; let lastDayNightHtml = null; let lastEvents = null; let lastEncounters = null; const hookClock = () => { const { numDays, numHours, numMinutes, numSecs, dayInfo, date, monthInfo, seasonInfos, year, yearInfos, eraInfos, dayOfYear, secsPerHour, secsPerMinute, minutesPerHour, hoursPerDay, } = getTimeInfo({isBase: true}); const todayMoonInfos = getMoonInfos(numDays); if (!CollectionUtil.deepEquals(lastMoonInfo, todayMoonInfos)) { lastMoonInfo = todayMoonInfos; $wrpMoons.empty(); if (!todayMoonInfos.length) { $wrpMoons.hide(); } else { $wrpMoons.show(); todayMoonInfos.forEach(moon => { $$`
${TimeTrackerBase.$getCvsMoon(moon).addClass("mr-2").addClass("dm-time__clock-moon-phase").title(null)}
${moon.name}
${moon.phaseName}(Day ${moon.dayOfPeriod + 1}/${moon.period})
`.appendTo($wrpMoons); }); } } const readableDate = TimeTrackerBase.formatDateInfo(dayInfo, date, monthInfo, seasonInfos); if (readableDate !== lastReadableDate) { lastReadableDate = readableDate; $dispReadableDate.text(readableDate); } const readableYear = TimeTrackerBase.formatYearInfo(year, yearInfos, eraInfos, true); if (readableYear !== lastReadableYearHtml) { lastReadableYearHtml = readableYear; $dispReadableYear.html(readableYear); } if (lastDay !== numDays) { lastDay = numDays; $wrpDays.text(`Day ${numDays + 1}`); } doUpdate$Ipt($iptHours, "hoursPerDay", numHours); doUpdate$Ipt($iptMinutes, "minutesPerHour", numMinutes); doUpdate$Ipt($iptSeconds, "secondsPerMinute", numSecs); if (seasonInfos.length) { $wrpDayNight.removeClass("hidden"); const dayNightHtml = seasonInfos.map(it => { const isDay = numHours >= it.sunriseHour && numHours < it.sunsetHour; const hoursToDayNight = isDay ? it.sunsetHour - numHours : numHours < it.sunriseHour ? it.sunriseHour - numHours : (this._parent.get("hoursPerDay") + it.sunriseHour) - numHours; return `${isDay ? "Day" : "Night"} (${hoursToDayNight === 1 ? `Less than 1 hour` : `More than ${hoursToDayNight - 1} hour${hoursToDayNight === 2 ? "" : "s"}`} to sun${isDay ? "set" : "rise"})`; }).join("/"); if (dayNightHtml !== lastDayNightHtml) { $wrpDayNight.html(dayNightHtml); lastDayNightHtml = dayNightHtml; } $btnNextSunrise.removeClass("hidden"); } else { $wrpDayNight.addClass("hidden"); $btnNextSunrise.addClass("hidden"); } const todayEvents = MiscUtil.copy(getEvents(year, dayOfYear)); const todayEncounters = MiscUtil.copy(getEncounters(year, dayOfYear)); if (!CollectionUtil.deepEquals(lastEvents, todayEvents) || !CollectionUtil.deepEquals(lastEncounters, todayEncounters)) { lastEvents = todayEvents; lastEncounters = todayEncounters; $wrpEventsEncounters.empty(); if (!lastEvents.length && !lastEncounters.length) { $hrEventsEncounters.hide(); $wrpEventsEncounters.hide(); } else { $hrEventsEncounters.show(); $wrpEventsEncounters.show(); todayEvents.forEach(event => { const hoverMeta = Renderer.hover.getMakePredefinedHover({type: "entries", entries: []}, {isBookContent: true}); const doUpdateMeta = () => { let name = event.name; if (event.hasTime) { const {hours, minutes, seconds} = TimeTrackerBase.getHoursMinutesSecondsFromSeconds(secsPerHour, secsPerMinute, event.timeOfDaySecs); name = `${name} at ${TimeTrackerBase.getPaddedNum(hours, hoursPerDay)}:${TimeTrackerBase.getPaddedNum(minutes, minutesPerHour)}:${TimeTrackerBase.getPaddedNum(seconds, secsPerMinute)}`; } const toShow = { name, type: "entries", entries: event.entries, data: {hoverTitle: name}, }; Renderer.hover.updatePredefinedHover(hoverMeta.id, toShow); }; const $dispEvent = $$`
*
` .mouseover(evt => { doUpdateMeta(); hoverMeta.mouseOver(evt, $dispEvent[0]); }) .mousemove(evt => hoverMeta.mouseMove(evt, $dispEvent[0])) .mouseleave(evt => hoverMeta.mouseLeave(evt, $dispEvent[0])) .click(() => { const comp = TimeTrackerRoot_Settings_Event.getInstance(this._board, this._$wrpPanel, this._parent, event); comp.doOpenEditModal(null); }) .appendTo($wrpEventsEncounters); }); todayEncounters.forEach(encounter => { const hoverMeta = Renderer.hover.getMakePredefinedHover({type: "entries", entries: []}, {isBookContent: true}); const pDoUpdateMeta = async () => { let name = encounter.displayName != null ? encounter.displayName : (encounter.name || "(Unnamed Encounter)"); if (encounter.hasTime) { const {hours, minutes, seconds} = TimeTrackerBase.getHoursMinutesSecondsFromSeconds(secsPerHour, secsPerMinute, encounter.timeOfDaySecs); name = `${name} at ${TimeTrackerBase.getPaddedNum(hours, hoursPerDay)}:${TimeTrackerBase.getPaddedNum(minutes, minutesPerHour)}:${TimeTrackerBase.getPaddedNum(seconds, secsPerMinute)}`; } const entityInfos = await ListUtil.pGetSublistEntities_fromHover({ exportedSublist: encounter.data, page: UrlUtil.PG_BESTIARY, }); const toShow = { name, type: "entries", entries: [ { type: "list", items: entityInfos.map(it => { return `${it.count || 1}× ${Renderer.hover.getEntityLink(it.entity)}`; }), }, ], data: {hoverTitle: name}, }; Renderer.hover.updatePredefinedHover(hoverMeta.id, toShow); }; const $dispEncounter = $$`
*
` .mouseover(async evt => { await pDoUpdateMeta(); hoverMeta.mouseOver(evt, $dispEncounter[0]); }) .mousemove(evt => hoverMeta.mouseMove(evt, $dispEncounter[0])) .mouseleave(evt => hoverMeta.mouseLeave(evt, $dispEncounter[0])) .click(async () => { const liveEncounter = this._parent.get("encounters")[encounter.id]; if (encounter.countUses) { liveEncounter.countUses = 0; this._parent.triggerMapUpdate("encounters"); } else { await TimeTrackerRoot_Calendar.pDoRunEncounter(this._parent, liveEncounter); } }) .appendTo($wrpEventsEncounters); }); } } }; this._parent.addHook("time", hookClock); // clock settings this._parent.addHook("offsetYears", hookClock); this._parent.addHook("offsetMonthStartDay", hookClock); this._parent.addHook("hoursPerDay", hookClock); this._parent.addHook("minutesPerHour", hookClock); this._parent.addHook("secondsPerMinute", hookClock); // calendar periods this._parent.addHook("days", hookClock); this._parent.addHook("months", hookClock); this._parent.addHook("seasons", hookClock); // special this._parent.addHook("events", hookClock); this._parent.addHook("encounters", hookClock); this._parent.addHook("moons", hookClock); hookClock(); const $btnSubDay = $(``) .click(evt => doModTime(-1 * this._parent.get("hoursPerDay") * this._parent.get("minutesPerHour") * this._parent.get("secondsPerMinute") * (evt.shiftKey ? 5 : 1), {isBase: true})); const $btnAddDay = $(``) .click(evt => doModTime(this._parent.get("hoursPerDay") * this._parent.get("minutesPerHour") * this._parent.get("secondsPerMinute") * (evt.shiftKey ? 5 : 1), {isBase: true})); const $btnAddHour = $(``) .click(evt => doModTime(this._parent.get("minutesPerHour") * this._parent.get("secondsPerMinute") * (evt.shiftKey ? 5 : (EventUtil.isCtrlMetaKey(evt) ? 12 : 1)), {isBase: true})); const $btnSubHour = $(``) .click(evt => doModTime(-1 * this._parent.get("minutesPerHour") * this._parent.get("secondsPerMinute") * (evt.shiftKey ? 5 : (EventUtil.isCtrlMetaKey(evt) ? 12 : 1)), {isBase: true})); const $btnAddMinute = $(``) .click(evt => doModTime(this._parent.get("secondsPerMinute") * (evt.shiftKey && (EventUtil.isCtrlMetaKey(evt)) ? 30 : (EventUtil.isCtrlMetaKey(evt) ? 15 : (evt.shiftKey ? 5 : 1))), {isBase: true})); const $btnSubMinute = $(``) .click(evt => doModTime(-1 * this._parent.get("secondsPerMinute") * (evt.shiftKey && (EventUtil.isCtrlMetaKey(evt)) ? 30 : (EventUtil.isCtrlMetaKey(evt) ? 15 : (evt.shiftKey ? 5 : 1))), {isBase: true})); const $btnAddSecond = $(``) .click(evt => doModTime((evt.shiftKey && (EventUtil.isCtrlMetaKey(evt)) ? 30 : (EventUtil.isCtrlMetaKey(evt) ? 15 : (evt.shiftKey ? 5 : 1))), {isBase: true})); const $btnSubSecond = $(``) .click(evt => doModTime(-1 * (evt.shiftKey && (EventUtil.isCtrlMetaKey(evt)) ? 30 : (EventUtil.isCtrlMetaKey(evt) ? 15 : (evt.shiftKey ? 5 : 1))), {isBase: true})); const $btnIsPaused = $(``) .click(() => this._parent.set("isPaused", !this._parent.get("isPaused"))); const hookPaused = () => $btnIsPaused.toggleClass("active", this._parent.get("isPaused") || this._parent.get("isAutoPaused")); this._parent.addHook("isPaused", hookPaused); this._parent.addHook("isAutoPaused", hookPaused); hookPaused(); const $btnAddLongRest = $(``) .click(evt => doModTime((evt.shiftKey ? -1 : 1) * this._parent.get("hoursPerLongRest") * this._parent.get("minutesPerHour") * this._parent.get("secondsPerMinute"), {isBase: true})); const $btnAddShortRest = $(``) .click(evt => doModTime((evt.shiftKey ? -1 : 1) * this._parent.get("minutesPerShortRest") * this._parent.get("secondsPerMinute"), {isBase: true})); const $btnAddTurn = $(``) .click(evt => doModTime((evt.shiftKey ? -1 : 1) * this._parent.get("secondsPerRound"), {isBase: true})); const $wrpWeather = $(`
`); this._compWeather.render($wrpWeather, this._parent); $$`
${$dispReadableDate} ${$dispReadableYear} ${$wrpMoons}
${$btnAddHour}
${$wrpHours}
${$btnSubHour}
:
${$btnAddMinute}
${$wrpMinutes}
${$btnSubMinute}
:
${$btnAddSecond}
${$wrpSeconds}
${$btnSubSecond}
${$btnIsPaused}
${$wrpDayNight}
${$btnAddLongRest}${$btnAddShortRest}
${$btnAddTurn}
${$btnNextSunrise} ${$btnNextDay}
${$wrpDays}
${$btnSubDay}${$btnAddDay}

${$wrpEventsEncounters} ${$hrEventsEncounters} ${$wrpWeather}
`.appendTo($parent); } } class TimeTrackerRoot_Clock_Weather extends TimeTrackerComponent { render ($parent, parent) { $parent.empty(); this._parent = parent; const {getTimeInfo} = parent; const $btnRandomise = $(``) .click(async evt => { const randomState = await TimeTrackerRoot_Clock_RandomWeather.pGetUserInput( { temperature: this._state.temperature, precipitation: this._state.precipitation, windDirection: this._state.windDirection, windSpeed: this._state.windSpeed, }, { unitsWindSpeed: this._parent.get("unitsWindSpeed"), isReroll: evt.shiftKey, }, ); if (randomState == null) return; Object.assign(this._state, randomState); }); const $btnTemperature = $(``) .click(() => { if (!this._state.allowedTemperatures.length || !this._state.allowedPrecipitations.length || !this._state.allowedWindSpeeds.length) { JqueryUtil.doToast({content: `Please select allowed values for all sections!`, type: "warning"}); } else doClose(true); }); $$`
Allowed Temperatures
${$btnsTemperature}
Allowed Precipitation Types
${$btnsPrecipitation}
Prevailing Wind Direction
${$btnWindDirection}
Allowed Wind Speeds
${$btnsWindSpeed}
${$btnOk}
`.appendTo($modalInner); } _getDefaultState () { return MiscUtil.copy(TimeTrackerRoot_Clock_RandomWeather._DEFAULT_STATE); } /** * @param curWeather The current weather state. * @param opts Options object. * @param opts.unitsWindSpeed Wind speed units. * @param [opts.isReroll] If the weather is being quick-rerolled. */ static async pGetUserInput (curWeather, opts) { opts = opts || {}; const comp = new TimeTrackerRoot_Clock_RandomWeather(opts); const prevState = await StorageUtil.pGetForPage(TimeTrackerRoot_Clock_RandomWeather._STORAGE_KEY); if (prevState) comp.setStateFrom(prevState); const getWeather = () => { StorageUtil.pSetForPage(TimeTrackerRoot_Clock_RandomWeather._STORAGE_KEY, comp.getSaveableState()); const inputs = comp.toObject(); // 66% chance of temperature change const isNewTemp = RollerUtil.randomise(3) > 1; // 80% chance of precipitation change const isNewPrecipitation = RollerUtil.randomise(5) > 1; // 40% chance of prevailing wind; 20% chance of current wind; 40% chance of random wind const rollWindDirection = RollerUtil.randomise(5); let windDirection; if (rollWindDirection === 1) windDirection = curWeather.windDirection; else if (rollWindDirection <= 3) windDirection = inputs.prevailingWindDirection; else windDirection = RollerUtil.randomise(360) - 1; windDirection += TimeTrackerRoot_Clock_RandomWeather._getBearingFudge(); // 2/7 chance wind speed stays the same; 1/7 chance each of it increasing/decreasing by 1/2/3 steps const rollWindSpeed = RollerUtil.randomise(7); let windSpeed; const ixCurWindSpeed = TimeTrackerRoot_Clock_Weather._WIND_SPEEDS.indexOf(curWeather.windSpeed); let windSpeedOffset = 0; if (rollWindSpeed <= 3) windSpeedOffset = -rollWindSpeed; else if (rollWindSpeed >= 5) windSpeedOffset = rollWindSpeed - 4; if (windSpeedOffset < 0) { windSpeed = TimeTrackerRoot_Clock_Weather._WIND_SPEEDS[ixCurWindSpeed + windSpeedOffset]; let i = -1; while (!inputs.allowedWindSpeeds.includes(windSpeed)) { windSpeed = TimeTrackerRoot_Clock_Weather._WIND_SPEEDS[ixCurWindSpeed + windSpeedOffset + i]; // If we run out of possibilities, scan the opposite direction if (--i < 0) { windSpeed = TimeTrackerRoot_Clock_Weather._WIND_SPEEDS.find(it => inputs.allowedWindSpeeds.includes(it)); } } } else if (windSpeedOffset > 0) { windSpeed = TimeTrackerRoot_Clock_Weather._WIND_SPEEDS[ixCurWindSpeed + windSpeedOffset]; let i = 1; while (!inputs.allowedWindSpeeds.includes(windSpeed)) { windSpeed = TimeTrackerRoot_Clock_Weather._WIND_SPEEDS[ixCurWindSpeed + windSpeedOffset + i]; // If we run out of possibilities, scan the opposite direction if (++i >= TimeTrackerRoot_Clock_Weather._WIND_SPEEDS.length) { windSpeed = [...TimeTrackerRoot_Clock_Weather._WIND_SPEEDS] .reverse() .find(it => inputs.allowedWindSpeeds.includes(it)); } } } else windSpeed = curWeather.windSpeed; return { temperature: isNewTemp ? RollerUtil.rollOnArray(inputs.allowedTemperatures) : curWeather.temperature, precipitation: isNewPrecipitation ? RollerUtil.rollOnArray(inputs.allowedPrecipitations) : curWeather.precipitation, windDirection, windSpeed, }; }; if (opts.isReroll) return getWeather(); return new Promise(resolve => { const {$modalInner, doClose} = UiUtil.getShowModal({ title: "Random Weather Configuration", isUncappedHeight: true, cbClose: (isDataEntered) => { if (!isDataEntered) resolve(null); else resolve(getWeather()); }, }); comp.render($modalInner, doClose); }); } static _getBearingFudge () { return Math.round(RollerUtil.randomise(20, 0)) * (RollerUtil.randomise(2) === 2 ? 1 : -1); } } TimeTrackerRoot_Clock_RandomWeather._DEFAULT_STATE = { allowedTemperatures: [...TimeTrackerRoot_Clock_Weather._TEMPERATURES], allowedPrecipitations: [...TimeTrackerRoot_Clock_Weather._PRECIPICATION], prevailingWindDirection: 0, allowedWindSpeeds: [...TimeTrackerRoot_Clock_Weather._WIND_SPEEDS], }; TimeTrackerRoot_Clock_RandomWeather._STORAGE_KEY = "TimeTracker_RandomWeatherModal"; class TimeTrackerRoot_Calendar extends TimeTrackerComponent { constructor (tracker, $wrpPanel) { super(tracker, $wrpPanel); // temp components this._tmpComps = []; } render ($parent, parent) { $parent.empty(); this._parent = parent; const {getTimeInfo, doModTime} = parent; // cache info to avoid re-rendering the calendar every second let lastRenderMeta = null; const $dispDayReadableDate = $(`
`); const $dispYear = $(`
`); const {$wrpDateControls, $iptYear, $iptMonth, $iptDay} = TimeTrackerRoot_Calendar.getDateControls(this._parent); const $btnBrowseMode = ComponentUiUtil.$getBtnBool( this._parent.component, "isBrowseMode", { $ele: $(``), fnHookPost: val => { if (val) this._parent.set("browseTime", this._parent.get("time")); else this._parent.set("browseTime", null); }, }, ); const $wrpCalendar = $(`
`); const hookCalendar = (prop) => { const timeInfo = getTimeInfo(); const { date, month, year, monthInfo, monthStartDay, daysPerWeek, dayInfo, monthStartDayOfYear, seasonInfos, numDays, yearInfos, eraInfos, secsPerDay, } = timeInfo; const renderMeta = { date, month, year, monthInfo, monthStartDay, daysPerWeek, dayInfo, monthStartDayOfYear, seasonInfos, numDays, yearInfos, eraInfos, secsPerDay, }; if (prop === "time" && CollectionUtil.deepEquals(lastRenderMeta, renderMeta)) return; lastRenderMeta = renderMeta; $dispDayReadableDate.text(TimeTrackerBase.formatDateInfo(dayInfo, date, monthInfo, seasonInfos)); $dispYear.html(TimeTrackerBase.formatYearInfo(year, yearInfos, eraInfos)); $iptYear.val(year + 1); $iptMonth.val(month + 1); $iptDay.val(date + 1); TimeTrackerRoot_Calendar.renderCalendar( this._parent, $wrpCalendar, timeInfo, (evt, eventYear, eventDay, moonDay) => { if (evt.shiftKey) this._render_doJumpToDay(eventYear, eventDay); else this._render_openDayModal(eventYear, eventDay, moonDay); }, { hasColumnLabels: this._parent.get("hasCalendarLabelsColumns"), hasRowLabels: this._parent.get("hasCalendarLabelsRows"), }, ); }; this._parent.addHook("time", hookCalendar); this._parent.addHook("browseTime", hookCalendar); this._parent.addHook("hasCalendarLabelsColumns", hookCalendar); this._parent.addHook("hasCalendarLabelsRows", hookCalendar); this._parent.addHook("months", hookCalendar); this._parent.addHook("events", hookCalendar); this._parent.addHook("encounters", hookCalendar); this._parent.addHook("moons", hookCalendar); hookCalendar(); $$`
${$dispDayReadableDate}
${$dispYear} ${$btnBrowseMode}
${$wrpDateControls}
${$wrpCalendar}
`.appendTo($parent); } /** * * @param parent Parent pod. * @param [opts] Options object. * @param [opts.isHideDays] True if the day controls should be hidden. * @param [opts.isHideWeeks] True if the week controls should be hidden. * @returns {object} */ static getDateControls (parent, opts) { opts = opts || {}; const {doModTime, getTimeInfo} = parent; const $btnSubDay = opts.isHideDays ? null : $(``) .click(evt => doModTime(-1 * getTimeInfo().secsPerDay * (evt.shiftKey ? 5 : 1))); const $btnAddDay = opts.isHideDays ? null : $(``) .click(evt => doModTime(getTimeInfo().secsPerDay * (evt.shiftKey ? 5 : 1))); const $btnSubWeek = opts.isHideWeeks ? null : $(``) .click(evt => doModTime(-1 * getTimeInfo().secsPerWeek * (evt.shiftKey ? 5 : 1))); const $btnAddWeek = opts.isHideWeeks ? null : $(``) .click(evt => doModTime(getTimeInfo().secsPerWeek * (evt.shiftKey ? 5 : 1))); const doModMonths = (numMonths) => { const doAddMonth = () => { const { secsPerDay, monthInfo, nextMonthInfo, date, } = getTimeInfo(); const dateNextMonth = date > nextMonthInfo.days ? nextMonthInfo.days - 1 : date; const daysDiff = (monthInfo.days - date) + dateNextMonth; doModTime(daysDiff * secsPerDay); }; const doSubMonth = () => { const { secsPerDay, prevMonthInfo, date, } = getTimeInfo(); const datePrevMonth = date > prevMonthInfo.days ? prevMonthInfo.days - 1 : date; const daysDiff = -date - (prevMonthInfo.days - datePrevMonth); doModTime(daysDiff * secsPerDay); }; if (numMonths === 1) doAddMonth(); else if (numMonths === -1) doSubMonth(); else { if (numMonths === 0) return; const timeInfoBefore = getTimeInfo(); if (numMonths > 1) { [...new Array(numMonths)].forEach(() => doAddMonth()); } else { [...new Array(Math.abs(numMonths))].forEach(() => doSubMonth()); } const timeInfoAfter = getTimeInfo(); if (timeInfoBefore.date !== timeInfoAfter.date && timeInfoBefore.date < timeInfoAfter.monthInfo.days) { const daysDiff = timeInfoBefore.date - timeInfoAfter.date; doModTime(daysDiff * timeInfoAfter.secsPerDay); } } }; const $btnSubMonth = $(``) .click(evt => doModMonths(evt.shiftKey ? -5 : -1)); const $btnAddMonth = $(``) .click(evt => doModMonths(evt.shiftKey ? 5 : 1)); const $btnSubYear = $(``) .click(evt => doModTime(-1 * getTimeInfo().secsPerYear * (evt.shiftKey ? 5 : 1))); const $btnAddYear = $(``) .click(evt => doModTime(getTimeInfo().secsPerYear * (evt.shiftKey ? 5 : 1))); const $iptYear = $(``) .change(() => { const { secsPerYear, year, } = getTimeInfo(); const nxt = UiUtil.strToInt($iptYear.val(), 1) - 1; $iptYear.val(nxt + 1); const diffYears = nxt - year; doModTime(diffYears * secsPerYear); }); const $iptMonth = $(``) .change(() => { const { month, monthsPerYear, } = getTimeInfo(); const nxtRaw = UiUtil.strToInt($iptMonth.val(), 1) - 1; const nxt = Math.max(0, Math.min(monthsPerYear - 1, nxtRaw)); $iptMonth.val(nxt + 1); const diffMonths = nxt - month; doModMonths(diffMonths); }); const $iptDay = opts.isHideDays ? null : $(``) .change(() => { const { secsPerDay, date, monthInfo, } = getTimeInfo(); const nxtRaw = UiUtil.strToInt($iptDay.val(), 1) - 1; const nxt = Math.max(0, Math.min(monthInfo.days - 1, nxtRaw)); $iptDay.val(nxt + 1); const diffDays = nxt - date; doModTime(diffDays * secsPerDay); }); const $wrpDateControls = $$`
${$btnSubYear} ${$btnSubMonth} ${$btnSubWeek} ${$btnSubDay}
${$iptYear}
/
${$iptMonth} ${$iptDay ? `
/
` : ""} ${$iptDay}
${$btnAddDay} ${$btnAddWeek} ${$btnAddMonth} ${$btnAddYear}
`; return {$wrpDateControls, $iptYear, $iptMonth, $iptDay}; } /** * @param parent Parent pod. * @param $wrpCalendar Wrapper element. * @param timeInfo Time info to render. * @param fnClickDay Function run with args `year, eventDay, moonDay` when a day is clicked. * @param [opts] Options object. * @param [opts.isHideDay] True if the day should not be highlighted. * @param [opts.hasColumnLabels] True if the columns should be labelled with the day of the week. * @param [opts.hasRowLabels] True if the rows should be labelled with the week of the year. */ static renderCalendar (parent, $wrpCalendar, timeInfo, fnClickDay, opts) { opts = opts || {}; const {getEvents, getEncounters, getMoonInfos} = parent; const { date, year, monthInfo, monthStartDay, daysPerWeek, monthStartDayOfYear, numDays, } = timeInfo; $wrpCalendar.empty().css({display: "grid"}); const gridOffsetX = opts.hasRowLabels ? 1 : 0; const gridOffsetY = opts.hasColumnLabels ? 1 : 0; if (opts.hasColumnLabels) { const days = parent.getAllDayInfos(); days.forEach((it, i) => { $(`
${it.name.slice(0, 2)}
`) .css({ "grid-column-start": `${i + gridOffsetX + 1}`, "grid-column-end": `${i + gridOffsetX + 2}`, "grid-row-start": `1`, "grid-row-end": `2`, }) .appendTo($wrpCalendar); }); } const daysInMonth = monthInfo.days; const loopBound = daysInMonth + (daysPerWeek - 1 - monthStartDay); for (let i = (-monthStartDay); i < loopBound; ++i) { const xPos = Math.floor((i + monthStartDay) % daysPerWeek); const yPos = Math.floor((i + monthStartDay) / daysPerWeek); if (xPos === 0 && opts.hasRowLabels && i < daysInMonth) { const weekNum = Math.floor(monthStartDayOfYear / daysPerWeek) + yPos; $(`
${weekNum}
`) .css({ "grid-column-start": `${xPos + 1}`, "grid-column-end": `${xPos + 2}`, "grid-row-start": `${yPos + gridOffsetY + 1}`, "grid-row-end": `${yPos + gridOffsetY + 2}`, }) .appendTo($wrpCalendar); } let $ele; if (i < 0 || i >= daysInMonth) { $ele = $(`
`); } else { const eventDay = monthStartDayOfYear + i; const moonDay = numDays - (date - i); const moonInfos = getMoonInfos(moonDay); const events = getEvents(year, eventDay); const encounters = getEncounters(year, eventDay); const activeMoons = moonInfos.filter(it => it.phaseFirstDay); let moonPart; if (activeMoons.length) { const $renderedMoons = activeMoons.map((m, i) => { if (i === 0 || activeMoons.length < 3) { return TimeTrackerBase.$getCvsMoon(m).addClass("dm-time__calendar-moon-phase"); } else if (i === 1) { const otherMoons = activeMoons.length - 1; return `
`; } }); moonPart = $$`
${$renderedMoons}
`; } else moonPart = ""; $ele = $$`
${i + 1} ${events.length ? `
*
` : ""} ${encounters.length ? `
*
` : ""} ${moonPart}
`.click((evt) => fnClickDay(evt, year, eventDay, moonDay)); } $ele.css({ "grid-column-start": `${xPos + gridOffsetX + 1}`, "grid-column-end": `${xPos + gridOffsetX + 2}`, "grid-row-start": `${yPos + gridOffsetY + 1}`, "grid-row-end": `${yPos + gridOffsetY + 2}`, }); $wrpCalendar.append($ele); } } _render_doJumpToDay (eventYear, eventDay) { const {getTimeInfo, doModTime} = this._parent; // Calculate difference vs base time, and exit browse mode if we're in it const { year, dayOfYear, secsPerYear, secsPerDay, } = getTimeInfo({isBase: true}); const daySecs = (eventYear * secsPerYear) + (eventDay * secsPerDay); const currentSecs = (year * secsPerYear) + (dayOfYear * secsPerDay); const offset = daySecs - currentSecs; doModTime(offset, {isBase: true}); this._parent.set("isBrowseMode", false); } _render_openDayModal (eventYear, eventDay, moonDay) { const {getTimeInfo, getEvents, getEncounters, getMoonInfos} = this._parent; const $btnJumpToDay = $(``) .click(() => { this._render_doJumpToDay(eventYear, eventDay); doClose(); }); const $btnAddEvent = $(``) .click(() => { const nxtPos = Object.keys(this._parent.get("events")).length; const nuEvent = TimeTrackerBase.getGenericEvent(nxtPos, year, eventDay); this._eventToEdit = nuEvent.id; this._parent.set("events", {...this._parent.get("events"), [nuEvent.id]: nuEvent}); }); const $btnAddEventAtTime = $(``) .click(async evt => { const chosenTimeInfo = await this._render_pGetEventTimeOfDay(eventYear, eventDay, evt.shiftKey); if (chosenTimeInfo == null) return; const nxtPos = Object.keys(this._parent.get("events")).length; const nuEvent = TimeTrackerBase.getGenericEvent(nxtPos, chosenTimeInfo.year, chosenTimeInfo.eventDay, chosenTimeInfo.timeOfDay); this._eventToEdit = nuEvent.id; this._parent.set("events", {...this._parent.get("events"), [nuEvent.id]: nuEvent}); }); const {year, dayInfo, date, monthInfo, seasonInfos, yearInfos, eraInfos} = getTimeInfo({year: eventYear, dayOfYear: eventDay}); const pMutAddEncounter = async ({exportedSublist, nuEncounter}) => { exportedSublist = MiscUtil.copy(exportedSublist); exportedSublist.name = exportedSublist.name || await InputUiUtil.pGetUserString({ title: "Enter Encounter Name", default: await EncounterBuilderHelpers.pGetEncounterName(exportedSublist), }) || "(Unnamed encounter)"; nuEncounter.name = exportedSublist.name; nuEncounter.data = exportedSublist; this._parent.set( "encounters", [...Object.values(this._parent.get("encounters")), nuEncounter] .mergeMap(it => ({[it.id]: it})), ); }; const menuEncounter = ContextUtil.getMenu([ ...ListUtilBestiary.getContextOptionsLoadSublist({ pFnOnSelect: async ({exportedSublist}) => { const nxtPos = Object.keys(this._parent.get("encounters")).length; const nuEncounter = TimeTrackerBase.getGenericEncounter(nxtPos, year, eventDay); return pMutAddEncounter({exportedSublist, nuEncounter}); }, optsSaveManager: { isReferencable: true, }, }), ]); const menuEncounterAtTime = ContextUtil.getMenu([ ...ListUtilBestiary.getContextOptionsLoadSublist({ pFnOnSelect: async ({exportedSublist, isShiftKey}) => { const chosenTimeInfo = await this._render_pGetEventTimeOfDay(eventYear, eventDay, isShiftKey); if (chosenTimeInfo == null) return; const nxtPos = Object.keys(this._parent.get("encounters")).length; const nuEncounter = TimeTrackerBase.getGenericEncounter(nxtPos, chosenTimeInfo.year, chosenTimeInfo.eventDay, chosenTimeInfo.timeOfDay); return pMutAddEncounter({exportedSublist, nuEncounter}); }, optsSaveManager: { isReferencable: true, }, optsFromCurrent: {title: "SHIFT to Add at Current Time"}, optsFromSaved: {title: "SHIFT to Add at Current Time"}, optsFromFile: {title: "SHIFT to Add at Current Time"}, }), ]); const $btnAddEncounter = $(``) .click(evt => ContextUtil.pOpenMenu(evt, menuEncounter)); const $btnAddEncounterAtTime = $(``) .click(evt => ContextUtil.pOpenMenu(evt, menuEncounterAtTime)); const {$modalInner, doClose} = UiUtil.getShowModal({ title: `${TimeTrackerBase.formatDateInfo(dayInfo, date, monthInfo, seasonInfos)}\u2014${TimeTrackerBase.formatYearInfo(year, yearInfos, eraInfos)}`, cbClose: () => { this._parent.removeHook("events", hookEvents); ContextUtil.deleteMenu(menuEncounter); }, zIndex: VeCt.Z_INDEX_BENEATH_HOVER, isUncappedHeight: true, isHeight100: true, $titleSplit: $btnJumpToDay, }); const $hrMoons = $(`
`); const $wrpMoons = $(`
`); const hookMoons = () => { const todayMoonInfos = getMoonInfos(moonDay); $wrpMoons.empty(); todayMoonInfos.forEach(moon => { $$`
${TimeTrackerBase.$getCvsMoon(moon).addClass("mr-2")}
${moon.name}
${moon.phaseName}(Day ${moon.dayOfPeriod + 1}/${moon.period})
`.appendTo($wrpMoons); }); $hrMoons.toggle(!!todayMoonInfos.length); }; this._parent.addHook("moons", hookMoons); hookMoons(); const $wrpEvents = $(`
`); const hookEvents = () => { const todayEvents = getEvents(year, eventDay); $wrpEvents.empty(); this._tmpComps = []; const fnOpenCalendarPicker = this._render_openDayModal_openCalendarPicker.bind(this); todayEvents.forEach(event => { const comp = TimeTrackerRoot_Settings_Event.getInstance(this._board, this._$wrpPanel, this._parent, event); this._tmpComps.push(comp); comp.render($wrpEvents, this._parent, fnOpenCalendarPicker); }); if (!todayEvents.length) $wrpEvents.append(`
(No events)
`); if (this._eventToEdit) { const toEdit = this._tmpComps.find(it => it._state.id === this._eventToEdit); this._eventToEdit = null; if (toEdit) toEdit.doOpenEditModal(); } }; this._parent.addHook("events", hookEvents); hookEvents(); const $wrpEncounters = $(`
`); const hookEncounters = async () => { await this._pLock("encounters"); const todayEncounters = getEncounters(year, eventDay); $wrpEncounters.empty(); // update reference names await Promise.all(todayEncounters.map(async encounter => { const fromStorage = await TimeTrackerRoot_Calendar._pGetDereferencedEncounter(encounter); if (fromStorage != null) encounter.name = fromStorage.name; })); todayEncounters.forEach(encounter => { const $iptName = $(``) .change(() => { encounter.displayName = $iptName.val().trim(); this._parent.triggerMapUpdate("encounters"); }) .val(encounter.displayName == null ? encounter.name : encounter.displayName); const $btnRunEncounter = $(``) .click(() => TimeTrackerRoot_Calendar.pDoRunEncounter(this._parent, encounter)); const $btnResetUse = $(``) .click(() => { if (encounter.countUses === 0) return; encounter.countUses = 0; this._parent.triggerMapUpdate("encounters"); }); const $btnSaveToFile = $(``) .click(async () => { const toSave = await TimeTrackerRoot_Calendar._pGetDereferencedEncounter(encounter); if (!toSave) return JqueryUtil.doToast({content: "Could not find encounter data! Has the encounter been deleted?", type: "warning"}); DataUtil.userDownload("encounter", toSave.data, {fileType: "encounter"}); }); const $cbHasTime = $(``) .prop("checked", encounter.hasTime) .change(() => { const nxtHasTime = $cbHasTime.prop("checked"); if (nxtHasTime) { const {secsPerDay} = getTimeInfo({isBase: true}); if (encounter.timeOfDaySecs == null) encounter.timeOfDaySecs = Math.floor(secsPerDay / 2); // Default to noon encounter.hasTime = true; } else encounter.hasTime = false; this._parent.triggerMapUpdate("encounters"); }); let timeInputs; if (encounter.hasTime) { const timeInfo = getTimeInfo({isBase: true}); const encounterCurTime = {hours: 0, minutes: 0, seconds: 0, timeOfDaySecs: encounter.timeOfDaySecs}; if (encounter.timeOfDaySecs != null) { Object.assign(encounterCurTime, TimeTrackerBase.getHoursMinutesSecondsFromSeconds(timeInfo.secsPerHour, timeInfo.secsPerMinute, encounter.timeOfDaySecs)); } timeInputs = TimeTrackerBase.getClockInputs( timeInfo, encounterCurTime, (nxtTimeSecs) => { encounter.timeOfDaySecs = nxtTimeSecs; this._parent.triggerMapUpdate("encounters"); }, ); } const $btnMove = $(``) .click(() => { this._render_openDayModal_openCalendarPicker({ title: "Choose Encounter Day", fnClick: (evt, eventYear, eventDay) => { encounter.when = { day: eventDay, year: eventYear, }; this._parent.triggerMapUpdate("encounters"); }, prop: "encounters", }); }); const $btnDelete = $(``) .click(() => { encounter.isDeleted = true; this._parent.triggerMapUpdate("encounters"); }); $$`
${$iptName} ${$btnRunEncounter} ${$btnResetUse} ${$btnSaveToFile} ${timeInputs ? $$`
${timeInputs.$iptHours}
:
${timeInputs.$iptMinutes}
:
${timeInputs.$iptSeconds}
` : ""} ${$btnMove} ${$btnDelete}
`.appendTo($wrpEncounters); }); if (!todayEncounters.length) $wrpEncounters.append(`
(No encounters)
`); this._unlock("encounters"); }; this._parent.addHook("encounters", hookEncounters); hookEncounters(); $$`
${$wrpMoons} ${$hrMoons}
Events
${$btnAddEvent}${$btnAddEventAtTime}
${$wrpEvents}
Encounters
${$btnAddEncounter}${$btnAddEncounterAtTime}
${$wrpEncounters}
`.appendTo($modalInner); } _render_getUserEventTime () { const {getTimeInfo} = this._parent; const { hoursPerDay, minutesPerHour, secsPerMinute, secsPerHour, } = getTimeInfo(); const padLengthHours = `${hoursPerDay}`.length; const padLengthMinutes = `${minutesPerHour}`.length; const padLengthSecs = `${secsPerMinute}`.length; return new Promise(resolve => { class EventTimeModal extends BaseComponent { render ($parent) { const $selMode = ComponentUiUtil.$getSelEnum(this, "mode", {values: ["Exact Time", "Time from Now"]}).addClass("mb-2"); const $iptExHour = ComponentUiUtil.$getIptInt( this, "exactHour", 0, { $ele: $(``), padLength: padLengthHours, min: 0, max: hoursPerDay - 1, }, ); const $iptExMinutes = ComponentUiUtil.$getIptInt( this, "exactMinute", 0, { $ele: $(``), padLength: padLengthMinutes, min: 0, max: minutesPerHour - 1, }, ); const $iptExSecs = ComponentUiUtil.$getIptInt( this, "exactSec", 0, { $ele: $(``), padLength: padLengthSecs, min: 0, max: secsPerMinute - 1, }, ); const $wrpExact = $$`
${$iptExHour}
:
${$iptExMinutes}
:
${$iptExSecs}
`; const $iptOffsetHour = ComponentUiUtil.$getIptInt( this, "offsetHour", 0, { $ele: $(``), min: -TimeTrackerBase._MAX_TIME, max: TimeTrackerBase._MAX_TIME, }, ); const $iptOffsetMinutes = ComponentUiUtil.$getIptInt( this, "offsetMinute", 0, { $ele: $(``), min: -TimeTrackerBase._MAX_TIME, max: TimeTrackerBase._MAX_TIME, }, ); const $iptOffsetSecs = ComponentUiUtil.$getIptInt( this, "offsetSec", 0, { $ele: $(``), min: -TimeTrackerBase._MAX_TIME, max: TimeTrackerBase._MAX_TIME, }, ); const $wrpOffset = $$`
${$iptOffsetHour}
hours,
${$iptOffsetMinutes}
minutes, and
${$iptOffsetSecs}
seconds from now
`; const hookMode = () => { $wrpExact.toggleClass("hidden", this._state.mode !== "Exact Time"); $wrpOffset.toggleClass("hidden", this._state.mode === "Exact Time"); }; this._addHookBase("mode", hookMode); hookMode(); const $btnOk = $(``) .click(() => doClose(true)); $$`
${$selMode} ${$wrpExact} ${$wrpOffset}
${$btnOk}
`.appendTo($parent); } _getDefaultState () { return { mode: "Exact Time", exactHour: 0, exactMinute: 0, exactSec: 0, offsetHour: 0, offsetMinute: 0, offsetSec: 0, }; } } const md = new EventTimeModal(); const {$modalInner, doClose} = UiUtil.getShowModal({ title: "Enter a Time", cbClose: (isDataEntered) => { if (!isDataEntered) return resolve(null); const obj = md.toObject(); if (obj.mode === "Exact Time") { resolve({mode: "timeExact", timeOfDaySecs: (obj.exactHour * secsPerHour) + (obj.exactMinute * secsPerMinute) + obj.exactSec}); } else { resolve({mode: "timeOffset", secsOffset: (obj.offsetHour * secsPerHour) + (obj.offsetMinute * secsPerMinute) + obj.offsetSec}); } }, }); md.render($modalInner); }); } async _render_pGetEventTimeOfDay (eventYear, eventDay, isShiftDown) { const {getTimeInfo} = this._parent; let timeOfDay = null; if (isShiftDown) { const {timeOfDaySecs} = getTimeInfo(); timeOfDay = timeOfDaySecs; } else { const userInput = await this._render_getUserEventTime(); if (userInput == null) return null; if (userInput.mode === "timeExact") timeOfDay = userInput.timeOfDaySecs; else { const {timeOfDaySecs, secsPerYear, secsPerDay} = getTimeInfo(); while (Math.abs(userInput.secsOffset) >= secsPerYear) { if (userInput.secsOffset < 0) { userInput.secsOffset += secsPerYear; eventYear -= 1; } else { userInput.secsOffset -= secsPerYear; eventYear += 1; } } eventYear = Math.max(0, eventYear); while (Math.abs(userInput.secsOffset) >= secsPerDay || userInput.secsOffset < 0) { if (userInput.secsOffset < 0) { userInput.secsOffset += secsPerDay; eventDay -= 1; } else { userInput.secsOffset -= secsPerDay; eventDay += 1; } } eventDay = Math.max(0, eventDay); timeOfDay = timeOfDaySecs + userInput.secsOffset; } } return {eventYear, eventDay, timeOfDay}; } /** * @param opts Options object. * @param opts.title Modal title. * @param opts.fnClick Click handler. * @param opts.prop Component state property. */ _render_openDayModal_openCalendarPicker (opts) { opts = opts || {}; const {$modalInner, doClose} = UiUtil.getShowModal({ title: opts.title, zIndex: VeCt.Z_INDEX_BENEATH_HOVER, }); // Create a copy of the current state, as a temp component const temp = new TimeTrackerBase(null, null, {isTemporary: true}); // Copy state Object.assign(temp.__state, this._parent.component.__state); const tempPod = temp.getPod(); const {$wrpDateControls, $iptYear, $iptMonth} = TimeTrackerRoot_Calendar.getDateControls(tempPod, {isHideWeeks: true, isHideDays: true}); $wrpDateControls.addClass("mb-2").appendTo($modalInner); const $wrpCalendar = $(`
`).appendTo($modalInner); const hookCalendar = () => { const timeInfo = tempPod.getTimeInfo(); TimeTrackerRoot_Calendar.renderCalendar( tempPod, $wrpCalendar, timeInfo, (evt, eventYear, eventDay) => { opts.fnClick(evt, eventYear, eventDay); doClose(); }, { isHideDay: true, hasColumnLabels: this._parent.get("hasCalendarLabelsColumns"), hasRowLabels: this._parent.get("hasCalendarLabelsRows"), }, ); $iptYear.val(timeInfo.year + 1); $iptMonth.val(timeInfo.month + 1); }; tempPod.addHook("time", hookCalendar); tempPod.addHook(opts.prop, hookCalendar); hookCalendar(); const hookComp = () => this._parent.set(opts.prop, tempPod.get(opts.prop)); tempPod.addHook(opts.prop, hookComp); // (Don't run hook immediately, as we won't make any changes) } static async _pGetDereferencedEncounter (encounter) { const saveManager = new SaveManager({ isReadOnlyUi: true, page: UrlUtil.PG_BESTIARY, isReferencable: true, }); await saveManager.pMutStateFromStorage(); encounter = MiscUtil.copy(encounter); if ( encounter.data.managerClient_isReferencable && !encounter.data.managerClient_isLoadAsCopy && encounter.data.saveId ) { encounter = MiscUtil.copy(encounter); const nxtData = await saveManager.pGetSaveBySaveId({saveId: encounter.data.saveId}); if (!nxtData) return null; encounter.data = nxtData; } return encounter; } static async pDoRunEncounter (parent, encounter) { if (encounter.countUses > 0) return; const $elesData = DmScreenUtil.$getPanelDataElements({board: parent.component._board, type: PANEL_TYP_INITIATIVE_TRACKER}); if ($elesData.length) { let $tracker; if ($elesData.length === 1) { $tracker = $elesData[0]; } else { const ix = await InputUiUtil.pGetUserEnum({ default: 0, title: "Choose a Tracker", placeholder: "Select tracker", }); if (ix != null && ~ix) { $tracker = $elesData[ix]; } } if ($tracker) { const toLoad = await TimeTrackerRoot_Calendar._pGetDereferencedEncounter(encounter); if (!toLoad) return JqueryUtil.doToast({content: "Could not find encounter data! Has the encounter been deleted?", type: "warning"}); const {entityInfos, encounterInfo} = await ListUtilBestiary.pGetLoadableSublist({exportedSublist: toLoad.data}); try { await $tracker.data("pDoLoadEncounter")({entityInfos, encounterInfo}); } catch (e) { JqueryUtil.doToast({type: "error", content: `Failed to add encounter! ${VeCt.STR_SEE_CONSOLE}`}); throw e; } JqueryUtil.doToast({type: "success", content: "Encounter added to Initiative Tracker."}); encounter.countUses += 1; parent.triggerMapUpdate("encounters"); } } else { encounter.countUses += 1; parent.triggerMapUpdate("encounters"); } } } class TimeTrackerRoot_Settings extends TimeTrackerComponent { static getTimeNum (str, isAllowNegative) { return UiUtil.strToInt( str, isAllowNegative ? 0 : TimeTrackerBase._MIN_TIME, { min: isAllowNegative ? -TimeTrackerBase._MAX_TIME : TimeTrackerBase._MIN_TIME, max: TimeTrackerBase._MAX_TIME, fallbackOnNaN: isAllowNegative ? 0 : TimeTrackerBase._MIN_TIME, }, ); } constructor (tracker, $wrpPanel) { super(tracker, $wrpPanel); // temp components this._tmpComps = {}; } render ($parent, parent) { $parent.empty(); this._parent = parent; const $getIptTime = (prop, opts) => { opts = opts || {}; const $ipt = $(``) .change(() => this._parent.set(prop, TimeTrackerRoot_Settings.getTimeNum($ipt.val(), opts.isAllowNegative))); const hook = () => $ipt.val(this._parent.get(prop)); this._parent.addHook(prop, hook); hook(); return $ipt; }; const btnHideHooks = []; const $getBtnHide = (prop, $ele, ...$eles) => { const $btn = $(``) .click(() => this._parent.set(prop, !this._parent.get(prop))); const hook = () => { const isHidden = this._parent.get(prop); $ele.toggleClass("hidden", isHidden); $btn.toggleClass("active", isHidden); if ($eles) $eles.forEach($e => $e.toggleClass("hidden", isHidden)); }; this._parent.addHook(prop, hook); btnHideHooks.push(hook); return $btn; }; const $getBtnReset = (...props) => { return $(``) .click(() => { if (!confirm("Are you sure?")) return; props.forEach(prop => this._parent.set(prop, TimeTrackerBase._DEFAULT_STATE[prop])); }); }; const $selWindUnits = $(``) .change(() => this._parent.set("unitsWindSpeed", $selWindUnits.val())); const hookWindUnits = () => $selWindUnits.val(this._parent.get("unitsWindSpeed")); this._parent.addHook("unitsWindSpeed", hookWindUnits); hookWindUnits(); const metaDays = this._render_getChildMeta_2({ prop: "days", Cls: TimeTrackerRoot_Settings_Day, name: "Day", fnGetGeneric: TimeTrackerRoot.getGenericDay, }); const metaMonths = this._render_getChildMeta_2({ prop: "months", Cls: TimeTrackerRoot_Settings_Month, name: "Month", fnGetGeneric: TimeTrackerRoot.getGenericMonth, }); const metaSeasons = this._render_getChildMeta_2({ prop: "seasons", Cls: TimeTrackerRoot_Settings_Season, name: "Season", $dispEmpty: $(`
(No seasons)
`), fnGetGeneric: TimeTrackerRoot.getGenericSeason, }); const metaYears = this._render_getChildMeta_2({ prop: "years", Cls: TimeTrackerRoot_Settings_Year, name: "Year", $dispEmpty: $(`
(No named years)
`), fnGetGeneric: TimeTrackerRoot.getGenericYear, }); const metaEras = this._render_getChildMeta_2({ prop: "eras", Cls: TimeTrackerRoot_Settings_Era, name: "Era", $dispEmpty: $(`
(No eras)
`), fnGetGeneric: TimeTrackerRoot.getGenericEra, }); const metaMoons = this._render_getChildMeta_2({ prop: "moons", Cls: TimeTrackerRoot_Settings_Moon, name: "Moon", $dispEmpty: $(`
(No moons)
`), fnGetGeneric: TimeTrackerRoot.getGenericMoon, }); const $sectClock = $$`
Hours per Day
${$getIptTime("hoursPerDay")}
Minutes per Hour
${$getIptTime("minutesPerHour")}
Seconds per Minute
${$getIptTime("secondsPerMinute")}
`; const $btnResetClock = $getBtnReset("hoursPerDay", "minutesPerHour", "secondsPerMinute"); const $btnHideSectClock = $getBtnHide("isClockSectionHidden", $sectClock, $btnResetClock); const $headClock = $$`
Clock
${$btnResetClock}${$btnHideSectClock}
`; const $sectCalendar = $$`
`; const $btnResetCalendar = $getBtnReset("hoursPerDay", "minutesPerHour", "secondsPerMinute"); const $btnHideSectCalendar = $getBtnHide("isCalendarSectionHidden", $sectCalendar, $btnResetCalendar); const $headCalendar = $$`
Calendar
${$btnResetCalendar}${$btnHideSectCalendar}
`; const $sectMechanics = $$`
Hours per Long rest
${$getIptTime("hoursPerLongRest")}
Minutes per Short Rest
${$getIptTime("minutesPerShortRest")}
Seconds per Round
${$getIptTime("secondsPerRound")}
`; const $btnResetMechanics = $getBtnReset("hoursPerLongRest", "minutesPerShortRest", "secondsPerRound"); const $btnHideSectMechanics = $getBtnHide("isMechanicsSectionHidden", $sectMechanics, $btnResetMechanics); const $headMechanics = $$`
Game Mechanics
${$btnResetMechanics}${$btnHideSectMechanics}
`; const $sectOffsets = $$`
Year Offset
${$getIptTime("offsetYears", {isAllowNegative: true})}
Year Start Weekday Offset
${$getIptTime("offsetMonthStartDay")}
`; const $btnResetOffsets = $getBtnReset("offsetYears", "offsetMonthStartDay"); const $btnHideSectOffsetsHide = $getBtnHide("isOffsetsSectionHidden", $sectOffsets, $btnResetOffsets); const $headOffsets = $$`
Offsets
${$btnResetOffsets}${$btnHideSectOffsetsHide}
`; const $sectDays = $$`
Name
${metaDays.$btnAdd}
${metaDays.$wrpRows}
`; const $btnHideSectDays = $getBtnHide("isDaysSectionHidden", $sectDays); const $headDays = $$`
Days
${$btnHideSectDays}
`; const $sectMonths = $$`
Name
Days
${metaMonths.$btnAdd.addClass("no-shrink")}
${metaMonths.$wrpRows}
`; const $btnHideSectMonths = $getBtnHide("isMonthsSectionHidden", $sectMonths); const $headMonths = $$`
Months
${$btnHideSectMonths}
`; const $sectSeasons = $$`
Name
Sunrise
Sunset
Start
End
${metaSeasons.$btnAdd.addClass("no-shrink")}
${metaSeasons.$wrpRows}
`; const $btnHideSectSeasons = $getBtnHide("isSeasonsSectionHidden", $sectSeasons); const $headSeasons = $$`
Seasons
${$btnHideSectSeasons}
`; const $sectYears = $$`
Name
Year
${metaYears.$btnAdd.addClass("no-shrink")}
${metaYears.$wrpRows}
`; const $btnHideSectYears = $getBtnHide("isYearsSectionHidden", $sectYears); const $headYears = $$`
Named Years
${$btnHideSectYears}
`; const $sectEras = $$`
Name
Abbv.
Start
End
${metaEras.$btnAdd.addClass("no-shrink")}
${metaEras.$wrpRows}
`; const $btnHideSectEras = $getBtnHide("isErasSectionHidden", $sectEras); const $headEras = $$`
Eras
${$btnHideSectEras}
`; const $sectMoons = $$`
Moon
Offset
Period
${metaMoons.$btnAdd.addClass("no-shrink")}
${metaMoons.$wrpRows}
`; const $btnHideSectMoons = $getBtnHide("isMoonsSectionHidden", $sectMoons); const $headMoons = $$`
Moons
${$btnHideSectMoons}
`; btnHideHooks.forEach(fn => fn()); $$`
${$headClock} ${$sectClock}
${$headCalendar} ${$sectCalendar}
${$headMechanics} ${$sectMechanics}
${$headOffsets} ${$sectOffsets}
Wind Speed Units
${$selWindUnits}

${$headDays} ${$sectDays}
${$headMonths} ${$sectMonths}
${$headSeasons} ${$sectSeasons}
${$headYears} ${$sectYears}
${$headEras} ${$sectEras}
${$headMoons} ${$sectMoons}
`.appendTo($parent); } _render_getChildMeta_2 ({prop, Cls, name, $dispEmpty = null, fnGetGeneric}) { const $wrpRows = this._render_$getWrpChildren(); if ($dispEmpty) $wrpRows.append($dispEmpty); const $btnAdd = this._render_$getBtnAddChild({ prop: prop, name, fnGetGeneric, }); const dragMeta = { swapRowPositions: (ixA, ixB) => { const a = this._parent.component._state[prop][ixA]; this._parent.component._state[prop][ixA] = this._parent.component._state[prop][ixB]; this._parent.component._state[prop][ixB] = a; this._parent.component._triggerCollectionUpdate(prop); this._parent.component._state[prop] .map(it => this._parent.component._rendered[prop][it.id].$wrpRow) .forEach($it => $wrpRows.append($it)); }, $getChildren: () => { return this._parent.component._state[prop] .map(it => this._parent.component._rendered[prop][it.id].$wrpRow); }, $parent: $wrpRows, }; const renderableCollection = new Cls( this._parent.component, prop, $wrpRows, dragMeta, ); const hk = () => { renderableCollection.render(); if ($dispEmpty) $dispEmpty.toggleVe(!this._parent.get(prop)?.length); }; this._parent.component._addHookBase(prop, hk); hk(); return {$btnAdd, $wrpRows}; } _render_$getWrpChildren () { return $(`
`); } _render_$getBtnAddChild ({prop, name, fnGetGeneric}) { return $(``) .click(() => { const nxt = fnGetGeneric(this._parent.get(prop).length); this._parent.set(prop, [...this._parent.get(prop), nxt]); }); } } class RenderableCollectionTimeTracker extends RenderableCollectionBase { constructor (comp, prop, $wrpRows, dragMeta) { super(comp, prop); this._$wrpRows = $wrpRows; this._dragMeta = dragMeta; } } class TimeTrackerRoot_Settings_Day extends RenderableCollectionTimeTracker { getNewRender (entity, i) { const comp = BaseComponent.fromObject(entity.data, "*"); comp._addHookAll("state", () => { entity.data = comp.toObject("*"); this._comp._triggerCollectionUpdate("days"); }); const $iptName = ComponentUiUtil.$getIptStr(comp, "name", {$ele: $(``)}); const $padDrag = DragReorderUiUtil.$getDragPadOpts(() => $wrpRow, this._dragMeta); const $btnRemove = $(``) .click(() => this._comp._state.days = this._comp._state.days.filter(it => it !== entity)); const $wrpRow = $$`
${$iptName} ${$padDrag} ${$btnRemove}
`.appendTo(this._$wrpRows); return { comp, $wrpRow, }; } doUpdateExistingRender (renderedMeta, entity, i) { renderedMeta.comp._proxyAssignSimple("state", entity.data, true); if (!renderedMeta.$wrpRow.parent().is(this._$wrpRows)) renderedMeta.$wrpRow.appendTo(this._$wrpRows); } } class TimeTrackerRoot_Settings_Month extends RenderableCollectionTimeTracker { getNewRender (entity, i) { const comp = BaseComponent.fromObject(entity.data, "*"); comp._addHookAll("state", () => { entity.data = comp.toObject("*"); this._comp._triggerCollectionUpdate("months"); }); const $iptName = ComponentUiUtil.$getIptStr(comp, "name", {$ele: $(``)}); const $iptDays = ComponentUiUtil.$getIptInt(comp, "days", 1, {$ele: $(``), min: TimeTrackerBase._MIN_TIME, max: TimeTrackerBase._MAX_TIME}); const $padDrag = DragReorderUiUtil.$getDragPadOpts(() => $wrpRow, this._dragMeta); const $btnRemove = $(``) .click(() => this._comp._state.months = this._comp._state.months.filter(it => it !== entity)); const $wrpRow = $$`
${$iptName} ${$iptDays} ${$padDrag} ${$btnRemove}
`.appendTo(this._$wrpRows); return { comp, $wrpRow, }; } doUpdateExistingRender (renderedMeta, entity, i) { renderedMeta.comp._proxyAssignSimple("state", entity.data, true); if (!renderedMeta.$wrpRow.parent().is(this._$wrpRows)) renderedMeta.$wrpRow.appendTo(this._$wrpRows); } } class TimeTrackerRoot_Settings_Event extends TimeTrackerComponent { render ($parent, parent, fnOpenCalendarPicker) { const {getTimeInfo} = parent; const doShowHideEntries = () => { const isShown = this._state.entries.length && !this._state.isHidden; $wrpEntries.toggleClass("hidden", !isShown); }; const $dispEntries = $(`
`); const hookEntries = () => { $dispEntries.html(Renderer.get().render({entries: MiscUtil.copy(this._state.entries)})); doShowHideEntries(); }; this._addHookBase("entries", hookEntries); const $wrpEntries = $$`
${$dispEntries}
`; const $iptName = $(``) .change(() => this._state.name = $iptName.val().trim() || "(Unnamed event)"); const hookName = () => $iptName.val(this._state.name || "(Unnamed event)"); this._addHookBase("name", hookName); const $btnShowHide = $(``) .click(() => this._state.isHidden = !this._state.isHidden); const hookShowHide = () => { $btnShowHide.toggleClass("active", !!this._state.isHidden); doShowHideEntries(); }; this._addHookBase("isHidden", hookShowHide); const $btnEdit = $(``) .click(() => this.doOpenEditModal()); const $cbHasTime = $(``) .prop("checked", this._state.hasTime) .change(() => { const nxtHasTime = $cbHasTime.prop("checked"); if (nxtHasTime) { const {secsPerDay} = getTimeInfo({isBase: true}); // Modify the base state to avoid double-updating the collection if (this.__state.timeOfDaySecs == null) this.__state.timeOfDaySecs = Math.floor(secsPerDay / 2); // Default to noon this._state.hasTime = true; } else this._state.hasTime = false; }); let timeInputs; if (this._state.hasTime) { const timeInfo = getTimeInfo({isBase: true}); const eventCurTime = {hours: 0, minutes: 0, seconds: 0, timeOfDaySecs: this._state.timeOfDaySecs}; if (this._state.timeOfDaySecs != null) { Object.assign(eventCurTime, TimeTrackerBase.getHoursMinutesSecondsFromSeconds(timeInfo.secsPerHour, timeInfo.secsPerMinute, this._state.timeOfDaySecs)); } timeInputs = TimeTrackerBase.getClockInputs( timeInfo, eventCurTime, (nxtTimeSecs) => { this._state.timeOfDaySecs = nxtTimeSecs; }, ); } const $btnMove = $(``) .click(() => { fnOpenCalendarPicker({ title: "Choose Event Day", fnClick: (evt, eventYear, eventDay) => { this._state.when = { day: eventDay, year: eventYear, }; }, prop: "events", }); }); const $btnRemove = $(``) .click(() => this._state.isDeleted = true); hookEntries(); hookName(); hookShowHide(); $$`
${$iptName} ${$btnShowHide} ${$btnEdit} ${timeInputs ? $$`
${timeInputs.$iptHours}
:
${timeInputs.$iptMinutes}
:
${timeInputs.$iptSeconds}
` : ""} ${$btnMove} ${$btnRemove}
${$wrpEntries}
`.appendTo($parent); } doOpenEditModal (overlayColor = "transparent") { // Edit against a fake component, so we don't modify the original until we save const fauxComponent = new BaseComponent(); fauxComponent._state.name = this._state.name; fauxComponent._state.entries = MiscUtil.copy(this._state.entries); const {$modalInner, doClose} = UiUtil.getShowModal({ title: "Edit Event", overlayColor: overlayColor, cbClose: (isDataEntered) => { if (!isDataEntered) return; this._state.name = fauxComponent._state.name; this._state.entries = MiscUtil.copy(fauxComponent._state.entries); }, }); const $iptName = ComponentUiUtil.$getIptStr(fauxComponent, "name", {$ele: $(``)}); const $iptEntries = ComponentUiUtil.$getIptEntries(fauxComponent, "entries", {$ele: $(`