> */
let localizedBadges;
let localizedBadgesIgnoreUpdateTimer = null;
let badgeGameIds = [];
-const assignTooltipCallbacks = new WeakMap;
+const didObserveBadgeCallbacks = new WeakMap;
const badgeGalleryModalContent = document.querySelector('#badgeGalleryModal .modalContent');
/** @type {IntersectionObserver?} */
-let observer;
+let badgeObserver;
let newUnlockBadges = new Set;
@@ -132,7 +136,7 @@ function yieldImmediately() {
return new Promise(resolve => setTimeout(resolve, 0));
}
-async function fetchAndUpdateBadgeModalBadges(slotRow, slotCol, searchMode) { }
+let fetchAndUpdateBadgeModalBadges = async (slotRow, slotCol, searchMode) => {};
function initBadgeControls() {
const badgeModalContent = document.querySelector('#badgesModal .modalContent');
@@ -211,23 +215,23 @@ function initBadgeControls() {
gameBadges = {};
const spacePattern = / /g;
let badgeCount = 0;
- observer?.disconnect();
- observer = new IntersectionObserver(observed => {
+ badgeObserver?.disconnect();
+ badgeObserver = new IntersectionObserver(observed => {
for (const { target } of observed) {
if (!target.parentElement)
continue;
- const assignTooltip = assignTooltipCallbacks.get(target);
- if (assignTooltip) {
- observer.unobserve(target);
- assignTooltipCallbacks.delete(target);
- window.requestAnimationFrame(() => assignTooltip());
+ const didObserveBadge = didObserveBadgeCallbacks.get(target);
+ if (didObserveBadge) {
+ badgeObserver.unobserve(target);
+ didObserveBadgeCallbacks.delete(target);
+ didObserveBadge();
}
}
}, { root: badgeModalContent });
let systemName;
for (const badge of playerBadges) {
// yield back to the game loop to prevent audio cracking
- if (badgeCount++ % 40 === 0) await yieldImmediately();
+ if (badgeCount++ % 350 === 0) await yieldImmediately();
if (!gameBadges[badge.game]) {
if (badge.game !== 'ynoproject') {
@@ -242,8 +246,8 @@ function initBadgeControls() {
if (!gameBadges[badge.game][badge.group])
gameBadges[badge.game][badge.group] = [];
- const item = getBadgeItem(badge, 'lazy', true, true, true, true, systemName);
- observer.observe(item);
+ const item = getBadgeItem(badge, true, true, true, true, true, systemName, true);
+ badgeObserver.observe(item);
if (badge.badgeId === (playerData?.badge || 'null'))
item.children[0].classList.add('selected');
if (!item.classList.contains('disabled')) {
@@ -289,7 +293,7 @@ function initBadgeControls() {
tab.appendChild(tabLabel);
tab.onclick = () => {
- if (badgeGameTabs === 'all') return;
+ if (badgeTabGame === 'all') return;
badgeGameTabs.querySelector('.active')?.classList.remove('active');
tab.classList.add('active');
@@ -393,57 +397,60 @@ function initBadgeControls() {
if ('all' === badgeTabGroup)
return;
- window.requestAnimationFrame(() => {
+ fastdom.mutate(() => {
badgeCategoryTabs.querySelector('.active')?.classList.remove('active');
subTab.classList.add('active');
+ });
- const game = tab.dataset.game;
- if (game !== 'ynoproject') {
- const systemName = getDefaultUiTheme(game).replace(spacePattern, '_');
- applyThemeStyles(nullBadge, systemName, game);
- } else {
- for (const cls of nullBadge.classList)
- if (cls.startsWith('theme_'))
- nullBadge.classList.remove(cls);
- }
+ const game = tab.dataset.game;
+ if (game !== 'ynoproject') {
+ const systemName = getDefaultUiTheme(game).replace(spacePattern, '_');
+ applyThemeStyles(nullBadge, systemName, game);
+ } else {
+ for (const cls of nullBadge.classList)
+ if (cls.startsWith('theme_'))
+ nullBadge.classList.remove(cls);
+ }
+ fastdom.mutate(() => {
badgeModalContent.replaceChildren(nullBadge);
for (const group in gameBadges[game])
badgeModalContent.append(...gameBadges[game][group]);
- badgeTabGame = game;
- badgeTabGroup = 'all';
});
+ badgeTabGame = game;
+ badgeTabGroup = 'all';
};
}
- badgeCategoryTabs.replaceChildren(...subTabs);
- if (badgeCategoryTabs.querySelector('[data-i18n]')) {
- // accommodates translated tooltips in the format of
- locI18next.init(i18next, { ...locI18nextOptions, document: badgeCategoryTabs })('[data-i18n]');
- for (const elm of badgeCategoryTabs.querySelectorAll('[title]')) {
- addTooltip(elm, elm.title, true, !elm.classList.contains('helpLink'));
- elm.removeAttribute('title');
+ fastdom.mutate(() => badgeCategoryTabs.replaceChildren(...subTabs)).then(() => {
+ if (badgeCategoryTabs.querySelector('[data-i18n]')) {
+ // accommodates translated tooltips in the format of
+ locI18next.init(i18next, { ...locI18nextOptions, document: badgeCategoryTabs })('[data-i18n]');
+ for (const elm of badgeCategoryTabs.querySelectorAll('[title]')) {
+ addTooltip(elm, elm.title, true, !elm.classList.contains('helpLink'));
+ elm.removeAttribute('title');
+ }
}
- }
-
- let activeSubTab;
- if (activeSubTab = subTabs.find(tab => tab.classList.contains('active'))) {
- badgeTabGroup = null;
- activeSubTab.click();
- } else {
- updateBadges(game, badgeTabGroup);
- }
+ let activeSubTab;
+ if (activeSubTab = subTabs.find(tab => tab.classList.contains('active'))) {
+ badgeTabGroup = null;
+ activeSubTab.click();
+ } else {
+ updateBadges(game, badgeTabGroup);
+ }
+ });
}; // tab.onclick
}
- badgeGameTabs.replaceChildren(...tabs);
- didUpdateBadgeModal = async () => {
+ let task = fastdom.mutate(() => badgeGameTabs.replaceChildren(...tabs));
+ didUpdateBadgeModal = async (prom) => {
+ await prom;
let activeTab;
if (activeTab = tabs.find(tab => tab.classList.contains('active'))) {
badgeTabGame = null; // temporarily set to null to populate subtabs
activeTab.click();
await updateBadgeVisibility();
if (badgeModalContent.dataset.lastScrollTop)
- badgeModalContent.scrollTo(0, +badgeModalContent.dataset.lastScrollTop);
+ fastdom.mutate(() => badgeModalContent.scrollTo(0, +badgeModalContent.dataset.lastScrollTop));
} else
await updateBadgeVisibility();
removeLoader(document.getElementById('badgesModal'));
@@ -451,7 +458,7 @@ function initBadgeControls() {
if (userSelectedSortOrder)
updateBadgeModalOnly();
else
- await didUpdateBadgeModal();
+ await didUpdateBadgeModal(task);
};
/** Updates badge elements based on a `cacheIndex` assigned to them in {@linkcode getBadgeItem} */
@@ -459,13 +466,15 @@ function initBadgeControls() {
const cacheIndexes = Array.from({ length: badgeFilterCache.length }, (_, i) => i);
cacheIndexes.sort((a, z) => badgeCompareFunc(badgeFilterCache[a], badgeFilterCache[z]));
setTimeout(async () => {
- for (let idx = 0; idx < cacheIndexes.length; idx++)
- badgeFilterCache[cacheIndexes[idx]].el.style.order = idx;
- if (!badgeModalContent.childElementCount)
- for (const game in gameBadges)
- for (const group in gameBadges[game])
- badgeModalContent.append(...gameBadges[game][group]);
- await didUpdateBadgeModal?.();
+ let task = fastdom.mutate(() => {
+ for (let idx = 0; idx < cacheIndexes.length; idx++)
+ badgeFilterCache[cacheIndexes[idx]].el.style.order = idx;
+ if (!badgeModalContent.childElementCount)
+ for (const game in gameBadges)
+ for (const group in gameBadges[game])
+ badgeModalContent.append(...gameBadges[game][group]);
+ });
+ await didUpdateBadgeModal?.(task);
}, 0);
};
@@ -518,7 +527,7 @@ function initBadgeControls() {
openModal('badgesModal', null, prevModal || null);
addLoader(document.getElementById('badgesModal'), true);
- if (!badgeCache.filter(b => !localizedBadges.hasOwnProperty(b.game) || !localizedBadges[b.game].hasOwnProperty(b.badgeId)).length || localizedBadgesIgnoreUpdateTimer)
+ if (!badgeCache.filter(b => localizedBadges?.[b.game]?.hasOwnProperty(b.badgeId)).length || localizedBadgesIgnoreUpdateTimer)
updateBadgesAndPopulateModal(slotRow, slotCol);
else
updateLocalizedBadges(() => updateBadgesAndPopulateModal(slotRow, slotCol));
@@ -552,9 +561,9 @@ function initBadgeControls() {
mapIdToCacheKey[mapId] = key;
}
- return new Promise(resolve => window.requestAnimationFrame(() => {
- badgeModalContent.querySelector('.nullBadgeItem')?.classList.toggle('hidden', exactMatch);
- for (let item of badgeFilterCache) {
+ badgeModalContent.querySelector('.nullBadgeItem')?.classList.toggle('hidden', exactMatch);
+ for (let item of badgeFilterCache) {
+ fastdom.measure(() => {
let visible = true;
if (unlockStatus === 'recentUnlock')
visible &= newUnlockBadges.has(item.badgeId);
@@ -584,11 +593,11 @@ function initBadgeControls() {
}
}
}
- if (!gameVisibilities.hasOwnProperty(item.game)) {
+ if (!(item.game in gameVisibilities)) {
gameVisibilities[item.game] = false;
gameGroupVisibilities[item.game] = {};
}
- if (item.group && !gameGroupVisibilities[item.game].hasOwnProperty(item.group))
+ if (item.group && !(item.group in gameGroupVisibilities[item.game]))
gameGroupVisibilities[item.game][item.group] = false;
if (visible) {
if (!gameVisibilities[item.game])
@@ -596,13 +605,14 @@ function initBadgeControls() {
if (item.group && !gameGroupVisibilities[item.game][item.group])
gameGroupVisibilities[item.game][item.group] = true;
}
- item.el.classList.toggle('hidden', !visible);
- }
+ fastdom.mutate(() => item.el.classList.toggle('hidden', !visible));
+ });
+ }
+ fastdom.mutate(() => {
for (let header of badgeModalContent.querySelectorAll('.itemCategoryHeader'))
header.classList.toggle('hidden', !(header.dataset.group ? gameGroupVisibilities[header.dataset.game][header.dataset.group] : gameVisibilities[header.dataset.game]));
- resolve();
- }));
+ })
};
document.getElementById('badgeUnlockStatus').onchange = updateBadgeVisibility;
@@ -735,13 +745,13 @@ function initBadgeControls() {
/** @param {MouseEvent} ev */
function highlightRemove(ev) {
- if (this.dataset.badgeId === 'null') {
- this.classList.remove('removing');
- return;
- }
- window.requestAnimationFrame(() =>
+ fastdom.mutate(() => {
+ if (this.dataset.badgeId === 'null') {
+ this.classList.remove('removing');
+ return;
+ }
this.classList.toggle('removing', ev.shiftKey && ev.type !== 'mouseleave')
- );
+ });
}
for (let r = 1; r <= maxBadgeSlotRows; r++) {
@@ -881,7 +891,8 @@ function updateBadgeButton() {
const badgeId = playerData?.badge || 'null';
const badge = playerData?.badge ? badgeCache.find(b => b.badgeId === badgeId) : null;
const badgeButton = document.getElementById('badgeButton');
- badgeButton.innerHTML = getBadgeItem(badge || { badgeId: 'null' }, false, true).innerHTML;
+ // badgeButton.innerHTML = getBadgeItem(badge || { badgeId: 'null' }, false, true).innerHTML;
+ badgeButton.replaceChildren(...getBadgeItem(badge || { badgeId: 'null' }, false, true).childNodes)
if (badge?.overlayType & BadgeOverlayType.LOCATION)
handleBadgeOverlayLocationColorOverride(badgeButton.querySelector('.badgeOverlay'), badgeButton.querySelector('.badgeOverlay2'), cachedLocations);
}
@@ -899,7 +910,7 @@ function getBadgeUrl(badge, staticOnly) {
/**
@param {Badge} badge
*/
-function getBadgeItem(badge, includeTooltip, emptyIcon, lockedIcon, scaled, filterable, parsedSystemName) {
+function getBadgeItem(badge, includeTooltip, emptyIcon, lockedIcon, scaled, filterable, parsedSystemName, lazy) {
const badgeId = badge.badgeId;
const item = document.createElement('div');
@@ -930,46 +941,51 @@ function getBadgeItem(badge, includeTooltip, emptyIcon, lockedIcon, scaled, filt
const badgeEl = (badge.unlocked || !badge.secret) && badgeId !== 'null' ? document.createElement('div') : null;
const badgeUrl = badgeEl ? getBadgeUrl(badge, !badge.unlocked) : null;
+ let setBadgeBackgroundImage;
+ let assignTooltip;
+
if (badgeEl) {
badgeEl.classList.add('badge');
if (scaled)
badgeEl.classList.add('scaledBadge');
- badgeEl.style.backgroundImage = `url('${badgeUrl}')`;
+ setBadgeBackgroundImage = () => {
+ badgeEl.style.backgroundImage = `url('${badgeUrl}')`;
- if (badge.overlayType) {
- badgeEl.classList.add('overlayBadge');
+ if (badge.overlayType) {
+ badgeEl.classList.add('overlayBadge');
- const badgeOverlay = document.createElement('div');
- badgeOverlay.classList.add('badgeOverlay');
- if (badge.overlayType & BadgeOverlayType.MULTIPLY)
- badgeOverlay.classList.add('badgeOverlayMultiply');
+ const badgeOverlay = document.createElement('div');
+ badgeOverlay.classList.add('badgeOverlay');
+ if (badge.overlayType & BadgeOverlayType.MULTIPLY)
+ badgeOverlay.classList.add('badgeOverlayMultiply');
- badgeEl.appendChild(badgeOverlay);
+ badgeEl.appendChild(badgeOverlay);
- const badgeMaskValue = badge.overlayType & BadgeOverlayType.MASK
- ? `url('${badgeUrl.replace('.', badge.overlayType & BadgeOverlayType.DUAL ? '_mask_fg.' : '_mask.')}')`
- : badgeEl.style.backgroundImage;
+ const badgeMaskValue = badge.overlayType & BadgeOverlayType.MASK
+ ? `url('${badgeUrl.replace('.', badge.overlayType & BadgeOverlayType.DUAL ? '_mask_fg.' : '_mask.')}')`
+ : badgeEl.style.backgroundImage;
- badgeOverlay.setAttribute('style', `-webkit-mask-image: ${badgeMaskValue}; mask-image: ${badgeMaskValue};`);
+ badgeOverlay.setAttribute('style', `-webkit-mask-image: ${badgeMaskValue}; mask-image: ${badgeMaskValue};`);
- if (badge.overlayType & BadgeOverlayType.DUAL) {
- const badgeMask2Value = badge.overlayType & BadgeOverlayType.MASK
- ? `url(${badgeUrl.replace('.', '_mask_bg.')})`
- : badgeEl.style.backgroundImage;
+ if (badge.overlayType & BadgeOverlayType.DUAL) {
+ const badgeMask2Value = badge.overlayType & BadgeOverlayType.MASK
+ ? `url(${badgeUrl.replace('.', '_mask_bg.')})`
+ : badgeEl.style.backgroundImage;
- badgeOverlay.classList.add('badgeOverlayBase');
+ badgeOverlay.classList.add('badgeOverlayBase');
- const badgeOverlay2 = document.createElement('div');
- badgeOverlay2.classList.add('badgeOverlay', 'badgeOverlay2');
- if (badge.overlayType & BadgeOverlayType.MULTIPLY)
- badgeOverlay2.classList.add('badgeOverlayMultiply');
- badgeOverlay2.classList.add(getStylePropertyValue('--base-color') !== getStylePropertyValue('--alt-color') ? 'badgeOverlayAlt' : 'badgeOverlayBg');
+ const badgeOverlay2 = document.createElement('div');
+ badgeOverlay2.classList.add('badgeOverlay', 'badgeOverlay2');
+ if (badge.overlayType & BadgeOverlayType.MULTIPLY)
+ badgeOverlay2.classList.add('badgeOverlayMultiply');
+ badgeOverlay2.classList.add(getStylePropertyValue('--base-color') !== getStylePropertyValue('--alt-color') ? 'badgeOverlayAlt' : 'badgeOverlayBg');
- badgeEl.appendChild(badgeOverlay2);
+ badgeEl.appendChild(badgeOverlay2);
- badgeOverlay2.setAttribute('style', `-webkit-mask-image: ${badgeMask2Value}; mask-image: ${badgeMask2Value};`);
+ badgeOverlay2.setAttribute('style', `-webkit-mask-image: ${badgeMask2Value}; mask-image: ${badgeMask2Value};`);
+ }
}
- }
+ };
badgeContainer.appendChild(badgeEl);
if (!badge.unlocked) {
@@ -994,7 +1010,7 @@ function getBadgeItem(badge, includeTooltip, emptyIcon, lockedIcon, scaled, filt
if (badgeId === 'null')
tooltipContent = ``;
else {
- if (localizedBadges.hasOwnProperty(badge.game) && localizedBadges[badge.game].hasOwnProperty(badgeId)) {
+ if (localizedBadges.hasOwnProperty(badge.game) && badgeId in localizedBadges[badge.game]) {
let badgeTitle = localizedMessages.badges.locked;
const localizedTooltip = localizedBadges[badge.game][badgeId];
if ((badge.unlocked || !badge.secret) && localizedTooltip.name)
@@ -1055,15 +1071,15 @@ function getBadgeItem(badge, includeTooltip, emptyIcon, lockedIcon, scaled, filt
const baseTooltipContent = tooltipContent;
const tooltipOptions = {};
- const assignTooltip = instance => {
+ const assignTooltipOrDefer = instance => {
const systemName = parsedSystemName;
const assignImmediately = () => {
const badgeTippy = addOrUpdateTooltip(item, tooltipContent, false, false, !!badge.mapId, tooltipOptions, instance);
if (systemName)
applyThemeStyles(badgeTippy.popper.querySelector('.tippy-box'), systemName, badge.game);
- };
- if (includeTooltip === 'lazy')
- assignTooltipCallbacks.set(item, assignImmediately);
+ }
+ if (lazy)
+ assignTooltip = assignImmediately;
else
assignImmediately();
};
@@ -1072,68 +1088,74 @@ function getBadgeItem(badge, includeTooltip, emptyIcon, lockedIcon, scaled, filt
const mapId = badge.mapId.toString().padStart(4, '0');
if (filterItem) filterItem.mapId = mapId;
const setTooltipLocation = instance => {
- if (gameLocalizedMapLocations[badge.game] && gameLocalizedMapLocations[badge.game].hasOwnProperty(mapId))
+ if (badge.game in gameLocalizedMapLocations && mapId in gameLocalizedMapLocations[badge.game])
tooltipContent = baseTooltipContent.replace('{LOCATION}', getLocalizedMapLocationsHtml(badge.game, mapId, '0000', badge.mapX, badge.mapY, getInfoLabel(' | ')));
else if (badge.game === '2kki') {
tooltipContent = baseTooltipContent.replace('{LOCATION}', getInfoLabel(getMassagedLabel(localizedMessages.location.queryingLocation)));
tooltipOptions.onCreate = instance => getOrQuery2kkiLocationsHtml(mapId, locationsHtml => instance.setContent(baseTooltipContent.replace('{LOCATION}', locationsHtml)));
} else
tooltipContent = baseTooltipContent.replace('{LOCATION}', getInfoLabel(getMassagedLabel(localizedMessages.location.unknownLocation)));
- assignTooltip(instance);
+ assignTooltipOrDefer(instance);
};
- if (gameLocalizedMapLocations.hasOwnProperty(badge.game))
+ if (badge.game in gameLocalizedMapLocations)
setTooltipLocation();
else {
tooltipContent = baseTooltipContent.replace('{LOCATION}', getInfoLabel(getMassagedLabel(localizedMessages.location.queryingLocation)));
tooltipOptions.onCreate = instance => {
includeTooltip = true;
- if (gameLocalizedMapLocations.hasOwnProperty(badge.game))
+ if (badge.game in gameLocalizedMapLocations)
setTooltipLocation(instance);
else
fetchAndInitLocations(globalConfig.lang, badge.game).then(() => setTooltipLocation(instance));
};
- assignTooltip();
+ assignTooltipOrDefer();
}
} else
- assignTooltip();
+ assignTooltipOrDefer();
}
}
}
+ if (lazy) {
+ didObserveBadgeCallbacks.set(item, () => {
+ if (setBadgeBackgroundImage)
+ fastdom.mutate(setBadgeBackgroundImage);
+ assignTooltip?.();
+ });
+ } else {
+ setBadgeBackgroundImage?.();
+ assignTooltip?.();
+ }
+
item.appendChild(badgeContainer);
return item;
}
-/**
- @param {(_: BadgeCache) => any}
-*/
async function fetchPlayerBadges() {
- return new Promise(resolve => {
- if (badgeCache?.full) {
- resolve(badgeCache);
- return;
- }
- apiFetch('badge?command=list')
- .then(response => {
- if (!response.ok)
- throw new Error(response.statusText);
- return response.json();
- })
- .then(badges => {
- for (const { badgeId, newUnlock } of badges) {
- if (newUnlock) {
- newUnlockBadges.add(badgeId);
- showBadgeToastMessage('badgeUnlocked', 'info', badgeId);
- }
+ if (badgeCache?.full)
+ return badgeCache;
+ return await apiFetch('badge?command=list')
+ .then(response => {
+ if (!response.ok)
+ throw new Error(response.statusText);
+ return response.json();
+ })
+ .then(badges => {
+ for (const { badgeId, newUnlock } of badges) {
+ if (newUnlock) {
+ newUnlockBadges.add(badgeId);
+ showBadgeToastMessage('badgeUnlocked', 'info', badgeId);
}
- badgeCache = badges;
- badgeCache.full = true;
- resolve(badgeCache);
- })
- .catch(err => console.error(err));
- });
-}
+ }
+ badgeCache = badges;
+ badgeCache.full = true;
+ return badgeCache;
+ }, err => {
+ console.error(err);
+ return badgeCache;
+ })
+};
function updateBadges(callback) {
apiFetch('badge?command=list&simple=true')
@@ -1255,7 +1277,7 @@ function addOrUpdatePlayerBadgeGalleryTooltip(badgeElement, name, sysName, mapId
badgeElement.dataset.systemName = sysName;
if (!badgeElement._badgeGalleryTippy) {
- badgeElement._badgeGalleryTippy = tippy(badgeElement, Object.assign({
+ badgeElement._badgeGalleryTippy = tippy(badgeElement, {
trigger: 'click',
interactive: true,
content: `${getMassagedLabel(localizedMessages.badgeGallery.loading, true)}
`,
@@ -1435,8 +1457,9 @@ function addOrUpdatePlayerBadgeGalleryTooltip(badgeElement, name, sysName, mapId
console.error(err);
instance.setContent('');
});
- }
- }, tippyConfig));
+ },
+ ...tippyConfig,
+ });
}
return badgeElement._badgeGalleryTippy;
diff --git a/chat.js b/chat.js
index 653b51a2..e39d9c40 100644
--- a/chat.js
+++ b/chat.js
@@ -15,13 +15,16 @@ const SCREENSHOT_FLAGS = {
const mentionSe = new Audio('./audio/mention.wav');
-function chatboxAddMessage(msg, type, player, ignoreNotify, mapId, prevMapId, prevLocationsStr, x, y, msgId, timestamp) {
+function chatboxAddMessage(msg, type, player, ignoreNotify, mapId, prevMapId, prevLocationsStr, x, y, msgId, timestamp, shouldScroll = true) {
const messages = document.getElementById("messages");
if (msgId && messages.querySelector(`.messageContainer[data-msg-id="${msgId}"]`))
return null;
+
+ let task = Promise.resolve(shouldScroll);
- const shouldScroll = Math.abs((messages.scrollHeight - messages.scrollTop) - messages.clientHeight) <= 20;
+ if (shouldScroll)
+ task = fastdom.measure(() => Math.abs((messages.scrollHeight - messages.scrollTop) - messages.clientHeight) <= 50);
const msgContainer = document.createElement("div");
msgContainer.classList.add("messageContainer");
@@ -65,7 +68,7 @@ function chatboxAddMessage(msg, type, player, ignoreNotify, mapId, prevMapId, pr
if (showLocation) {
const playerLocation = document.createElement("bdi");
- if (gameId === "2kki" && (!localizedMapLocations || !localizedMapLocations.hasOwnProperty(mapId))) {
+ if (gameId === "2kki" && (!localizedMapLocations.hasOwnProperty(mapId))) {
const prevLocations = prevLocationsStr && prevMapId !== "0000" ? decodeURIComponent(window.atob(prevLocationsStr)).split("|").map(l => { return { title: l }; }) : null;
set2kkiGlobalChatMessageLocation(playerLocation, mapId, prevMapId, prevLocations);
} else {
@@ -273,11 +276,13 @@ function chatboxAddMessage(msg, type, player, ignoreNotify, mapId, prevMapId, pr
const messageContentsWrapper = document.createElement('div');
messageContentsWrapper.classList.add('messageContentsWrapper');
- messageContentsWrapper.appendChild(messageContents);
- messageContentsWrapper.dir = "auto";
- message.appendChild(messageContentsWrapper);
- msgContainer.appendChild(message);
- messages.appendChild(msgContainer);
+ fastdom.mutate(() => {
+ messageContentsWrapper.appendChild(messageContents);
+ messageContentsWrapper.dir = "auto";
+ message.appendChild(messageContentsWrapper);
+ msgContainer.appendChild(message);
+ messages.appendChild(msgContainer);
+ });
if (player)
addGameChatMessage(message.innerHTML, type, uuid);
@@ -329,12 +334,17 @@ function chatboxAddMessage(msg, type, player, ignoreNotify, mapId, prevMapId, pr
tabMessages.shift().remove();
}
- if (shouldScroll)
- messages.scrollTop = messages.scrollHeight;
+ task.then(top => top && scrollChatMessages());
return msgContainer;
}
+function scrollChatMessages() {
+ fastdom.mutate(() => {
+ document.getElementById('messages').scrollTo({ top: Number.MAX_SAFE_INTEGER });
+ });
+}
+
let gameChatModeIndex = 0;
function addGameChatMessage(messageHtml, messageType, senderUuid) {
@@ -630,48 +640,48 @@ function updateChatMessageTimestamps() {
timestamp.innerHTML = getChatMessageTimestampLabel(new Date(parseInt(timestamp.dataset.time)));
}
-function syncChatHistory() {
- return new Promise((resolve, reject) => {
- const messages = document.getElementById("messages");
- const idMessages = messages.querySelectorAll('.messageContainer[data-msg-id]');
- const lastMessageId = idMessages.length ? idMessages[idMessages.length - 1].dataset.msgId : null;
-
- updateChatMessageTimestamps();
-
- apiFetch(`chathistory?globalMsgLimit=${globalConfig.globalChatHistoryLimit}&partyMsgLimit=${globalConfig.partyChatHistoryLimit}${lastMessageId ? `&lastMsgId=${lastMessageId}` : ''}`)
- .then(response => {
- if (!response.ok)
- reject(response.statusText);
- return response.json();
- })
- .then(chatHistory => {
- if (chatHistory.players) {
- for (let player of chatHistory.players) {
- let badge = player.badge;
-
- if (badge === 'null')
- badge = null;
-
- globalPlayerData[player.uuid] = {
- name: player.name,
- systemName: player.systemName,
- rank: player.rank,
- account: player.account,
- badge: badge,
- medals: player.medals
- };
- }
- }
-
- if (chatHistory.messages) {
- for (let message of chatHistory.messages)
- chatboxAddMessage(message.contents, message.party ? MESSAGE_TYPE.PARTY : MESSAGE_TYPE.GLOBAL, message.uuid, true, message.mapId, message.prevMapId, message.prevLocations, message.x, message.y, message.msgId, new Date(message.timestamp));
- }
+async function syncChatHistory() {
+ const messages = document.getElementById("messages");
+ const idMessages = messages.querySelectorAll('.messageContainer[data-msg-id]');
+ const lastMessageId = idMessages.length ? idMessages[idMessages.length - 1].dataset.msgId : null;
+
+ updateChatMessageTimestamps();
+
+ const chatHistory = await apiFetch(`chathistory?globalMsgLimit=${globalConfig.globalChatHistoryLimit}&partyMsgLimit=${globalConfig.partyChatHistoryLimit}${lastMessageId ? `&lastMsgId=${lastMessageId}` : ''}`)
+ .then(response => {
+ if (!response.ok)
+ throw new Error(response.statusText);
+ return response.json();
+ });
+ if (chatHistory.players) {
+ for (let player of chatHistory.players) {
+ let badge = player.badge;
+
+ if (badge === 'null')
+ badge = null;
+
+ globalPlayerData[player.uuid] = {
+ name: player.name,
+ systemName: player.systemName,
+ rank: player.rank,
+ account: player.account,
+ badge: badge,
+ medals: player.medals
+ };
+ }
+ }
- resolve();
- })
- .catch(err => reject(err));
- });
+ let nmessages = 0;
+ if (chatHistory.messages) {
+ const epoch = performance.now();
+ for (let message of chatHistory.messages) {
+ if (nmessages++ % 20 === 0) await yieldImmediately();
+ chatboxAddMessage(message.contents, message.party ? MESSAGE_TYPE.PARTY : MESSAGE_TYPE.GLOBAL, message.uuid, true, message.mapId, message.prevMapId, message.prevLocations, message.x, message.y, message.msgId, new Date(message.timestamp), false);
+ }
+ const total = performance.now() - epoch;
+ console.log({ total, avg: total / chatHistory.messages.length });
+ scrollChatMessages();
+ }
}
const markdownSyntax = [
diff --git a/friends.js b/friends.js
index 2cc957f2..6f3103f0 100644
--- a/friends.js
+++ b/friends.js
@@ -20,7 +20,7 @@ function onUpdatePlayerFriends(playerFriends) {
for (let playerUuid of removedPlayerUuids)
removePlayerListEntry(friendsPlayerList, playerUuid);
- Array.from(friendsPlayerList.querySelectorAll('.listEntryCategoryHeader')).map(h => h.remove());
+ Array.from(friendsPlayerList.querySelectorAll('.listEntryCategoryHeader')).forEach(h => h.remove());
for (let playerFriend of playerFriends) {
const uuid = playerFriend.uuid;
diff --git a/gamecanvas.css b/gamecanvas.css
index c74c6114..aaacc939 100644
--- a/gamecanvas.css
+++ b/gamecanvas.css
@@ -62,11 +62,6 @@ html {
opacity: 0.4;
}
-#apad > * {
- width: var(--controls-size);
- height: var(--controls-size);
-}
-
#apad .apadBtn {
width: var(--controls-size);
height: var(--controls-size);
diff --git a/icons.js b/icons.js
index 0a50f76b..b30f2fca 100644
--- a/icons.js
+++ b/icons.js
@@ -43,7 +43,7 @@ const icons = {
/** @param {keyof typeof icons} iconId */
function getSvgIcon(iconId, fill) {
- if (!icons.hasOwnProperty(iconId))
+ if (!(iconId in icons))
return null;
const icon = document.createElement('div');
diff --git a/index.php b/index.php
index 816d1de5..4bbfef76 100644
--- a/index.php
+++ b/index.php
@@ -2068,6 +2068,8 @@ class="joystickBase" />
+
+
diff --git a/init.js b/init.js
index 7af0dbef..97cbe757 100644
--- a/init.js
+++ b/init.js
@@ -40,9 +40,20 @@ const tippyConfig = {
arrow: false,
animation: 'scale',
allowHTML: true,
- touch: ['hold', 400],
+ touch: /** @type {['hold', number]} */(['hold', 400]),
};
+Object.defineProperty(Object.prototype, 'hasTitle', {
+ enumerable: false,
+ value() {
+ return typeof this === 'string' || 'title' in this;
+ }
+})
+
+Object.defineProperty(String.prototype, 'title', {
+ get() { return this; },
+})
+
let easyrpgPlayer = {
initialized: false,
game: ynoGameId,
@@ -84,10 +95,7 @@ async function injectScripts() {
if (gameId === '2kki') {
gameVersion = document.querySelector('meta[name="2kkiVersion"]')?.content?.replace(' Patch ', 'p');
- //init2kkiFileVersionAppend();
}
- /*if (globalConfig.preloads)
- initPreloads();*/
easyrpgPlayerLoadFuncs.push(() => {
easyrpgPlayer.initialized = true;
@@ -269,11 +277,9 @@ function getSpriteImg(img, spriteData, sprite, idx, frameIdx, width, height, xOf
const data = imageData.data;
const transPixel = data.slice(0, 3);
let yOffset = hasYOffset ? -1 : 0;
- const checkPixelTransparent = isBrave
- ? o => (data[o] === transPixel[0] || data[o] - 1 === transPixel[0]) && (data[o + 1] === transPixel[1] || data[o + 1] - 1 === transPixel[1]) && (data[o + 2] === transPixel[2] || data[o + 2] - 1 === transPixel[2])
- : o => data[o] === transPixel[0] && data[o + 1] === transPixel[1] && data[o + 2] === transPixel[2];
+ const checkPixelTransparent = isBrave ? transparencyChecker.brave : transparencyChecker.default;
for (let i = 0; i < data.length; i += 4) {
- if (checkPixelTransparent(i))
+ if (checkPixelTransparent(data, i, transPixel))
data[i + 3] = 0;
else if (yOffset === -1)
yOffset = Math.max(Math.min(i >> 7, 15), 3);
@@ -299,6 +305,14 @@ function getSpriteImg(img, spriteData, sprite, idx, frameIdx, width, height, xOf
});
}
+let transparencyChecker = {
+ default: (data, o, transPixel) => data[o] === transPixel[0] && data[o + 1] === transPixel[1] && data[o + 2] === transPixel[2],
+ brave: (data, o, transPixel) => (data[o] === transPixel[0] || data[o] - 1 === transPixel[0]) && (data[o + 1] === transPixel[1] || data[o + 1] - 1 === transPixel[1]) && (data[o + 2] === transPixel[2] || data[o + 2] - 1 === transPixel[2]),
+}
+
+/**
+ * @param {Element} target
+ */
function addTooltip(target, content, asTooltipContent, delayed, interactive, options) {
if (!options)
options = {};
@@ -306,12 +320,25 @@ function addTooltip(target, content, asTooltipContent, delayed, interactive, opt
options.interactive = true;
if (delayed)
options.delay = [750, 0];
- options.content = asTooltipContent ? `${content}
` : content;
+ if (!asTooltipContent)
+ options.content = content;
+ else if (content instanceof Element) {
+ const tooltipContent = document.createElement('div');
+ tooltipContent.classList.add('tooltipContent');
+ tooltipContent.appendChild(content);
+ options.content = tooltipContent;
+ } else
+ options.content = `${content}
`;
options.appendTo = document.getElementById('layout');
target._tippy?.destroy();
return tippy(target, Object.assign(options, tippyConfig));
}
+const playerTooltipCache = new Map;
+
+/**
+ * @param {HTMLElement} target
+ */
function addPlayerContextMenu(target, player, uuid, messageType) {
if (!player || uuid === playerData?.uuid || uuid === defaultUuid) {
target.addEventListener('contextmenu', event => event.preventDefault());
@@ -321,10 +348,60 @@ function addPlayerContextMenu(target, player, uuid, messageType) {
if (player && !player.hasOwnProperty('uuid'))
player = Object.assign({ uuid }, player);
+ target.addEventListener('contextmenu', function(event) {
+ event.preventDefault();
+
+ const cacheKey = `${uuid};${messageType}`;
+
+ let playerTooltip = playerTooltipCache.get(cacheKey);
+ if (!playerTooltip) {
+ playerTooltip = createPlayerTooltip(this, player, uuid, messageType);
+ playerTooltipCache.set(cacheKey, playerTooltip);
+ }
+
+ const isFriend = !!playerFriendsCache.find(pf => pf.uuid === uuid);
+
+ const addFriendAction = playerTooltip.popper.querySelector('.addPlayerFriendAction');
+ const removeFriendAction = playerTooltip.popper.querySelector('.removePlayerFriendAction');
+
+ if (addFriendAction)
+ addFriendAction.classList.toggle('hidden', isFriend);
+ if (removeFriendAction)
+ removeFriendAction.classList.toggle('hidden', !isFriend);
+
+ const isBlocked = blockedPlayerUuids.indexOf(uuid) > -1;
+
+ const blockAction = playerTooltip.popper.querySelector('.blockPlayerAction');
+ const unblockAction = playerTooltip.popper.querySelector('.unblockPlayerAction');
+
+ if (blockAction)
+ blockAction.classList.toggle('hidden', isBlocked);
+ if (unblockAction)
+ unblockAction.classList.toggle('hidden', !isBlocked);
+
+ playerTooltip.setProps({
+ getReferenceClientRect: () => ({
+ width: 0,
+ height: 0,
+ top: event.clientY,
+ bottom: event.clientY,
+ left: event.clientX,
+ right: event.clientX,
+ }),
+ });
+
+ playerTooltip.show();
+ });
+}
+
+/**
+ * @param {Element} target
+ */
+function createPlayerTooltip(target, player, uuid, messageType) {
const isMod = playerData?.rank > player?.rank;
const isBlockable = playerData?.rank >= player?.rank;
const playerName = getPlayerName(player, true, false, true);
-
+
let tooltipHtml = '';
if (messageType)
@@ -564,44 +641,12 @@ function addPlayerContextMenu(target, player, uuid, messageType) {
};
});
- target.addEventListener('contextmenu', event => {
- event.preventDefault();
-
- const isFriend = !!playerFriendsCache.find(pf => pf.uuid === uuid);
-
- const addFriendAction = playerTooltip.popper.querySelector('.addPlayerFriendAction');
- const removeFriendAction = playerTooltip.popper.querySelector('.removePlayerFriendAction');
-
- if (addFriendAction)
- addFriendAction.classList.toggle('hidden', isFriend);
- if (removeFriendAction)
- removeFriendAction.classList.toggle('hidden', !isFriend);
-
- const isBlocked = blockedPlayerUuids.indexOf(uuid) > -1;
-
- const blockAction = playerTooltip.popper.querySelector('.blockPlayerAction');
- const unblockAction = playerTooltip.popper.querySelector('.unblockPlayerAction');
-
- if (blockAction)
- blockAction.classList.toggle('hidden', isBlocked);
- if (unblockAction)
- unblockAction.classList.toggle('hidden', !isBlocked);
-
- playerTooltip.setProps({
- getReferenceClientRect: () => ({
- width: 0,
- height: 0,
- top: event.clientY,
- bottom: event.clientY,
- left: event.clientX,
- right: event.clientX,
- }),
- });
-
- playerTooltip.show();
- });
+ return playerTooltip;
}
+/**
+ * @param {import('tippy.js').Instance} [instance]
+ */
function addOrUpdateTooltip(target, content, asTooltipContent, delayed, interactive, options, instance) {
if (!instance)
return addTooltip(target, content, asTooltipContent, delayed, interactive, options);
diff --git a/loader.js b/loader.js
index 337368e5..202e860e 100644
--- a/loader.js
+++ b/loader.js
@@ -8,6 +8,8 @@ function addLoader(target, instant) {
if (activeLoaders.has(target))
return;
activeLoaders.set(target, true);
+ // TODO: optimize
+ const targetPosition = getComputedStyle(target).position;
const getLoaderSprites = [
getLoaderSpriteImg(playerLoaderSprite, playerLoaderSpriteIdx, 0),
@@ -23,7 +25,6 @@ function addLoader(target, instant) {
const el = document.createElement('div');
el.classList.add('loader');
- const targetPosition = getComputedStyle(target).position;
switch (targetPosition) {
case 'fixed':
case 'relative':
@@ -85,16 +86,19 @@ function addLoader(target, instant) {
function updateLoader(target) {
if (activeLoaders.has(target)) {
const el = activeLoaders.get(target).element;
- el.style.top = `${target.offsetTop}px`;
- el.style.left = `${target.offsetLeft}px`;
- el.style.width = `${target.offsetWidth}px`;
- el.style.height = `${target.offsetHeight}px`;
-
- const scaleX = Math.max(Math.min(Math.floor(target.offsetWidth / 48), 10), 1);
- const scaleY = Math.max(Math.min(Math.floor(target.offsetHeight / 64), 10), 1);
- const scale = Math.min(scaleX, scaleY);
-
- el.children[0].style.transform = `scale(${scale})`;
+ fastdom.measure(() => {
+ const {offsetTop, offsetLeft, offsetWidth, offsetHeight} = target;
+ const scaleX = Math.max(Math.min(Math.floor(offsetWidth / 48), 10), 1);
+ const scaleY = Math.max(Math.min(Math.floor(offsetHeight / 64), 10), 1);
+ const scale = Math.min(scaleX, scaleY);
+ fastdom.mutate(() => {
+ el.style.top = `${offsetTop}px`;
+ el.style.left = `${offsetLeft}px`;
+ el.style.width = `${offsetWidth}px`;
+ el.style.height = `${offsetHeight}px`;
+ el.children[0].style.transform = `scale(${scale})`;
+ });
+ });
}
}
@@ -107,8 +111,6 @@ function removeLoader(target) {
clearTimeout(longTimer);
}
activeLoaders.delete(target);
- if (typeof loadingMessageTimer !== 'undefined')
- clearTimeout(loadingMessageTimer);
}
async function getLoaderSpriteImg(sprite, idx, frameIdx, dir) {
diff --git a/parties.js b/parties.js
index 04542826..6def21e4 100644
--- a/parties.js
+++ b/parties.js
@@ -443,7 +443,7 @@ function addOrUpdatePartyListEntry(party) {
const isInParty = party.id === joinedPartyId;
const partyList = document.getElementById('partyList');
- let partyListEntry = document.querySelector(`.partyListEntry[data-id="${party.id}"]`);
+ let partyListEntry = partyList.querySelector(`.partyListEntry[data-id="${party.id}"]`);
const partyListEntrySprite = partyListEntry ? partyListEntry.querySelector('.partyListEntrySprite') : document.createElement('img');
const nameText = partyListEntry ? partyListEntry.querySelector('.nameText') : document.createElement('span');
@@ -633,9 +633,7 @@ function addOrUpdatePartyListEntry(party) {
}
function removePartyListEntry(id) {
- const partyListEntry = document.querySelector(`.partyListEntry[data-id="${id}"]`);
- if (partyListEntry)
- partyListEntry.remove();
+ document.getElementById('partyList').querySelector(`.partyListEntry[data-id="${id}"]`)?.remove();
}
function clearPartyList() {
diff --git a/play.js b/play.js
index 23a80989..3ecfe673 100644
--- a/play.js
+++ b/play.js
@@ -1,35 +1,47 @@
/**
- * @typedef {Object} MapTitle
+ * @typedef {object} Coords
+ * @property {number} x1
+ * @property {number} x2
+ * @property {number} y1
+ * @property {number} y2
+ */
+
+/**
+ * @typedef {object} MapTitle
* @property {string} title
* @property {string} [urlTitle]
- * @property {Object} [coords]
- * @property {number} coords.x1
- * @property {number} coords.x2
- * @property {number} coords.y1
- * @property {number} coords.y2
+ * @property {Coords} [coords]
+ * @property {boolean} [explorer]
*/
/**
- * @typedef {string | MapTitle | (string | MapTitle)[] | Record<'else' | (string & {}), MapTitle>} MapDescriptor
+ * @typedef {string | MapTitle | (string | MapTitle)[] | Record<'else' | number | `0${number}`, string | MapTitle | (string | MapTitle)[]>} MapDescriptor
* In the array form, the last element is customarily the fallback title.
*
* The third object form allows matching the correct world for map IDs shared between worlds:
- * a mapping from the previous map ID Urotsuki was on to, to the matching map title.
+ * a mapping from the previous map ID the player came from, to the matching map title.
+ */
+
+/**
+ * @typedef {Record>} MapDescriptorRecord
+ * game -> map-id
*/
let localizedVersion;
-/** @type {import('./lang/en.json')['messages']} */
+/** @type {import('./lang/en.json')['messages']?} */
let localizedMessages;
+/** @type {Record?} */
let localizedMapLocations;
+/** @type {Record?} */
let mapLocations;
let localizedLocationUrlRoot;
let locationUrlRoot;
-/** @type {Record>} */
+/** @type {MapDescriptorRecord} */
let gameLocalizedMapLocations = {};
-/** @type {Record>} */
+/** @type {MapDescriptorRecord} */
let gameMapLocations = {};
let gameLocalizedLocationUrlRoots = {};
let gameLocationUrlRoots = {};
@@ -374,8 +386,13 @@ function checkUpdateLocation(mapId, mapChanged) {
if (!cachedMapId)
document.getElementById('location').classList.remove('hidden');
- document.getElementById('locationText').innerHTML = getLocalizedMapLocationsHtml(gameId, mapId, cachedMapId, tpX, tpY, '
');
- document.documentElement.style.setProperty('--location-width', `${document.querySelector('#locationText > *').offsetWidth}px`);
+ const localizedLocationHtml = getLocalizedMapLocationsHtml(gameId, mapId, cachedMapId, tpX, tpY, '
');
+ fastdom.mutate(() => document.getElementById('locationText').innerHTML = localizedLocationHtml).then(() => {
+ fastdom.measure(() => {
+ const width = `${document.querySelector('#locationText > *').offsetWidth}px`;
+ fastdom.mutate(() => document.getElementById('nextLocationContainer').style.setProperty('--location-width', width));
+ })
+ });
onUpdateChatboxInfo();
if (is2kki) {
@@ -586,9 +603,9 @@ function preToggle(buttonElement) {
/**
* Opens a modal.
* @param {string} modalId The modal's ID.
- * @param {string} theme The theme for the modal to use. Player-selected or in-game menu theme is used if none is specified.
- * @param {string} lastModalId The previously-opened modal, used when opening sub-modals.
- * @param {Object} modalData Data to be passed to the modal.
+ * @param {string} [theme] The theme for the modal to use. Player-selected or in-game menu theme is used if none is specified.
+ * @param {string} [lastModalId] The previously-opened modal, used when opening sub-modals.
+ * @param {Object} [modalData] Data to be passed to the modal.
*/
function openModal(modalId, theme, lastModalId, modalData) {
const modalContainer = document.getElementById('modalContainer');
@@ -1460,32 +1477,45 @@ function updateYnomojiContainerPos(isScrollUpdate, chatInput) {
function onUpdateChatboxInfo() {
const layout = document.getElementById('layout');
-
const chatboxContainer = document.getElementById('chatboxContainer');
const chatboxInfo = document.getElementById('chatboxInfo');
const chatboxTabs = document.getElementsByClassName('chatboxTab');
-
- const backgroundSize = chatboxContainer.classList.contains('fullBg') ? window.getComputedStyle(chatboxContainer).backgroundSize : null;
-
- for (let tab of chatboxTabs) {
- tab.style.backgroundSize = backgroundSize;
- tab.style.backgroundPositionX = `${-8 + tab.parentElement.offsetLeft - tab.getBoundingClientRect().left}px`;
- tab.style.backgroundPositionY = `${chatboxContainer.offsetTop - tab.parentElement.getBoundingClientRect().top}px`;
- }
-
const messages = document.getElementById('messages');
const partyPlayerList = document.getElementById('partyPlayerList');
- messages.style.backgroundPositionY = partyPlayerList.style.backgroundPositionY = `${chatboxContainer.offsetTop - partyPlayerList.getBoundingClientRect().top}px`;
- if (!layout.classList.contains('immersionMode') && !document.fullscreenElement && window.getComputedStyle(layout).flexWrap === 'wrap') {
+ fastdom.measure(() => {
+ const backgroundSize = chatboxContainer.classList.contains('fullBg') ? getComputedStyle(chatboxContainer).backgroundSize : null;
+ const backgroundPositionY = `${chatboxContainer.offsetTop - partyPlayerList.getBoundingClientRect().top}px`;
+ const hasFlexWrap = getComputedStyle(layout).flexWrap === 'wrap';
const lastTab = chatboxTabs[chatboxTabs.length - 1];
- const offsetLeft = `${(lastTab.offsetLeft + lastTab.offsetWidth) - 24}px`;
- chatboxInfo.style.marginInlineStart = offsetLeft;
- chatboxInfo.style.marginBottom = '-32px';
- if (chatboxInfo.offsetHeight >= 72)
- chatboxInfo.setAttribute('style', '');
- } else
- chatboxInfo.setAttribute('style', '');
+ const lastTabOffset = lastTab.offsetLeft + lastTab.offsetRight;
+
+ fastdom.mutate(() => {
+ for (let tab of chatboxTabs) {
+ tab.style.backgroundSize = backgroundSize;
+ fastdom.measure(() => {
+ const posx = `${-8 + tab.parentElement.offsetLeft - tab.getBoundingClientRect().left}px`;
+ const posy = `${chatboxContainer.offsetTop - tab.parentElement.getBoundingClientRect().top}px`;
+ fastdom.mutate(() => {
+ tab.style.backgroundPositionX = posx;
+ tab.style.backgroundPositionY = posy;
+ });
+ });
+ }
+
+ messages.style.backgroundPositionY = partyPlayerList.style.backgroundPositionY = backgroundPositionY;
+ if (!layout.classList.contains('immersionMode') && !document.fullscreenElement && hasFlexWrap) {
+ const offsetLeft = `${lastTabOffset - 24}px`;
+ chatboxInfo.style.marginInlineStart = offsetLeft;
+ chatboxInfo.style.marginBottom = '-32px';
+ fastdom.measure(() => {
+ if (chatboxInfo.offsetHeight >= 72)
+ fastdom.mutate(() => chatboxInfo.setAttribute('style', ''));
+ })
+ } else
+ chatboxInfo.setAttribute('style', '');
+ });
+ });
}
function isOverflow(scale) {
@@ -1902,11 +1932,11 @@ function initLocalizedLocations(game) {
gameLocationsMap[game][mapLocation.title] = mapLocation;
};
- for (let mapId of Object.keys(gameMapLocations[game])) {
+ for (let mapId in gameMapLocations[game]) {
const locations = gameMapLocations[game][mapId];
if (!locations)
continue;
- if (locations.hasOwnProperty('title')) // Text location
+ if (locations.hasTitle()) // Text location
trySetLocalizedLocation(locations, gameLocalizedMapLocations[game][mapId]);
else if (Array.isArray(locations)) // Multiple locations
locations.forEach((location, i) => trySetLocalizedLocation(location, gameLocalizedMapLocations[game][mapId][i]));
@@ -1915,7 +1945,7 @@ function initLocalizedLocations(game) {
const locationsInner = gameMapLocations[game][mapId][key];
if (!locationsInner)
continue;
- if (locationsInner.hasOwnProperty('title'))
+ if (locationsInner.hasTitle())
trySetLocalizedLocation(locationsInner, gameLocalizedMapLocations[game][mapId][key]);
else
locationsInner.forEach((location, i) => trySetLocalizedLocation(location, gameLocalizedMapLocations[game][mapId][key][i]));
@@ -1924,29 +1954,36 @@ function initLocalizedLocations(game) {
}
}
+/**
+ * @param {Record} mapLocations
+ * @return {MapTitle[]}
+ */
function getMapLocationsArray(mapLocations, mapId, prevMapId, x, y) {
- if (mapLocations.hasOwnProperty(mapId)) {
+ if (mapId in mapLocations) {
const locations = mapLocations[mapId];
- if (locations.hasOwnProperty('title')) // Text location
+ if (locations.hasTitle()) // Text location
return [ locations ];
if (Array.isArray(locations)) // Multiple locations
return getMapLocationsFromArray(locations, x, y);
- if (locations.hasOwnProperty(prevMapId)) {// Previous map ID matches a key
+ if (prevMapId in locations) {// Previous map ID matches a key
if (Array.isArray(locations[prevMapId]))
return getMapLocationsFromArray(locations[prevMapId], x, y);
return [ locations[prevMapId] ];
}
- if (locations.hasOwnProperty('else')) { // Else case
- if (locations.else.hasOwnProperty('title'))
+ if ('else' in locations) { // Else case
+ if (locations.else.hasTitle())
return [ locations.else ];
- if (Array.isArray(locations.else))
- return getMapLocationsFromArray(locations.else, x, y);
+ return getMapLocationsFromArray(locations.else, x, y);
}
}
}
+/**
+ * @param {MapTitle[]} locations
+ * @return {MapTitle[]}
+ */
function getMapLocationsFromArray(locations, x, y) {
- if (locations.length && locations[0].hasOwnProperty('coords') && x !== null && y !== null) {
+ if (Array.isArray(locations) && locations[0].hasOwnProperty('coords') && x !== null && y !== null) {
const coordLocation = locations.find(l => l.hasOwnProperty('coords') && ((l.coords.x1 === -1 && l.coords.x2 === -1) || (l.coords.x1 <= x && l.coords.x2 >= x)) && ((l.coords.y1 === -1 && l.coords.y2 === -1) || (l.coords.y1 <= y && l.coords.y2 >= y)));
if (coordLocation)
return [ coordLocation ];
@@ -1958,47 +1995,50 @@ function getMapLocationsFromArray(locations, x, y) {
}
function getLocalizedMapLocations(game, mapId, prevMapId, x, y, separator, forDisplay) {
- if (gameLocalizedMapLocations[game]?.hasOwnProperty(mapId)) {
- const localizedLocations = gameLocalizedMapLocations[game][mapId];
+ const localizedLocations = gameLocalizedMapLocations[game]?.[mapId];
+ if (localizedLocations) {
+ // locations have the same type as localizedLocations
const locations = gameMapLocations[game][mapId];
- if (localizedLocations.hasOwnProperty('title')) // Text location
+ if (localizedLocations.hasTitle()) // Text location
return getLocalizedLocation(game, localizedLocations, locations, false, forDisplay);
if (Array.isArray(localizedLocations)) // Multiple locations
return getMapLocationsFromArray(localizedLocations, x, y).map((l, i) => getLocalizedLocation(game, l, getMapLocationsFromArray(locations, x, y)[i], false, forDisplay)).join(separator);
- if (localizedLocations.hasOwnProperty(prevMapId)) { // Previous map ID matches a key
+ if (prevMapId in localizedLocations) { // Previous map ID matches a key
if (Array.isArray(localizedLocations[prevMapId]))
return getMapLocationsFromArray(localizedLocations[prevMapId], x, y).map((l, i) => getLocalizedLocation(game, l, getMapLocationsFromArray(locations[prevMapId], x, y)[i], false, forDisplay)).join(separator);
return getLocalizedLocation(game, localizedLocations[prevMapId], locations[prevMapId]);
}
- if (localizedLocations.hasOwnProperty('else')) { // Else case
- if (localizedLocations.else.hasOwnProperty('title'))
+ if ('else' in localizedLocations) { // Else case
+ if (localizedLocations.else.hasTitle())
return getLocalizedLocation(game, localizedLocations.else, locations.else, false, forDisplay);
- if (Array.isArray(localizedLocations.else))
- return getMapLocationsFromArray(localizedLocations.else, x, y).map((l, i) => getLocalizedLocation(game, l, getMapLocationsFromArray(locations.else, x, y)[i], false, forDisplay)).join(separator);
+ return getMapLocationsFromArray(localizedLocations.else, x, y).map((l, i) => getLocalizedLocation(game, l, getMapLocationsFromArray(locations.else, x, y)[i], false, forDisplay)).join(separator);
}
}
return localizedMessages.location.unknownLocation;
}
+/**
+ * @param {number | `0${number}`} prevMapId
+ */
function getLocalizedMapLocationsHtml(game, mapId, prevMapId, x, y, separator) {
if (gameLocalizedMapLocations[game]?.hasOwnProperty(mapId)) {
const localizedLocations = gameLocalizedMapLocations[game][mapId];
const locations = gameMapLocations[game][mapId];
let locationsHtml;
- if (localizedLocations.hasOwnProperty('title')) // Text location
+ if (localizedLocations.hasTitle()) // Text location
locationsHtml = getLocalizedLocation(game, localizedLocations, locations, true);
else if (Array.isArray(localizedLocations)) // Multiple locations
locationsHtml = getMapLocationsFromArray(localizedLocations, x, y).map((l, i) => getLocalizedLocation(game, l, getMapLocationsFromArray(locations, x, y)[i], true)).join(separator);
- else if (localizedLocations.hasOwnProperty(prevMapId)) { // Previous map ID matches a key
+ else if (prevMapId in localizedLocations) { // Previous map ID matches a key
if (Array.isArray(localizedLocations[prevMapId]))
locationsHtml = getMapLocationsFromArray(localizedLocations[prevMapId], x, y).map((l, i) => getLocalizedLocation(game, l, getMapLocationsFromArray(locations[prevMapId], x, y)[i], true)).join(separator);
else
locationsHtml = getLocalizedLocation(game, localizedLocations[prevMapId], locations[prevMapId], true);
- } else if (localizedLocations.hasOwnProperty('else')) { // Else case
- if (localizedLocations.else.hasOwnProperty('title'))
+ } else if ('else' in localizedLocations) { // Else case
+ if (localizedLocations.else.hasTitle())
locationsHtml = getLocalizedLocation(game, localizedLocations.else, locations.else, true);
- else if (Array.isArray(localizedLocations.else))
+ else
locationsHtml = getMapLocationsFromArray(localizedLocations.else, x, y).map((l, i) => getLocalizedLocation(game, l, getMapLocationsFromArray(locations.else, x, y)[i], true)).join(separator);
}
@@ -2020,7 +2060,7 @@ function massageMapLocations(mapLocations, locationUrlTitles) {
}
}
} else {
- if (mapLocations.hasOwnProperty('title')) {
+ if (mapLocations.hasTitle()) {
if (locationUrlTitles?.hasOwnProperty(mapLocations.title))
mapLocations.urlTitle = locationUrlTitles[mapLocations.title];
return;
@@ -2037,6 +2077,10 @@ function massageMapLocations(mapLocations, locationUrlTitles) {
}
}
+/**
+ * @param {MapTitle} location
+ * @param {MapTitle} locationEn
+ */
function getLocalizedLocation(game, location, locationEn, asHtml, forDisplay) {
let template = getMassagedLabel(localizedMessages[forDisplay ? 'locationDisplay' : 'location'].template);
let ret;
@@ -2095,7 +2139,7 @@ function massageLabels(data) {
function getMassagedLabel(label, isUI) {
if (label)
- label = label.replace(/\n/g, '
');
+ label = label.replaceAll('\n', '
');
if (langLabelMassageFunctions.hasOwnProperty(globalConfig.lang) && label)
return langLabelMassageFunctions[globalConfig.lang](label, isUI);
return label;
@@ -2151,9 +2195,9 @@ function getMapButton(url, label) {
function getOrQueryLocationColors(locationName) {
return new Promise((resolve, _reject) => {
- if (Array.isArray(locationName) && locationName.length && locationName[0].hasOwnProperty('title'))
+ if (Array.isArray(locationName) && locationName.length && locationName[0].hasTitle())
locationName = locationName[0].title;
- else if (locationName?.hasOwnProperty('title'))
+ else if (locationName?.hasTitle())
locationName = locationName.title;
else if (!locationName) {
resolve(['#FFFFFF', '#FFFFFF']);
@@ -2169,7 +2213,7 @@ function getOrQueryLocationColors(locationName) {
if (gameId === '2kki') {
const url = `${apiUrl}/2kki?action=getLocationColors&locationName=${locationName}`;
- const callback = response => {
+ send2kkiApiRequest(url, response => {
let errCode = null;
if (response && !response.err_code)
@@ -2181,8 +2225,7 @@ function getOrQueryLocationColors(locationName) {
console.error({ error: response.error, errCode: errCode });
resolve([response?.fgColor, response?.bgColor]);
- };
- send2kkiApiRequest(url, callback);
+ });
} else {
sendSessionCommand('lcol', [ locationName ], params => {
if (params.length === 2) {
diff --git a/playerlist.js b/playerlist.js
index f5c2ea06..bdbaa8ad 100644
--- a/playerlist.js
+++ b/playerlist.js
@@ -159,10 +159,11 @@ function getPlayerName(player, includeMarkers, includeBadge, asHtml) {
function addOrUpdatePlayerListEntry(playerList, player, showLocation, sortEntries) {
const uuid = player.uuid === defaultUuid ? playerData?.uuid || defaultUuid : player.uuid;
const name = player.name;
+ /** @type {string} */
let systemName = player.systemName;
let playerGameId = player.game || gameId;
- if (!allGameUiThemes.hasOwnProperty(playerGameId))
+ if (!(playerGameId in allGameUiThemes))
playerGameId = gameId;
if (!playerList)
@@ -262,9 +263,9 @@ function addOrUpdatePlayerListEntry(playerList, player, showLocation, sortEntrie
playerList.scrollTop = playerList.scrollHeight;
}
- if (player.hasOwnProperty('spriteName')) {
- getSpriteProfileImg(player.spriteName, player.hasOwnProperty('spriteIndex') ? player.spriteIndex : 0, undefined, undefined, playerGameId).then(spriteImg => {
- if (spriteImg)
+ if ('spriteName' in player) {
+ getSpriteProfileImg(player.spriteName, player.spriteIndex || 0, undefined, undefined, playerGameId).then(spriteImg => {
+ if (spriteImg && playerListEntrySprite.src !== spriteImg)
playerListEntrySprite.src = spriteImg
});
} else {
@@ -273,7 +274,7 @@ function addOrUpdatePlayerListEntry(playerList, player, showLocation, sortEntrie
playerSpriteCacheEntry = playerSpriteCache[defaultUuid];
if (playerSpriteCacheEntry) {
getSpriteProfileImg(playerSpriteCacheEntry.sprite, playerSpriteCacheEntry.idx, undefined, undefined, playerGameId).then(spriteImg => {
- if (spriteImg)
+ if (spriteImg && playerListEntrySprite.src !== spriteImg)
playerListEntrySprite.src = spriteImg
});
if (uuid === defaultUuid)
@@ -505,10 +506,10 @@ function addOrUpdatePlayerListEntry(playerList, player, showLocation, sortEntrie
playerListEntryMedals.classList.toggle('hidden', !showMedals);
if (systemName) {
- systemName = systemName.replace(/'/g, '');
- if (playerListEntry.dataset.unnamed || !allGameUiThemes.hasOwnProperty(playerGameId) || allGameUiThemes[playerGameId].indexOf(systemName) === -1)
+ systemName = systemName.replaceAll("'", '');
+ if (playerListEntry.dataset.unnamed || !(playerGameId in allGameUiThemes) || allGameUiThemes[playerGameId].indexOf(systemName) === -1)
systemName = getDefaultUiTheme(playerGameId);
- const parsedSystemName = systemName.replace(/ /g, '_');
+ const parsedSystemName = systemName.replaceAll(' ', '_');
initUiThemeContainerStyles(systemName, playerGameId, false, () => {
initUiThemeFontStyles(systemName, playerGameId, 0, false, () => {
const gameParsedSystemName = `${playerGameId !== gameId ? `${playerGameId}-` : ''}${parsedSystemName}`;
@@ -547,7 +548,7 @@ function addOrUpdatePlayerListEntryLocation(locationVisible, player, entry) {
let playerLocation = entry.querySelector('.playerLocation');
const initLocation = !playerLocation;
const isValidMap = !!parseInt(player.mapId);
- const showLastOnline = player.hasOwnProperty('online') && !player.online && player.hasOwnProperty('lastActive') && player.lastActive;
+ const showLastOnline = !player.online && player.lastActive;
if (initLocation) {
playerLocation = document.createElement('small');
@@ -560,12 +561,12 @@ function addOrUpdatePlayerListEntryLocation(locationVisible, player, entry) {
playerLocation.classList.toggle('hidden', (!locationVisible || !shouldDisplayLocation) && !showLastOnline);
if (locationVisible && player.online && shouldDisplayLocation) {
- if (!allGameUiThemes.hasOwnProperty(playerGameId))
+ if (!(playerGameId in allGameUiThemes))
playerGameId = gameId;
const parsedSystemName = player.systemName ? (allGameUiThemes[playerGameId].indexOf(player.systemName) > -1 ? player.systemName : getDefaultUiTheme(playerGameId)).replace(/ /g, '_') : null;
playerLocation.dataset.systemOverride = parsedSystemName || null;
if (playerGameId === gameId) {
- if (gameId === '2kki' && (!localizedMapLocations || !localizedMapLocations.hasOwnProperty(player.mapId))) {
+ if (gameId === '2kki' && (!localizedMapLocations?.hasOwnProperty(player.mapId))) {
const prevLocations = player.prevLocations && player.prevMapId !== '0000' ? decodeURIComponent(window.atob(player.prevLocations)).split('|').map(l => { return { title: l }; }) : null;
set2kkiGlobalChatMessageLocation(playerLocation, player.mapId, player.prevMapId, prevLocations);
} else {
@@ -580,8 +581,9 @@ function addOrUpdatePlayerListEntryLocation(locationVisible, player, entry) {
applyThemeStyles(playerLocation, playerLocation.dataset.systemOverride, playerGameId);
}
} else if (showLastOnline) {
- if (parseInt(getLastOnlineInterval(new Date(player.lastActive))) < 5000)
- playerLocation.innerHTML = `${getMassagedLabel(localizedMessages.lastOnline.template).replace('{INTERVAL}', getLastOnlineInterval(new Date(player.lastActive)))}`;
+ const lastActive = getLastOnlineInterval(new Date(player.lastActive));
+ if (parseInt(lastActive) < 5000)
+ playerLocation.innerHTML = `${getMassagedLabel(localizedMessages.lastOnline.template).replace('{INTERVAL}', lastActive)}`;
else
playerLocation.innerHTML = `${getMassagedLabel(localizedMessages.lastOnline.longTime)}`;
if (playerLocation.dataset.systemOverride)
@@ -616,9 +618,8 @@ function sortPlayerListEntries(playerList) {
return a.innerText.localeCompare(b.innerText, { sensitivity: 'base' });
});
- entries.forEach(function (ple) {
- playerList.appendChild(ple);
- });
+ for (const ple of entries)
+ playerList.appendChild(ple);
}
}
diff --git a/screenshots.js b/screenshots.js
index 55b484a7..e23f8a7a 100644
--- a/screenshots.js
+++ b/screenshots.js
@@ -286,7 +286,12 @@ function takeScreenshot() {
thumb.src = url;
toast.querySelector('.toastMessage').prepend(thumb);
toast.classList.add('screenshotToast');
- document.documentElement.style.setProperty('--toast-offset', `-${toast.getBoundingClientRect().height + 8}px`);
+ fastdom.measure(() => {
+ const height = toast.getBoundingClientRect().height + 8;
+ fastdom.mutate(() => {
+ document.getElementById('toastContainer').style.setProperty('--toast-offset', `-${height}px`);
+ });
+ });
thumb.onclick = () => viewScreenshot(url, dateTaken, { mapId, mapX, mapY });
@@ -452,15 +457,11 @@ function initScreenshotsModal(isCommunity) {
locationContent = getInfoLabel(getMassagedLabel(localizedMessages.location.unknownLocation));
screenshotLocation.innerHTML = locationContent;
};
- if (gameLocalizedMapLocations.hasOwnProperty(screenshot.game))
+ if (screenshot.game in gameLocalizedMapLocations)
setScreenshotLocation();
else {
- locationContent = getInfoLabel(getMassagedLabel(localizedMessages.location.queryingLocation));
- if (gameLocalizedMapLocations.hasOwnProperty(screenshot.game))
- setScreenshotLocation();
- else
- fetchAndInitLocations(globalConfig.lang, screenshot.game).then(() => setScreenshotLocation());
- screenshotLocation.innerHTML = locationContent;
+ screenshotLocation.innerHTML = getInfoLabel(getMassagedLabel(localizedMessages.location.queryingLocation));
+ fetchAndInitLocations(globalConfig.lang, screenshot.game).then(() => setScreenshotLocation());
}
}
diff --git a/session.js b/session.js
index 23b2a5ca..6844b4dc 100644
--- a/session.js
+++ b/session.js
@@ -9,7 +9,7 @@ let hasConnected;
function initSessionWs(attempt) {
return new Promise(resolve => {
if (sessionWs)
- closeSessionWs(sessionWs);
+ closeSessionWs();
if (config.singlePlayer) {
resolve();
return;
@@ -50,7 +50,7 @@ function initSessionWs(attempt) {
sessionWs.onmessage = event => {
const args = event.data.split(wsDelim);
const command = args[0];
- if (sessionCommandHandlers.hasOwnProperty(command)) {
+ if (command in sessionCommandHandlers) {
const params = args.slice(1);
if (sessionCommandHandlers[command])
sessionCommandHandlers[command](params);
@@ -90,4 +90,4 @@ function sendSessionCommand(command, commandParams, callbackFunc, callbackComman
}
sessionWs.send(args.join(wsDelim));
-}
\ No newline at end of file
+}
diff --git a/system.js b/system.js
index 8854caf4..59f70581 100644
--- a/system.js
+++ b/system.js
@@ -464,19 +464,23 @@ function initUiThemeContainerStyles(uiTheme, themeGameId, setTheme, callback) {
if (!rootStyle.getPropertyValue(baseBgColorProp)) {
addSystemSvgDropShadow(uiTheme, themeGameId, shadow);
- rootStyle.setProperty(baseBgColorProp, color);
- rootStyle.setProperty(shadowColorProp, shadow);
- rootStyle.setProperty(svgShadowProp, `url(#dropShadow_${themeGameId !== gameId ? `${themeGameId}_` : ''}${parsedUiTheme})`);
- rootStyle.setProperty(containerBgImageUrlProp, `url('images/ui/${themeGameId}/${uiTheme}/containerbg.png')`);
- rootStyle.setProperty(borderImageUrlProp, `url('images/ui/${themeGameId}/${uiTheme}/border.png')`);
+ fastdom.mutate(() => {
+ rootStyle.setProperty(baseBgColorProp, color);
+ rootStyle.setProperty(shadowColorProp, shadow);
+ rootStyle.setProperty(svgShadowProp, `url(#dropShadow_${themeGameId !== gameId ? `${themeGameId}_` : ''}${parsedUiTheme})`);
+ rootStyle.setProperty(containerBgImageUrlProp, `url('images/ui/${themeGameId}/${uiTheme}/containerbg.png')`);
+ rootStyle.setProperty(borderImageUrlProp, `url('images/ui/${themeGameId}/${uiTheme}/border.png')`);
+ });
}
if (setTheme && themeGameId === gameId) {
- rootStyle.setProperty('--base-bg-color', `var(${baseBgColorProp})`);
- rootStyle.setProperty('--shadow-color', `var(${shadowColorProp})`);
- rootStyle.setProperty('--svg-shadow', `var(${svgShadowProp})`);
- rootStyle.setProperty('--container-bg-image-url', `var(${containerBgImageUrlProp})`);
- rootStyle.setProperty('--border-image-url', `var(${borderImageUrlProp})`);
+ fastdom.mutate(() => {
+ rootStyle.setProperty('--base-bg-color', `var(${baseBgColorProp})`);
+ rootStyle.setProperty('--shadow-color', `var(${shadowColorProp})`);
+ rootStyle.setProperty('--svg-shadow', `var(${svgShadowProp})`);
+ rootStyle.setProperty('--container-bg-image-url', `var(${containerBgImageUrlProp})`);
+ rootStyle.setProperty('--border-image-url', `var(${borderImageUrlProp})`);
+ });
}
if (callback)
@@ -526,27 +530,31 @@ function initUiThemeFontStyles(uiTheme, themeGameId, fontStyle, setTheme, callba
if (!rootStyle.getPropertyValue(baseColorProp)) {
addSystemSvgGradient(uiTheme, themeGameId, baseColors);
addSystemSvgGradient(uiTheme, themeGameId, altColors, true);
- rootStyle.setProperty(baseColorProp, getColorRgb(baseColors[8]));
- rootStyle.setProperty(altColorProp, getColorRgb(altColors[8]));
- rootStyle.setProperty(baseGradientProp, `linear-gradient(to bottom, ${getGradientText(baseColors)})`);
- rootStyle.setProperty(baseGradientBProp, `linear-gradient(to bottom, ${getGradientText(baseColors, true)})`);
- rootStyle.setProperty(altGradientProp, `linear-gradient(to bottom, ${getGradientText(altColors)})`);
- rootStyle.setProperty(altGradientBProp, `linear-gradient(to bottom, ${getGradientText(altColors, true)})`);
- rootStyle.setProperty(svgBaseGradientProp, `url(#baseGradient_${themeGameId !== gameId ? `${themeGameId}_` : ''}${parsedUiTheme})`);
- rootStyle.setProperty(svgAltGradientProp, `url(#altGradient_${themeGameId !== gameId ? `${themeGameId}_` : ''}${parsedUiTheme})`);
- rootStyle.setProperty(baseColorImageUrlProp, `url('images/ui/${themeGameId}/${uiTheme}/font${fontStyle + 1}.png')`);
+ fastdom.mutate(() => {
+ rootStyle.setProperty(baseColorProp, getColorRgb(baseColors[8]));
+ rootStyle.setProperty(altColorProp, getColorRgb(altColors[8]));
+ rootStyle.setProperty(baseGradientProp, `linear-gradient(to bottom, ${getGradientText(baseColors)})`);
+ rootStyle.setProperty(baseGradientBProp, `linear-gradient(to bottom, ${getGradientText(baseColors, true)})`);
+ rootStyle.setProperty(altGradientProp, `linear-gradient(to bottom, ${getGradientText(altColors)})`);
+ rootStyle.setProperty(altGradientBProp, `linear-gradient(to bottom, ${getGradientText(altColors, true)})`);
+ rootStyle.setProperty(svgBaseGradientProp, `url(#baseGradient_${themeGameId !== gameId ? `${themeGameId}_` : ''}${parsedUiTheme})`);
+ rootStyle.setProperty(svgAltGradientProp, `url(#altGradient_${themeGameId !== gameId ? `${themeGameId}_` : ''}${parsedUiTheme})`);
+ rootStyle.setProperty(baseColorImageUrlProp, `url('images/ui/${themeGameId}/${uiTheme}/font${fontStyle + 1}.png')`);
+ });
}
if (setTheme && themeGameId === gameId) {
- rootStyle.setProperty('--base-color', `var(${baseColorProp})`);
- rootStyle.setProperty('--alt-color', `var(${altColorProp})`);
- rootStyle.setProperty('--base-gradient', `var(${baseGradientProp})`);
- rootStyle.setProperty('--base-gradient-b', `var(${baseGradientBProp})`);
- rootStyle.setProperty('--alt-gradient', `var(${altGradientProp})`);
- rootStyle.setProperty('--alt-gradient-b', `var(${altGradientBProp})`);
- rootStyle.setProperty('--svg-base-gradient', `var(${svgBaseGradientProp})`);
- rootStyle.setProperty('--svg-alt-gradient', `var(${svgAltGradientProp})`);
- rootStyle.setProperty('--base-color-image-url', `var(${baseColorImageUrlProp})`);
+ fastdom.mutate(() => {
+ rootStyle.setProperty('--base-color', `var(${baseColorProp})`);
+ rootStyle.setProperty('--alt-color', `var(${altColorProp})`);
+ rootStyle.setProperty('--base-gradient', `var(${baseGradientProp})`);
+ rootStyle.setProperty('--base-gradient-b', `var(${baseGradientBProp})`);
+ rootStyle.setProperty('--alt-gradient', `var(${altGradientProp})`);
+ rootStyle.setProperty('--alt-gradient-b', `var(${altGradientBProp})`);
+ rootStyle.setProperty('--svg-base-gradient', `var(${svgBaseGradientProp})`);
+ rootStyle.setProperty('--svg-alt-gradient', `var(${svgAltGradientProp})`);
+ rootStyle.setProperty('--base-color-image-url', `var(${baseColorImageUrlProp})`);
+ });
}
if (callback)
@@ -563,6 +571,9 @@ function initUiThemeFontStyles(uiTheme, themeGameId, fontStyle, setTheme, callba
});
}
+/**
+ * @type {(el: Element, uiTheme?: string, themeGameId?: string) => void}
+ */
let applyThemeStyles;
{
@@ -681,23 +692,28 @@ let applyThemeStyles;
const themeStylesId = `theme${themeSuffix}`;
let themeStyles = document.getElementById(themeStylesId);
+ let task = Promise.resolve();
if (!themeStyles) {
const themePropSuffix = `-${themeGameId !== gameId ? `${themeGameId}-` : ''}${uiTheme}`;
themeStyles = document.createElement('style');
themeStyles.id = themeStylesId;
- themeStyles.innerHTML = themeStyleTemplate.replace(/\{THEME\}/g, themeSuffix).replace(/\{THEME_PROP\}/g, themePropSuffix).replace(/\{FULL_BG\|(.*?)\}/, allGameFullBgUiThemes[themeGameId].indexOf(uiTheme) > -1 ? '$1' : '');
- document.head.appendChild(themeStyles);
+ themeStyles.innerHTML = themeStyleTemplate
+ .replaceAll('{THEME}', themeSuffix)
+ .replaceAll('{THEME_PROP}', themePropSuffix)
+ .replace(/\{FULL_BG\|(.*?)\}/, allGameFullBgUiThemes[themeGameId].indexOf(uiTheme) > -1 ? '$1' : '');
+ task = fastdom.mutate(() => void(document.head.appendChild(themeStyles)));
}
-
- for (let cls of el.classList) {
- if (cls.startsWith('theme_')) {
- if (cls === themeStylesId)
- continue;
- el.classList.remove(cls);
+ task.then(() => fastdom.mutate(() => {
+ for (let cls of el.classList) {
+ if (cls.startsWith('theme_')) {
+ if (cls === themeStylesId)
+ continue;
+ el.classList.remove(cls);
+ }
}
- }
- el.classList.add(themeStylesId);
+ el.classList.add(themeStylesId);
+ }));
};
}
@@ -803,6 +819,9 @@ function getUiThemeOption(uiTheme) {
return item;
}
+/**
+ * @param {HTMLElement} themedContainer
+ */
function updateThemedContainer(themedContainer) {
if (!themedContainer)
return;
@@ -889,7 +908,7 @@ function getFontShadow(uiTheme, themeGameId, callback) {
const img = new Image();
img.onload = function () {
const canvas = document.createElement('canvas');
- const context = canvas.getContext('2d');
+ const context = canvas.getContext('2d', { willReadFrequently: true });
context.drawImage(img, 0, 0);
pixel = context.getImageData(0, 8, 1, 1).data;
uiThemeFontShadows[themeGameId][uiTheme] = [ pixel[0], pixel[1], pixel[2] ];
@@ -910,7 +929,7 @@ function getBaseBgColor(uiTheme, themeGameId, callback) {
return callback(getColorRgb(pixel));
img.onload = function () {
const canvas = document.createElement('canvas');
- const context = canvas.getContext('2d');
+ const context = canvas.getContext('2d', { willReadFrequently: true });
context.drawImage(img, 0, 0);
pixel = context.getImageData(0, 0, 1, 1).data;
const pixel2 = context.getImageData(4, 4, 1, 1).data;
@@ -974,7 +993,7 @@ function addSystemSvgGradient(systemName, systemGameId, colors, alt) {
svgGradient.setAttribute('x2', '0%');
svgGradient.setAttribute('y2', '100%');
updateSvgGradient(svgGradient, colors);
- svgDefs.appendChild(svgGradient);
+ fastdom.mutate(() => svgDefs.appendChild(svgGradient));
}
}
@@ -992,9 +1011,10 @@ function addSystemSvgDropShadow(systemName, systemGameId, color) {
svgDropShadow.setAttribute('dy', '1');
svgDropShadow.setAttribute('stdDeviation', '0.2');
svgDropShadow.setAttribute('flood-color', `rgb(${color})`);
-
- svgDropShadowFilter.appendChild(svgDropShadow);
- svgDefs.appendChild(svgDropShadowFilter);
+ fastdom.mutate(() => {
+ svgDropShadowFilter.appendChild(svgDropShadow);
+ svgDefs.appendChild(svgDropShadowFilter);
+ });
}
}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..e5e6bee0
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "include": [
+ "**/*.js",
+ "**/*.ts",
+ ],
+ "exclude": ["vendor/"],
+ "compilerOptions": {
+ "types": ["tippy.js", "wasm-feature-detect"],
+ "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
+ "module": "Preserve", /* Specify what module code is generated. */
+ "resolveJsonModule": true, /* Enable importing .json files. */
+ "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
+ "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
+ "noEmit": true, /* Disable emitting files from a compilation. */
+ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
+ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
+
+ /* Type Checking */
+ "strict": true, /* Enable all strict type-checking options. */
+ "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
+ "strictNullChecks": false, /* When type checking, take into account 'null' and 'undefined'. */
+ "noUncheckedIndexedAccess": true,
+ "skipLibCheck": true /* Skip type checking all .d.ts files. */
+ }
+}
\ No newline at end of file
diff --git a/types.d.ts b/types.d.ts
new file mode 100644
index 00000000..56267d7a
--- /dev/null
+++ b/types.d.ts
@@ -0,0 +1,31 @@
+declare const wasmFeatureDetect: typeof import('wasm-feature-detect');
+declare const tippy: typeof import('tippy.js').default;
+
+interface ParentNode {
+ querySelector(tag: K): HTMLElementTagNameMap[K] | null;
+ querySelector(selectors: string): HTMLElement | null;
+ querySelectorAll(tag: K): NodeListOf;
+ querySelectorAll(selectors: string): NodeListOf;
+}
+
+interface Object {
+ /**
+ * Prefer the `key in obj` syntax over this helper if the object is 1)
+ * definitely an object and 2) is a simple object (aka null prototype)
+ */
+ hasOwnProperty(key: K): this is Record;
+ hasTitle(): this is string | Record<'title', string>;
+}
+
+interface String {
+ get title(): string;
+}
+
+declare class FastdomPromised {
+ clear>(task: T): void;
+ initialize(): void;
+ measure void>(task: T, context?: any): Promise>>;
+ mutate void>(task: T, context?: any): Promise>>;
+}
+
+declare const fastdom: FastdomPromised;
\ No newline at end of file
diff --git a/vendor/fastdom-promised.js b/vendor/fastdom-promised.js
new file mode 100644
index 00000000..5e391869
--- /dev/null
+++ b/vendor/fastdom-promised.js
@@ -0,0 +1,82 @@
+!(function() {
+
+/**
+ * Wraps fastdom in a Promise API
+ * for improved control-flow.
+ *
+ * @example
+ *
+ * // returning a result
+ * fastdom.measure(() => el.clientWidth)
+ * .then(result => ...);
+ *
+ * // returning promises from tasks
+ * fastdom.measure(() => {
+ * var w = el1.clientWidth;
+ * return fastdom.mutate(() => el2.style.width = w + 'px');
+ * }).then(() => console.log('all done'));
+ *
+ * // clearing pending tasks
+ * var promise = fastdom.measure(...)
+ * fastdom.clear(promise);
+ *
+ * @type {Object}
+ */
+var exports = {
+ initialize: function() {
+ this._tasks = new Map();
+ },
+
+ mutate: function(fn, ctx) {
+ return create(this, 'mutate', fn, ctx);
+ },
+
+ measure: function(fn, ctx) {
+ return create(this, 'measure', fn, ctx);
+ },
+
+ clear: function(promise) {
+ var tasks = this._tasks;
+ var task = tasks.get(promise);
+ this.fastdom.clear(task);
+ tasks.delete(promise);
+ }
+};
+
+/**
+ * Create a fastdom task wrapped in
+ * a 'cancellable' Promise.
+ *
+ * @param {FastDom} fastdom
+ * @param {String} type - 'measure'|'mutate'
+ * @param {Function} fn
+ * @return {Promise}
+ */
+function create(promised, type, fn, ctx) {
+ var tasks = promised._tasks;
+ var fastdom = promised.fastdom;
+ var task;
+
+ var promise = new Promise(function(resolve, reject) {
+ task = fastdom[type](function() {
+ tasks.delete(promise);
+ try {
+ const res = ctx ? fn.call(ctx) : fn();
+ if (res instanceof Promise)
+ res.then(resolve, reject);
+ else resolve(res);
+ }
+ catch (e) { reject(e); }
+ }, ctx);
+ });
+
+ tasks.set(promise, task);
+ return promise;
+}
+
+// Expose to CJS, AMD or global
+if ((typeof define)[0] == 'f') define(function() { return exports; });
+else if ((typeof module)[0] == 'o') module.exports = exports;
+else window.fastdom = window.fastdom.extend(exports);
+
+})();
\ No newline at end of file
diff --git a/vendor/fastdom.js b/vendor/fastdom.js
new file mode 100644
index 00000000..5ef81a1c
--- /dev/null
+++ b/vendor/fastdom.js
@@ -0,0 +1,244 @@
+!(function(win) {
+
+/**
+ * FastDom
+ *
+ * Eliminates layout thrashing
+ * by batching DOM read/write
+ * interactions.
+ *
+ * @author Wilson Page
+ * @author Kornel Lesinski
+ */
+
+'use strict';
+
+/**
+ * Mini logger
+ *
+ * @return {Function}
+ */
+var debug = 0 ? console.log.bind(console, '[fastdom]') : function() {};
+
+/**
+ * Normalized rAF
+ *
+ * @type {Function}
+ */
+var raf = win.requestAnimationFrame
+ || win.webkitRequestAnimationFrame
+ || win.mozRequestAnimationFrame
+ || win.msRequestAnimationFrame
+ || function(cb) { return setTimeout(cb, 16); };
+
+/**
+ * Initialize a `FastDom`.
+ *
+ * @constructor
+ */
+function FastDom() {
+ var self = this;
+ self.reads = [];
+ self.writes = [];
+ self.raf = raf.bind(win); // test hook
+ debug('initialized', self);
+}
+
+FastDom.prototype = {
+ constructor: FastDom,
+
+ /**
+ * We run this inside a try catch
+ * so that if any jobs error, we
+ * are able to recover and continue
+ * to flush the batch until it's empty.
+ *
+ * @param {Array} tasks
+ */
+ runTasks: function(tasks) {
+ debug('run tasks');
+ var task; while (task = tasks.shift()) task();
+ },
+
+ /**
+ * Adds a job to the read batch and
+ * schedules a new frame if need be.
+ *
+ * @param {Function} fn
+ * @param {Object} ctx the context to be bound to `fn` (optional).
+ * @public
+ */
+ measure: function(fn, ctx) {
+ debug('measure');
+ var task = !ctx ? fn : fn.bind(ctx);
+ this.reads.push(task);
+ scheduleFlush(this);
+ return task;
+ },
+
+ /**
+ * Adds a job to the
+ * write batch and schedules
+ * a new frame if need be.
+ *
+ * @param {Function} fn
+ * @param {Object} ctx the context to be bound to `fn` (optional).
+ * @public
+ */
+ mutate: function(fn, ctx) {
+ debug('mutate');
+ var task = !ctx ? fn : fn.bind(ctx);
+ this.writes.push(task);
+ scheduleFlush(this);
+ return task;
+ },
+
+ /**
+ * Clears a scheduled 'read' or 'write' task.
+ *
+ * @param {Object} task
+ * @return {Boolean} success
+ * @public
+ */
+ clear: function(task) {
+ debug('clear', task);
+ return remove(this.reads, task) || remove(this.writes, task);
+ },
+
+ /**
+ * Extend this FastDom with some
+ * custom functionality.
+ *
+ * Because fastdom must *always* be a
+ * singleton, we're actually extending
+ * the fastdom instance. This means tasks
+ * scheduled by an extension still enter
+ * fastdom's global task queue.
+ *
+ * The 'super' instance can be accessed
+ * from `this.fastdom`.
+ *
+ * @example
+ *
+ * var myFastdom = fastdom.extend({
+ * initialize: function() {
+ * // runs on creation
+ * },
+ *
+ * // override a method
+ * measure: function(fn) {
+ * // do extra stuff ...
+ *
+ * // then call the original
+ * return this.fastdom.measure(fn);
+ * },
+ *
+ * ...
+ * });
+ *
+ * @param {Object} props properties to mixin
+ * @return {FastDom}
+ */
+ extend: function(props) {
+ debug('extend', props);
+ if (typeof props != 'object') throw new Error('expected object');
+
+ var child = Object.create(this);
+ mixin(child, props);
+ child.fastdom = this;
+
+ // run optional creation hook
+ if (child.initialize) child.initialize();
+
+ return child;
+ },
+
+ // override this with a function
+ // to prevent Errors in console
+ // when tasks throw
+ catch: null
+};
+
+/**
+ * Schedules a new read/write
+ * batch if one isn't pending.
+ *
+ * @private
+ */
+function scheduleFlush(fastdom) {
+ if (!fastdom.scheduled) {
+ fastdom.scheduled = true;
+ fastdom.raf(flush.bind(null, fastdom));
+ debug('flush scheduled');
+ }
+}
+
+/**
+ * Runs queued `read` and `write` tasks.
+ *
+ * Errors are caught and thrown by default.
+ * If a `.catch` function has been defined
+ * it is called instead.
+ *
+ * @private
+ */
+function flush(fastdom) {
+ debug('flush');
+
+ var writes = fastdom.writes;
+ var reads = fastdom.reads;
+ var error;
+
+ try {
+ debug('flushing reads', reads.length);
+ fastdom.runTasks(reads);
+ debug('flushing writes', writes.length);
+ fastdom.runTasks(writes);
+ } catch (e) { error = e; }
+
+ fastdom.scheduled = false;
+
+ // If the batch errored we may still have tasks queued
+ if (reads.length || writes.length) scheduleFlush(fastdom);
+
+ if (error) {
+ debug('task errored', error.message);
+ if (fastdom.catch) fastdom.catch(error);
+ else throw error;
+ }
+}
+
+/**
+ * Remove an item from an Array.
+ *
+ * @param {Array} array
+ * @param {*} item
+ * @return {Boolean}
+ */
+function remove(array, item) {
+ var index = array.indexOf(item);
+ return !!~index && !!array.splice(index, 1);
+}
+
+/**
+ * Mixin own properties of source
+ * object into the target.
+ *
+ * @param {Object} target
+ * @param {Object} source
+ */
+function mixin(target, source) {
+ for (var key in source) {
+ if (source.hasOwnProperty(key)) target[key] = source[key];
+ }
+}
+
+// There should never be more than
+// one instance of `FastDom` in an app
+var exports = win.fastdom = (win.fastdom || new FastDom()); // jshint ignore:line
+
+// Expose to CJS & AMD
+if ((typeof define) == 'function') define(function() { return exports; });
+else if ((typeof module) == 'object') module.exports = exports;
+
+})( typeof window !== 'undefined' ? window : typeof this != 'undefined' ? this : globalThis);