// ==UserScript==
// @name [PS] Battle Log Styler
// @namespace https://greasyfork.org/en/users/1357767-indigeau
// @version 0.0
// @description Re-styles the Pokémon Showdown battle log.
// @match https://play.pokemonshowdown.com/*
// @exclude https://play.pokemonshowdown.com/sprites/*
// @author indigeau
// @license GNU GPLv3
// @icon https://www.google.com/s2/favicons?sz=64&domain=pokemonshowdown.com
// @grant none
// ==/UserScript==
const main = () => {
const logSelector = '.battle-log > .message-log';
(() => {
const styleSheet = (() => {
const styleElement = document.createElement('style');
document.head.appendChild(styleElement);
return styleElement.sheet;
})();
const rules = [
[`${logSelector} > .spacer.battle-history`, [
['width', '100%'],
['height', '2px'],
]],
[`${logSelector} > .spacer.battle-history:not(h2 + *):not(:has(+h2))`, [
['margin', '8px -8px'],
['padding', '0 8px'],
]],
[`${logSelector} > h2.battle-history`, [['border-width', '2px']]],
[`${logSelector} > :not(.battle-history):not(.chat)`, [
['margin', '-8px'],
['padding', '5px 8px'],
]],
[`${logSelector} > .chat:first-child`, [['visibility', 'hidden']]],
['.battle-log > .battle-options:has(+ .message-log > .chat:first-child)', [['margin-bottom', '-1em']]],
['html.dark .ps-room .battle-log', [['scrollbar-color', '#333 #5a5a5a']]],
['html:not(.dark) .ps-room .battle-log', [['scrollbar-color', '#eef2f5 #aaa']]],
[`html:not(.dark) ${logSelector} > .spacer.battle-history:not(h2 + *):not(:has(+h2))`, [['background', '#aaa']]],
[`html.dark ${logSelector} > .spacer.battle-history:not(h2 + *):not(:has(+h2))`, [['background', '#5a5a5a']]],
];
for (let rule of rules) {
styleSheet.insertRule(`${rule[0]}{${rule[1].map(([property, value]) => `${property}:${value};`).join('')}}`);
}
})();
(() => {
const onBattle = (() => {
const addListeners = [];
const removeListeners = [];
const getBattleRoom = (node) => {
if (!node.classList.contains('ps-room')) {
return null;
}
const room = window.app.rooms[node.id.slice(5)];
return room instanceof window.BattleRoom ? room : null;
};
const rooms = [...document.body.children]
.map((node) => getBattleRoom(node))
.filter((room) => room !== null);
(new MutationObserver((mutations) => {
for (const {addedNodes, removedNodes} of mutations) {
for (const node of addedNodes) {
const room = getBattleRoom(node);
if (!room) {
return;
}
for (const listener of addListeners) {
listener(room, node.id);
}
rooms.push(room);
}
for (const node of removedNodes) {
for (const listener of removeListeners) {
listener(node.id);
}
}
}
})).observe(document.body, {childList: true});
return (onAdd, onRemove) => {
addListeners.push(onAdd);
if (onRemove) {
removeListeners.push(onRemove);
}
for (const room of rooms) {
onAdd(room, room.el.id);
}
};
})();
// Fixes scroll issue for leave message
onBattle(({'$chat': [{parentElement, children}], 'userList': {'el': userList}}) => {
(new MutationObserver(() => {
if (children.length === 0) {
return;
}
const {offsetTop} = children[children.length - 2];
if (parentElement.scrollTop + parentElement.clientHeight >= offsetTop) {
parentElement.scrollTop = parentElement.scrollHeight;
}
})).observe(userList, {childList: true});
});
// Header colour setting
// HSL without the H
const SL = {
LIGHT: [24, 90],
DARK: [20, 20],
};
// https://stackoverflow.com/a/6445104
function rgbToHue(...args) {
const [r, g, b] = args.map((arg) => arg / 255);
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
if (max === min) {
return 0;
}
const d = max - min;
switch (max) {
case r:
return ((g - b) / d + (g < b ? 6 : 0)) / 6 * 255;
case g:
return ((b - r) / d + 2) / 6 * 255;
}
return ((r - g) / d + 4) / 6 * 255;
}
const styleSheets = {};
onBattle(
({battle}, id) => {
const [deleteColours, setColours] = (() => {
const styleSheet = (() => {
const styleElement = document.createElement('style');
document.head.appendChild(styleElement);
return styleElement.sheet;
})();
const selectors = [`#${id} ${logSelector} > :not(.battle-history):not(.chat)`, `#${id} ${logSelector} > h2.battle-history`];
styleSheets[id] = styleSheet;
return [
(deleteInitial = false) => {
for (let i = styleSheet.cssRules.length - 1; i >= (deleteInitial ? 0 : 2); --i) {
styleSheet.deleteRule(i);
}
},
(hue, doTransition = true) => {
const rules = [
[selectors.map((selector) => `html:not(.dark) > body > ${selector}`).toString(), [
['background-color', `hsl(${hue}, ${SL.LIGHT[0]}%, ${SL.LIGHT[1]}%)${doTransition ? ' !important' : ''}`],
]],
[selectors.map((selector) => `html.dark > body > ${selector}`).toString(), [
['background-color', `hsl(${hue}, ${SL.DARK[0]}%, ${SL.DARK[1]}%)${doTransition ? ' !important' : ''}`],
]],
];
if (doTransition) {
rules.push([selectors.toString(), [['transition', 'background-color 0.9s ease-in-out']]]);
}
for (let rule of rules) {
styleSheet.insertRule(`${rule[0]}{${rule[1].map(([property, value]) => `${property}:${value};`).join('')}}`, styleSheet.cssRules.length);
}
},
];
})();
const init = async () => {
const image = new Image();
await new Promise((resolve) => {
image.onload = resolve;
image.src = battle.scene.backdropImage;
});
const baseHue = rgbToHue(...(new window.ColorThief()).getColor(image));
const updateWeather = (doTransition = false) => {
const [r, g, b, a = 1] = [...getComputedStyle(battle.scene.$weather[0]).backgroundColor]
.filter((c) => /[\d\s]/.test(c))
.join('')
.split(' ')
.map((string) => Number.parseInt(string));
deleteColours();
if (a === 0) {
return;
}
const weatherHue = rgbToHue(r, g, b);
setColours(weatherHue, doTransition);
};
setColours(baseHue, false);
if (battle.weather) {
updateWeather();
}
Object.defineProperty(battle, 'weather', (() => {
let weather = battle.weather;
return {
set(value) {
weather = value;
updateWeather(true);
},
get() {
return weather;
},
};
})());
};
init();
Object.defineProperty(battle.scene, 'backdropImage', (() => {
let image = battle.scene.backdropImage;
return {
set(value) {
image = value;
deleteColours(true);
init();
},
get() {
return image;
},
};
})());
},
(id) => {
if (!(id in styleSheets)) {
return;
}
styleSheets[id].node.remove();
delete styleSheets[id];
},
);
Object.defineProperty(Storage.prefs.data, 'theme', (() => {
let theme = Storage.prefs.data.theme;
return {
set(value) {
theme = value;
for (const sheet of Object.values(styleSheets)) {
if (sheet.cssRules.length > 4) {
sheet.deleteRule(4);
}
}
},
get() {
return theme;
},
};
})());
})();
};
// Dealing with firefox being restrictive
(() => {
const context = typeof unsafeWindow === 'undefined' ? window : unsafeWindow;
context._test = {};
const isAccessDenied = (() => {
try {
window.eval('_test._');
} catch (e) {
return true;
}
return false;
})();
delete context._test;
if (isAccessDenied) {
window.eval(`(${main.toString()})()`);
} else {
main();
}
})();