`);
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 = 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}));
$$`
`.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 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 = $(`
`);
// 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 => {
$$`
`.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 `
`;
}).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(async () => {
let ixCur = TimeTrackerRoot_Clock_Weather._TEMPERATURES.indexOf(this._state.temperature);
if (!~ixCur) ixCur = 2;
const ix = await InputUiUtil.pGetUserIcon({
values: TimeTrackerRoot_Clock_Weather._TEMPERATURES.map((it, i) => {
const meta = TimeTrackerRoot_Clock_Weather._TEMPERATURE_META[i];
return {
name: it.uppercaseFirst(),
buttonClassActive: meta.class ? `${meta.class} active` : null,
iconClass: `fal ${meta.icon}`,
};
}),
title: "Temperature",
default: ixCur,
});
if (ix != null) this._state.temperature = TimeTrackerRoot_Clock_Weather._TEMPERATURES[ix];
});
const hookTemperature = () => {
TimeTrackerRoot_Clock_Weather._TEMPERATURE_META.forEach(it => $btnTemperature.removeClass(it.class));
let ix = TimeTrackerRoot_Clock_Weather._TEMPERATURES.indexOf(this._state.temperature);
if (!~ix) ix = 0;
const meta = TimeTrackerRoot_Clock_Weather._TEMPERATURE_META[ix];
$btnTemperature.addClass(meta.class);
$btnTemperature.title(this._state.temperature.uppercaseFirst()).html(`
`);
};
this._addHookBase("temperature", hookTemperature);
hookTemperature();
const $btnPrecipitation = $(`
`)
.click(async () => {
const {
numHours,
seasonInfos,
} = getTimeInfo({isBase: true});
const useNightIcon = seasonInfos.length && !(numHours >= seasonInfos[0].sunriseHour && numHours < seasonInfos[0].sunsetHour);
let ixCur = TimeTrackerRoot_Clock_Weather._PRECIPICATION.indexOf(this._state.precipitation);
if (!~ixCur) ixCur = 0;
const ix = await InputUiUtil.pGetUserIcon({
values: TimeTrackerRoot_Clock_Weather._PRECIPICATION.map((it, i) => {
const meta = TimeTrackerRoot_Clock_Weather._PRECIPICATION_META[i];
return {
name: TimeTrackerUtil.revSlugToText(it),
iconClass: `fal ${useNightIcon && meta.iconNight ? meta.iconNight : meta.icon}`,
buttonClass: `btn-default`,
};
}),
title: "Weather",
default: ixCur,
});
if (ix != null) this._state.precipitation = TimeTrackerRoot_Clock_Weather._PRECIPICATION[ix];
});
let lastPrecipitationTimeInfo = null;
const hookPrecipitation = (prop) => {
const {
numHours,
seasonInfos,
} = getTimeInfo({isBase: true});
const precipitationTimeInfo = {numHours, seasonInfos};
if (prop === "time" && CollectionUtil.deepEquals(lastPrecipitationTimeInfo, precipitationTimeInfo)) return;
lastPrecipitationTimeInfo = precipitationTimeInfo;
const useNightIcon = seasonInfos.length && !(numHours >= seasonInfos[0].sunriseHour && numHours < seasonInfos[0].sunsetHour);
let ix = TimeTrackerRoot_Clock_Weather._PRECIPICATION.indexOf(this._state.precipitation);
if (!~ix) ix = 0;
const meta = TimeTrackerRoot_Clock_Weather._PRECIPICATION_META[ix];
$btnPrecipitation.title(TimeTrackerUtil.revSlugToText(this._state.precipitation)).html(`
`);
};
this._addHookBase("precipitation", hookPrecipitation);
this._parent.addHook("time", hookPrecipitation);
hookPrecipitation();
const $btnWindDirection = $(`
`)
.click(async () => {
const bearing = await TimeTrackerUtil.pGetUserWindBearing(this._state.windDirection);
if (bearing != null) this._state.windDirection = bearing;
});
const hookWindDirection = () => {
let ixCur = TimeTrackerRoot_Clock_Weather._WIND_SPEEDS.indexOf(this._state.windSpeed);
if (!~ixCur) ixCur = 0;
if (ixCur) {
const speedClass = ixCur >= 5 ? "fas" : ixCur >= 3 ? "far" : "fal";
$btnWindDirection.html(`
`);
} else $btnWindDirection.html(`
`);
};
this._addHookBase("windDirection", hookWindDirection);
this._addHookBase("windSpeed", hookWindDirection);
hookWindDirection();
const $btnWindSpeed = $(`
`)
.click(async () => {
let ixCur = TimeTrackerRoot_Clock_Weather._WIND_SPEEDS.indexOf(this._state.windSpeed);
if (!~ixCur) ixCur = 0;
const ix = await InputUiUtil.pGetUserIcon({
values: TimeTrackerRoot_Clock_Weather._WIND_SPEEDS.map((it, i) => {
const meta = TimeTrackerRoot_Clock_Weather._WIND_SPEEDS_META[i];
return {
name: TimeTrackerUtil.revSlugToText(it),
buttonClass: `btn-default`,
iconContent: `
${this._parent.get("unitsWindSpeed") === "mph" ? `${meta.mph} mph` : `${meta.kmph} km/h`}
`,
};
}),
title: "Wind Speed",
default: ixCur,
});
if (ix != null) this._state.windSpeed = TimeTrackerRoot_Clock_Weather._WIND_SPEEDS[ix];
});
const hookWindSpeed = () => {
let ix = TimeTrackerRoot_Clock_Weather._WIND_SPEEDS.indexOf(this._state.windSpeed);
if (!~ix) ix = 0;
const meta = TimeTrackerRoot_Clock_Weather._WIND_SPEEDS_META[ix];
$btnWindSpeed.text(TimeTrackerUtil.revSlugToText(this._state.windSpeed)).title(this._parent.get("unitsWindSpeed") === "mph" ? `${meta.mph} mph` : `${meta.kmph} km/h`);
};
this._addHookBase("windSpeed", hookWindSpeed);
this._parent.addHook("unitsWindSpeed", hookWindSpeed);
hookWindSpeed();
const $hovEnvEffects = $(`
`);
const $wrpEnvEffects = $$`
${$hovEnvEffects}
`;
let hoverMetaEnvEffects = null;
const hookEnvEffects = () => {
$hovEnvEffects.off("mouseover").off("mousemove").off("mouseleave");
const hashes = [];
const fnGetHash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_TRAPS_HAZARDS];
if (this._state.temperature === TimeTrackerRoot_Clock_Weather._TEMPERATURES[0]) {
hashes.push(fnGetHash(({name: "Extreme Cold", source: Parser.SRC_DMG})));
}
if (this._state.temperature === TimeTrackerRoot_Clock_Weather._TEMPERATURES.last()) {
hashes.push(fnGetHash(({name: "Extreme Heat", source: Parser.SRC_DMG})));
}
if (["rain-heavy", "thunderstorm", "snow"].includes(this._state.precipitation)) {
hashes.push(fnGetHash(({name: "Heavy Precipitation", source: Parser.SRC_DMG})));
}
if (TimeTrackerRoot_Clock_Weather._WIND_SPEEDS.indexOf(this._state.windSpeed) >= 3) {
hashes.push(fnGetHash(({name: "Strong Wind", source: Parser.SRC_DMG})));
}
$hovEnvEffects.show();
if (hashes.length === 1) {
const ele = $hovEnvEffects[0];
$hovEnvEffects.mouseover(evt => Renderer.hover.pHandleLinkMouseOver(evt, ele, {page: UrlUtil.PG_TRAPS_HAZARDS, source: Parser.SRC_DMG, hash: hashes[0]}));
$hovEnvEffects.mouseleave(evt => Renderer.hover.handleLinkMouseLeave(evt, ele));
$hovEnvEffects.mousemove(evt => Renderer.hover.handleLinkMouseMove(evt, ele));
} else if (hashes.length) {
if (hoverMetaEnvEffects == null) hoverMetaEnvEffects = Renderer.hover.getMakePredefinedHover({type: "entries", entries: []});
$hovEnvEffects
.mouseover(async evt => {
// load the first on its own, to avoid racing to fill the cache
const first = await DataLoader.pCacheAndGet(UrlUtil.PG_TRAPS_HAZARDS, Parser.SRC_DMG, hashes[0]);
const others = await Promise.all(hashes.slice(1).map(hash => DataLoader.pCacheAndGet(UrlUtil.PG_TRAPS_HAZARDS, Parser.SRC_DMG, hash)));
const allEntries = [first, ...others].map(it => ({type: "statblockInline", dataType: "hazard", data: MiscUtil.copy(it)}));
const toShow = {
type: "entries",
entries: allEntries,
data: {hoverTitle: `Weather Effects`},
};
Renderer.hover.updatePredefinedHover(hoverMetaEnvEffects.id, toShow);
hoverMetaEnvEffects.mouseOver(evt, $hovEnvEffects[0]);
})
.mousemove(evt => hoverMetaEnvEffects.mouseMove(evt, $hovEnvEffects[0]))
.mouseleave(evt => hoverMetaEnvEffects.mouseLeave(evt, $hovEnvEffects[0]));
} else $hovEnvEffects.hide();
};
this._addHookBase("temperature", hookEnvEffects);
this._addHookBase("precipitation", hookEnvEffects);
this._addHookBase("windSpeed", hookEnvEffects);
hookEnvEffects();
$$`
Weather ${$btnRandomise}
${$btnTemperature}${$btnPrecipitation}
Wind
${$btnWindDirection}
${$btnWindSpeed}
${$wrpEnvEffects}
`.appendTo($parent);
}
_getDefaultState () { return MiscUtil.copy(TimeTrackerRoot_Clock_Weather._DEFAULT_STATE); }
}
TimeTrackerRoot_Clock_Weather._TEMPERATURES = [
"freezing",
"cold",
"mild",
"hot",
"scorching",
];
TimeTrackerRoot_Clock_Weather._PRECIPICATION = [
"sunny",
"cloudy",
"foggy",
"rain-light",
"rain-heavy",
"thunderstorm",
"hail",
"snow",
];
TimeTrackerRoot_Clock_Weather._WIND_SPEEDS = [
"calm",
"breeze-light",
"breeze-moderate",
"breeze-strong",
"gale-near",
"gale",
"gale-severe",
"storm",
"hurricane",
];
TimeTrackerRoot_Clock_Weather._DEFAULT_STATE = {
temperature: TimeTrackerRoot_Clock_Weather._TEMPERATURES[2],
precipitation: TimeTrackerRoot_Clock_Weather._PRECIPICATION[0],
windDirection: 0,
windSpeed: TimeTrackerRoot_Clock_Weather._WIND_SPEEDS[0],
};
TimeTrackerRoot_Clock_Weather._TEMPERATURE_META = [
{icon: "fa-temperature-frigid", class: "btn-primary"},
{icon: "fa-thermometer-quarter", class: "btn-info"},
{icon: "fa-thermometer-half"},
{icon: "fa-thermometer-three-quarters", class: "btn-warning"},
{icon: "fa-temperature-hot", class: "btn-danger"},
];
TimeTrackerRoot_Clock_Weather._PRECIPICATION_META = [
{icon: "fa-sun", iconNight: "fa-moon"},
{icon: "fa-clouds-sun", iconNight: "fa-clouds-moon"},
{icon: "fa-fog"},
{icon: "fa-cloud-drizzle"},
{icon: "fa-cloud-showers-heavy"},
{icon: "fa-thunderstorm"},
{icon: "fa-cloud-hail"},
{icon: "fa-cloud-snow"},
];
TimeTrackerRoot_Clock_Weather._WIND_SPEEDS_META = [ // (Beaufort scale equivalent)
{mph: "<1", kmph: "<2"}, // 0-2
{mph: "1-7", kmph: "2-11"}, // 1-2
{mph: "8-18", kmph: "12-28"}, // 3-4
{mph: "19-31", kmph: "29-49"}, // 5-6
{mph: "32-38", kmph: "50-61"}, // 7
{mph: "39-46", kmph: "62-74"}, // 8
{mph: "47-54", kmph: "75-88"}, // 9
{mph: "55-72", kmph: "89-117"}, // 10-11
{mph: "≥73", kmph: "≥118"}, // 12
];
class TimeTrackerRoot_Clock_RandomWeather extends BaseComponent {
constructor (opts) {
super();
this._unitsWindSpeed = opts.unitsWindSpeed;
}
render ($modalInner, doClose) {
$modalInner.empty();
const $btnsTemperature = TimeTrackerRoot_Clock_Weather._TEMPERATURES
.map((it, i) => {
const meta = TimeTrackerRoot_Clock_Weather._TEMPERATURE_META[i];
return {
temperature: it,
name: it.uppercaseFirst(),
buttonClass: meta.class,
iconClass: `fal ${meta.icon}`,
};
})
.map(v => {
const $btn = $$`
`
.click(() => {
if (this._state.allowedTemperatures.includes(v.temperature)) this._state.allowedTemperatures = this._state.allowedTemperatures.filter(it => it !== v.temperature);
else this._state.allowedTemperatures = [...this._state.allowedTemperatures, v.temperature];
});
const hookTemperature = () => {
const isActive = this._state.allowedTemperatures.includes(v.temperature);
if (v.buttonClass) {
$btn.toggleClass("btn-default", !isActive);
$btn.toggleClass(v.buttonClass, isActive);
}
$btn.toggleClass("active", isActive);
};
this._addHookBase("allowedTemperatures", hookTemperature);
hookTemperature();
return $btn;
});
const $btnsPrecipitation = TimeTrackerRoot_Clock_Weather._PRECIPICATION
.map((it, i) => {
const meta = TimeTrackerRoot_Clock_Weather._PRECIPICATION_META[i];
return {
precipitation: it,
name: TimeTrackerUtil.revSlugToText(it),
iconClass: `fal ${meta.icon}`,
};
})
.map(v => {
const $btn = $$`
`
.click(() => {
if (this._state.allowedPrecipitations.includes(v.precipitation)) this._state.allowedPrecipitations = this._state.allowedPrecipitations.filter(it => it !== v.precipitation);
else this._state.allowedPrecipitations = [...this._state.allowedPrecipitations, v.precipitation];
});
const hookPrecipitation = () => $btn.toggleClass("active", this._state.allowedPrecipitations.includes(v.precipitation));
this._addHookBase("allowedPrecipitations", hookPrecipitation);
hookPrecipitation();
return $btn;
});
const $btnWindDirection = $(`
`)
.click(async () => {
const bearing = await TimeTrackerUtil.pGetUserWindBearing(this._state.prevailingWindDirection);
if (bearing != null) this._state.prevailingWindDirection = bearing;
});
const hookWindDirection = () => {
$btnWindDirection.html(`
`);
};
this._addHookBase("prevailingWindDirection", hookWindDirection);
hookWindDirection();
const $btnsWindSpeed = TimeTrackerRoot_Clock_Weather._WIND_SPEEDS
.map((it, i) => {
const meta = TimeTrackerRoot_Clock_Weather._WIND_SPEEDS_META[i];
return {
speed: it,
name: TimeTrackerUtil.revSlugToText(it),
iconContent: `
${this._unitsWindSpeed === "mph" ? `${meta.mph} mph` : `${meta.kmph} km/h`}
`,
};
})
.map(v => {
const $btn = $$`
${v.iconContent}
${v.name}
`
.click(() => {
if (this._state.allowedWindSpeeds.includes(v.speed)) this._state.allowedWindSpeeds = this._state.allowedWindSpeeds.filter(it => it !== v.speed);
else this._state.allowedWindSpeeds = [...this._state.allowedWindSpeeds, v.speed];
});
const hookSpeed = () => $btn.toggleClass("active", this._state.allowedWindSpeeds.includes(v.speed));
this._addHookBase("allowedWindSpeeds", hookSpeed);
hookSpeed();
return $btn;
});
const $btnOk = $(`
Confirm and Roll Weather `)
.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: $(`
Browse `),
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 : $(`
\u2012D `)
.click(evt => doModTime(-1 * getTimeInfo().secsPerDay * (evt.shiftKey ? 5 : 1)));
const $btnAddDay = opts.isHideDays ? null : $(`
D+ `)
.click(evt => doModTime(getTimeInfo().secsPerDay * (evt.shiftKey ? 5 : 1)));
const $btnSubWeek = opts.isHideWeeks ? null : $(`
\u2012W `)
.click(evt => doModTime(-1 * getTimeInfo().secsPerWeek * (evt.shiftKey ? 5 : 1)));
const $btnAddWeek = opts.isHideWeeks ? null : $(`
W+ `)
.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 = $(`
\u2012M `)
.click(evt => doModMonths(evt.shiftKey ? -5 : -1));
const $btnAddMonth = $(`
M+ `)
.click(evt => doModMonths(evt.shiftKey ? 5 : 1));
const $btnSubYear = $(`
\u2012Y `)
.click(evt => doModTime(-1 * getTimeInfo().secsPerYear * (evt.shiftKey ? 5 : 1)));
const $btnAddYear = $(`
Y+ `)
.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 = $(`
Go to Day `)
.click(() => {
this._render_doJumpToDay(eventYear, eventDay);
doClose();
});
const $btnAddEvent = $(`
Add Event`)
.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 = $(`
At Time... `)
.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 = $(`
Add Encounter`)
.click(evt => ContextUtil.pOpenMenu(evt, menuEncounter));
const $btnAddEncounterAtTime = $(`
At Time... `)
.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}
Has Time?
${$cbHasTime}
${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}
${$btnAddEvent}${$btnAddEventAtTime}
${$wrpEvents}
${$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 = $(`
Enter `)
.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 $(`
Reset Section `)
.click(() => {
if (!confirm("Are you sure?")) return;
props.forEach(prop => this._parent.set(prop, TimeTrackerBase._DEFAULT_STATE[prop]));
});
};
const $selWindUnits = $(`
Miles per Hour
Kilometres per Hour
`)
.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 = $$`
Show Calendar Column Labels
${ComponentUiUtil.$getCbBool(this._parent.component, "hasCalendarLabelsColumns")}
Show Calendar Row Labels
${ComponentUiUtil.$getCbBool(this._parent.component, "hasCalendarLabelsRows")}
`;
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 = $$`
`;
const $btnHideSectDays = $getBtnHide("isDaysSectionHidden", $sectDays);
const $headDays = $$`
`;
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 = $$`
`;
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 = $$`
`;
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}
Has Time?
${$cbHasTime}
${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: $(`
`)});
const $btnOk = $(`
Save `)
.click(() => doClose(true));
$$`
${$iptName}
${$iptEntries}
${$btnOk}
`.appendTo($modalInner);
}
getState () { return MiscUtil.copy(this._state); }
_getDefaultState () { return MiscUtil.copy(TimeTrackerBase._DEFAULT_STATE__EVENT); }
static getInstance (board, $wrpPanel, parent, event) {
const comp = new TimeTrackerRoot_Settings_Event(board, $wrpPanel);
comp.setStateFrom({state: event});
comp._addHookAll("state", () => {
const otherEvents = Object.values(parent.get("events"))
.filter(it => !(it.isDeleted || it.id === comp.getState().id));
parent.set("events", [...otherEvents, comp.getState()].mergeMap(it => ({[it.id]: it})));
});
return comp;
}
}
class TimeTrackerRoot_Settings_Season extends RenderableCollectionTimeTracker {
getNewRender (entity, i) {
const comp = BaseComponent.fromObject(entity.data, "*");
comp._addHookAll("state", () => {
entity.data = comp.toObject("*");
this._comp._triggerCollectionUpdate("seasons");
});
const $iptName = ComponentUiUtil.$getIptStr(comp, "name", {$ele: $(`
`)});
const $getIptHours = (prop) => ComponentUiUtil.$getIptInt(comp, prop, 0, {$ele: $(`
`), min: 0});
const $getIptDays = (prop) => ComponentUiUtil.$getIptInt(comp, prop, 1, {$ele: $(`
`), offset: 1, min: 1});
const $iptSunrise = $getIptHours("sunriseHour");
const $iptSunset = $getIptHours("sunsetHour");
const $iptDaysStart = $getIptDays("startDay");
const $iptDaysEnd = $getIptDays("endDay");
const $btnRemove = $(`
`)
.click(() => this._comp._state.seasons = this._comp._state.seasons.filter(it => it !== entity));
const $wrpRow = $$`
${$iptName}
${$iptSunrise}
${$iptSunset}
${$iptDaysStart}
${$iptDaysEnd}
${$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_Year extends RenderableCollectionTimeTracker {
getNewRender (entity, i) {
const comp = BaseComponent.fromObject(entity.data, "*");
comp._addHookAll("state", () => {
entity.data = comp.toObject("*");
this._comp._triggerCollectionUpdate("years");
});
const $iptName = ComponentUiUtil.$getIptStr(comp, "name", {$ele: $(`
`)});
const $iptYear = ComponentUiUtil.$getIptInt(comp, "year", 1, {$ele: $(`
`), offset: 1, min: 1});
const $btnRemove = $(`
`)
.click(() => this._comp._state.years = this._comp._state.years.filter(it => it !== entity));
const $wrpRow = $$`
${$iptName}
${$iptYear}
${$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_Era extends RenderableCollectionTimeTracker {
getNewRender (entity, i) {
const comp = BaseComponent.fromObject(entity.data, "*");
comp._addHookAll("state", () => {
entity.data = comp.toObject("*");
this._comp._triggerCollectionUpdate("eras");
});
const $getIptYears = (prop) => ComponentUiUtil.$getIptInt(comp, prop, 1, {$ele: $(`
`), offset: 1, min: 1});
const $iptName = ComponentUiUtil.$getIptStr(comp, "name", {$ele: $(`
`)});
const $iptAbbreviation = ComponentUiUtil.$getIptStr(comp, "abbreviation", {$ele: $(`
`)});
const $iptYearsStart = $getIptYears("startYear");
const $iptYearsEnd = $getIptYears("endYear");
const $btnRemove = $(`
`)
.click(() => this._comp._state.eras = this._comp._state.eras.filter(it => it !== entity));
const $wrpRow = $$`
${$iptName}
${$iptAbbreviation}
${$iptYearsStart}
${$iptYearsEnd}
${$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_Moon extends RenderableCollectionTimeTracker {
getNewRender (entity, i) {
const comp = BaseComponent.fromObject(entity.data, "*");
comp._addHookAll("state", () => {
entity.data = comp.toObject("*");
this._comp._triggerCollectionUpdate("moons");
});
const $iptName = ComponentUiUtil.$getIptStr(comp, "name", {$ele: $(`
`)});
const $iptColor = ComponentUiUtil.$getIptColor(comp, "color", {$ele: $(`
`)});
const $iptPhaseOffset = ComponentUiUtil.$getIptInt(comp, "phaseOffset", 0, {$ele: $(`
`)});
const $iptPeriod = ComponentUiUtil.$getIptInt(comp, "period", 1, {$ele: $(`
`), min: TimeTrackerBase._MIN_TIME, max: TimeTrackerBase._MAX_TIME});
const $btnRemove = $(`
`)
.click(() => this._comp._state.moons = this._comp._state.moons.filter(it => it !== entity));
const $wrpRow = $$`
${$iptName}
${$iptColor}
${$iptPhaseOffset}
${$iptPeriod}
${$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);
}
}