diff --git a/counters/Pups-Overlay by Puparia/deps/countUp.js b/counters/Pups-Overlay by Puparia/deps/countUp.js new file mode 100644 index 00000000..79e3d6b6 --- /dev/null +++ b/counters/Pups-Overlay by Puparia/deps/countUp.js @@ -0,0 +1,261 @@ + +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + define(factory); + } else if (typeof exports === 'object') { + module.exports = factory(require, exports, module); + } else { + root.CountUp = factory(); + } + }(this, function(require, exports, module) { + + /* + + countUp.js + by @inorganik + + */ + + // target = id of html element or var of previously selected html element where counting occurs + // startVal = the value you want to begin at + // endVal = the value you want to arrive at + // decimals = number of decimal places, default 0 + // duration = duration of animation in seconds, default 2 + // options = optional object of options (see below) + + var CountUp = function(target, startVal, endVal, decimals, duration, options) { + + var self = this; + self.version = function () { return '1.9.3'; }; + + // default options + self.options = { + useEasing: true, // toggle easing + useGrouping: true, // 1,000,000 vs 1000000 + separator: '', // character to use as a separator + decimal: '.', // character to use as a decimal + easingFn: easeOutExpo, // optional custom easing function, default is Robert Penner's easeOutExpo + formattingFn: formatNumber, // optional custom formatting function, default is formatNumber above + prefix: '', // optional text before the result + suffix: '', // optional text after the result + numerals: [] // optionally pass an array of custom numerals for 0-9 + }; + + // extend default options with passed options object + if (options && typeof options === 'object') { + for (var key in self.options) { + if (options.hasOwnProperty(key) && options[key] !== null) { + self.options[key] = options[key]; + } + } + } + + if (self.options.separator === '') { + self.options.useGrouping = false; + } + else { + // ensure the separator is a string (formatNumber assumes this) + self.options.separator = '' + self.options.separator; + } + + // make sure requestAnimationFrame and cancelAnimationFrame are defined + // polyfill for browsers without native support + // by Opera engineer Erik Möller + var lastTime = 0; + var vendors = ['webkit', 'moz', 'ms', 'o']; + for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; + window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; + } + if (!window.requestAnimationFrame) { + window.requestAnimationFrame = function(callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + } + if (!window.cancelAnimationFrame) { + window.cancelAnimationFrame = function(id) { + clearTimeout(id); + }; + } + + function formatNumber(num) { + var neg = (num < 0), + x, x1, x2, x3, i, len; + num = Math.abs(num).toFixed(self.decimals); + num += ''; + x = num.split('.'); + x1 = x[0]; + x2 = x.length > 1 ? self.options.decimal + x[1] : ''; + if (self.options.useGrouping) { + x3 = ''; + for (i = 0, len = x1.length; i < len; ++i) { + if (i !== 0 && ((i % 3) === 0)) { + x3 = self.options.separator + x3; + } + x3 = x1[len - i - 1] + x3; + } + x1 = x3; + } + // optional numeral substitution + if (self.options.numerals.length) { + x1 = x1.replace(/[0-9]/g, function(w) { + return self.options.numerals[+w]; + }) + x2 = x2.replace(/[0-9]/g, function(w) { + return self.options.numerals[+w]; + }) + } + return (neg ? '-' : '') + self.options.prefix + x1 + x2 + self.options.suffix; + } + // Robert Penner's easeOutExpo + function easeOutExpo(t, b, c, d) { + return c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b; + } + function ensureNumber(n) { + return (typeof n === 'number' && !isNaN(n)); + } + + self.initialize = function() { + if (self.initialized) return true; + + self.error = ''; + self.d = (typeof target === 'string') ? document.getElementById(target) : target; + if (!self.d) { + self.error = '[CountUp] target is null or undefined' + return false; + } + self.startVal = Number(startVal); + self.endVal = Number(endVal); + // error checks + if (ensureNumber(self.startVal) && ensureNumber(self.endVal)) { + self.decimals = Math.max(0, decimals || 0); + self.dec = Math.pow(10, self.decimals); + self.duration = Number(duration) * 1000 || 2000; + self.countDown = (self.startVal > self.endVal); + self.frameVal = self.startVal; + self.initialized = true; + return true; + } + else { + self.error = '[CountUp] startVal ('+startVal+') or endVal ('+endVal+') is not a number'; + return false; + } + }; + + // Print value to target + self.printValue = function(value) { + var result = self.options.formattingFn(value); + + if (self.d.tagName === 'INPUT') { + this.d.value = result; + } + else if (self.d.tagName === 'text' || self.d.tagName === 'tspan') { + this.d.textContent = result; + } + else { + this.d.innerHTML = result; + } + }; + + self.count = function(timestamp) { + + if (!self.startTime) { self.startTime = timestamp; } + + self.timestamp = timestamp; + var progress = timestamp - self.startTime; + self.remaining = self.duration - progress; + + // to ease or not to ease + if (self.options.useEasing) { + if (self.countDown) { + self.frameVal = self.startVal - self.options.easingFn(progress, 0, self.startVal - self.endVal, self.duration); + } else { + self.frameVal = self.options.easingFn(progress, self.startVal, self.endVal - self.startVal, self.duration); + } + } else { + if (self.countDown) { + self.frameVal = self.startVal - ((self.startVal - self.endVal) * (progress / self.duration)); + } else { + self.frameVal = self.startVal + (self.endVal - self.startVal) * (progress / self.duration); + } + } + + // don't go past endVal since progress can exceed duration in the last frame + if (self.countDown) { + self.frameVal = (self.frameVal < self.endVal) ? self.endVal : self.frameVal; + } else { + self.frameVal = (self.frameVal > self.endVal) ? self.endVal : self.frameVal; + } + + // decimal + self.frameVal = Math.round(self.frameVal*self.dec)/self.dec; + + // format and print value + self.printValue(self.frameVal); + + // whether to continue + if (progress < self.duration) { + self.rAF = requestAnimationFrame(self.count); + } else { + if (self.callback) self.callback(); + } + }; + // start your animation + self.start = function(callback) { + if (!self.initialize()) return; + self.callback = callback; + self.rAF = requestAnimationFrame(self.count); + }; + // toggles pause/resume animation + self.pauseResume = function() { + if (!self.paused) { + self.paused = true; + cancelAnimationFrame(self.rAF); + } else { + self.paused = false; + delete self.startTime; + self.duration = self.remaining; + self.startVal = self.frameVal; + requestAnimationFrame(self.count); + } + }; + // reset to startVal so animation can be run again + self.reset = function() { + self.paused = false; + delete self.startTime; + self.initialized = false; + if (self.initialize()) { + cancelAnimationFrame(self.rAF); + self.printValue(self.startVal); + } + }; + // pass a new endVal and start animation + self.update = function (newEndVal) { + if (!self.initialize()) return; + newEndVal = Number(newEndVal); + if (!ensureNumber(newEndVal)) { + self.error = '[CountUp] update() - new endVal is not a number: '+newEndVal; + return; + } + self.error = ''; + if (newEndVal === self.frameVal) return; + cancelAnimationFrame(self.rAF); + self.paused = false; + delete self.startTime; + self.startVal = self.frameVal; + self.endVal = newEndVal; + self.countDown = (self.startVal > self.endVal); + self.rAF = requestAnimationFrame(self.count); + }; + + // format startVal on initialization + if (self.initialize()) self.printValue(self.startVal); + }; + + return CountUp; + + })); \ No newline at end of file diff --git a/counters/Pups-Overlay by Puparia/deps/fast-smooth.js b/counters/Pups-Overlay by Puparia/deps/fast-smooth.js new file mode 100644 index 00000000..457f3263 --- /dev/null +++ b/counters/Pups-Overlay by Puparia/deps/fast-smooth.js @@ -0,0 +1,212 @@ +/** + * Find the maximum or array slice. Start is inclusive but end is exclusive + * + * @param array + * @param start + * @param end + * @return {number} + */ +export function max(array, start = 0, end = -1) { + if (end === -1) { + end = array.length; + } + + let maximum = Number.NEGATIVE_INFINITY; + + for (let i = start; i < array.length && i < end; i++) { + if (maximum < array[i]) { + maximum = array[i]; + } + } + + return maximum; +} + +/** + * Calculate sum of array slice. Start is inclusive but end is exclusive + * + * @param {ArrayLike} array + * @param {number} start + * @param {number} end + * @return {number} + */ +function sum(array, start = 0, end = - 1) { + if (end === -1) { + end = array.length; + } + + let s = 0; + + for (let i = start; i < array.length && i < end; i++) { + s += array[i]; + } + + return s; +} + +/** + * Calculate mean of array slice. Start is inclusive but end is exclusive + * + * @param {ArrayLike} array + * @param {number} start + * @param {number} end + * @return {number} + */ +function mean(array, start = 0, end = -1) { + return sum(array, start, end) / end; +} + +/** + * @param {ArrayLike} array + * @param {number} windowWidth + * @param {boolean} doSmoothEnds + * @return {Float64Array} + */ +function smooth(array, windowWidth, doSmoothEnds) { + const width = Math.round(windowWidth); + if (width <= 1) { + return new Float64Array(array); + } + + const half = Math.round(width / 2); + const ret = new Float64Array(array.length); + + let sumPoints = sum(array, 0, width); + let i = 0; + + for (; i < array.length - width + 1; i++) { + ret[i + half - 1] = Math.max(0, sumPoints); + sumPoints -= array[i]; + sumPoints += array[i + width]; + } + + ret[i + half] = Math.max(0, sum(array, array.length - width + 1, array.length)); + + for (let j = 0; j < ret.length; j++) { + ret[j] /= width; + } + + if (!doSmoothEnds) { + return ret; + } + + const start = (windowWidth + 1) / 2; + ret[0] = (array[0] + array[1]) / 2; + + for (let j = 1; j < start; j++) { + ret[j] = Math.max(0, mean(array, 0, 2 * j - 1)); + ret[array.length - j] = Math.max(0, mean(array, array.length - 2 * j + 2, array.length)); + } + + ret[ret.length - 1] = Math.max(0, (array[array.length - 1] + array[array.length - 2]) / 2); + + return ret; +} + +export const FAST_SMOOTH_TYPE_NO_SMOOTHING = 0; +export const FAST_SMOOTH_TYPE_RECTANGULAR = 1; +export const FAST_SMOOTH_TYPE_TRIANGULAR = 2; +export const FAST_SMOOTH_TYPE_PSEUDO_GAUSSIAN_3 = 3; +export const FAST_SMOOTH_TYPE_PSEUDO_GAUSSIAN_4 = 4; +export const FAST_SMOOTH_TYPE_MULTIPLE_WIDTH = 5; + +/** + * Smooths array with smooth of width windowWidth. + * The argument "type" determines the smooth type: + * - If type = FAST_SMOOTH_TYPE_RECTANGULAR = 0, rectangular (sliding-average or boxcar) + * - If type = FAST_SMOOTH_TYPE_TRIANGULAR = 1, triangular (2 passes of sliding-average) + * - If type = FAST_SMOOTH_TYPE_PSEUDO_GAUSSIAN_3 = 2, pseudo-Gaussian (3 passes of sliding-average) + * - If type = FAST_SMOOTH_TYPE_PSEUDO_GAUSSIAN_4 = 3, pseudo-Gaussian (4 passes of same sliding-average) + * - If type = FAST_SMOOTH_TYPE_MULTIPLE_WIDTH = 4, multiple-width (4 passes of different sliding-average) + * The argument "doSmoothEnds" controls how the "ends" of the signal (the first w/2 points and the last w/2 points) are + * handled. + * - If ends=0, the ends are zero. (In this mode the elapsed time is independent of the smooth width). The fastest. + * - If ends=1, the ends are smoothed with progressively smaller smooths the closer to the end. (In this mode the + * elapsed time increases with increasing smooth widths). + * + * Version 3.0, October 2016. + * + * Copyright (c) 2024, Jan Horák + * + * Copyright (c) 2012, Thomas C. O'Haver + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @param array + * @param windowWidth + * @param type + * @param doSmoothEnds + * @return {Float64Array} + * @see https://www.mathworks.com/matlabcentral/fileexchange/19998-fast-smoothing-function + */ +export function fastSmooth(array, windowWidth, type = FAST_SMOOTH_TYPE_RECTANGULAR, doSmoothEnds = false) { + const a = array; + const w = windowWidth; + const e = doSmoothEnds; + + switch (type) { + case FAST_SMOOTH_TYPE_NO_SMOOTHING: + return new Float64Array(array); + + default: + case FAST_SMOOTH_TYPE_RECTANGULAR: + return smooth(a, w, e); + + case FAST_SMOOTH_TYPE_TRIANGULAR: + return smooth( + smooth( + a, w, e + ), w, e + ); + + case FAST_SMOOTH_TYPE_PSEUDO_GAUSSIAN_3: + return smooth( + smooth( + smooth( + a, w, e + ), w, e + ), w, e + ); + + case FAST_SMOOTH_TYPE_PSEUDO_GAUSSIAN_4: + return smooth( + smooth( + smooth( + smooth( + a, w, e + ), + w, e + ), w, e + ), w, e + ); + + case FAST_SMOOTH_TYPE_MULTIPLE_WIDTH: + return smooth( + smooth( + smooth( + smooth( + a, Math.round(1.6 * w), e + ), + Math.round(1.4 * w), e + ), + Math.round(1.2 * w), e + ), w, e + ); + } +} diff --git a/counters/Pups-Overlay by Puparia/deps/socket.js b/counters/Pups-Overlay by Puparia/deps/socket.js new file mode 100644 index 00000000..b8eaf921 --- /dev/null +++ b/counters/Pups-Overlay by Puparia/deps/socket.js @@ -0,0 +1,1013 @@ +class WebSocketManager { + constructor(host) { + this.version = '0.1.5'; + + if (host) { + this.host = host; + } + + this.createConnection = this.createConnection.bind(this); + + /** + * @type {{ [key: string]: WebSocket }} asd; + */ + this.sockets = {}; + } + + createConnection(url, callback, filters) { + let INTERVAL = ''; + + const that = this; + this.sockets[url] = new WebSocket(`ws://${this.host}${url}?l=${encodeURI(window.COUNTER_PATH)}`); + + this.sockets[url].onopen = () => { + console.log(`[OPEN] ${url}: Connected`); + + if (INTERVAL) clearInterval(INTERVAL); + if (Array.isArray(filters)) { + this.sockets[url].send(`applyFilters:${JSON.stringify(filters)}`); + } + }; + + this.sockets[url].onclose = (event) => { + console.log(`[CLOSED] ${url}: ${event.reason}`); + + delete this.sockets[url]; + INTERVAL = setTimeout(() => { + that.createConnection(url, callback, filters); + }, 1000); + }; + + this.sockets[url].onerror = (event) => { + console.log(`[ERROR] ${url}: ${event.reason}`); + }; + + + this.sockets[url].onmessage = (event) => { + try { + const data = JSON.parse(event.data); + if (data.error != null) { + console.error(`[MESSAGE_ERROR] ${url}:`, data.error); + return; + }; + + if (data.message != null) { + if (data.message.error != null) { + console.error(`[MESSAGE_ERROR] ${url}:`, data.message.error); + return; + } + }; + + callback(data); + } catch (error) { + console.log(`[MESSAGE_ERROR] ${url}: Couldn't parse incomming message`, error); + }; + }; + }; + + + /** + * Connects to gosu compatible socket api. + * @param {(data: WEBSOCKET_V1) => void} callback The function to handle received messages. + * @param {Filters[]} filters + */ + api_v1(callback, filters) { + this.createConnection(`/ws`, callback, filters); + }; + + + /** + * Connects to tosu advanced socket api. + * @param {(data: WEBSOCKET_V2) => void} callback The function to handle received messages. + * @param {Filters[]} filters + */ + api_v2(callback, filters) { + this.createConnection(`/websocket/v2`, callback, filters); + }; + + + /** + * Connects to tosu precise socket api. + * @param {(data: WEBSOCKET_V2_PRECISE) => void} callback The function to handle received messages. + * @param {Filters[]} filters + */ + api_v2_precise(callback, filters) { + this.createConnection(`/websocket/v2/precise`, callback, filters); + }; + + + /** + * Calculate custom pp for a current, or specified map + * @param {CALCULATE_PP} params + * @returns {Promise} + */ + async calculate_pp(params) { + try { + if (typeof params != 'object') { + return { + error: 'Wrong argument type, should be object with params' + }; + }; + + + const url = new URL(`http://${this.host}/api/calculate/pp`); + Object.keys(params) + .forEach(key => url.searchParams.append(key, params[key])); + + const request = await fetch(url, { method: "GET", }); + + + const json = await request.json(); + return json; + } catch (error) { + console.error(error); + + return { + error: error.message, + }; + }; + }; + + + /** + * Get beatmap **.osu** file (local) + * @param {string} file_path Path to a file **beatmap_folder_name/osu_file_name.osu** + * @returns {string | { error: string }} + */ + async getBeatmapOsuFile(file_path) { + try { + if (typeof file_path != 'object') { + return { + error: 'Wrong argument type, should be object with params' + }; + }; + + + const request = await fetch(`${this.host}/files/beatmap/${file_path}`, { + method: "GET", + }); + + + const text = await request.text(); + return text; + } catch (error) { + console.error(error); + + return { + error: error.message, + }; + }; + }; + + + /** + * Connects to message + * @param {(data: { command: string, message: any }) => void} callback The function to handle received messages. + */ + commands(callback) { + this.createConnection(`/websocket/commands`, callback); + }; + + /** + * + * @param {string} name + * @param {string|Object} payload + */ + sendCommand(name, command, amountOfRetries = 1) { + const that = this; + + + if (!this.sockets['/websocket/commands']) { + setTimeout(() => { + that.sendCommand(name, command, amountOfRetries + 1); + }, 100); + + return; + }; + + + try { + const payload = typeof command == 'object' ? JSON.stringify(command) : command; + this.sockets['/websocket/commands'].send(`${name}:${payload}`); + } catch (error) { + if (amountOfRetries <= 3) { + console.log(`[COMMAND_ERROR] Attempt ${amountOfRetries}`, error); + setTimeout(() => { + that.sendCommand(name, command, amountOfRetries + 1); + }, 1000); + return; + }; + + + console.error(`[COMMAND_ERROR]`, error); + }; + }; + + + close(url) { + this.host = url; + + const array = Object.keys(this.sockets); + for (let i = 0; i < array.length; i++) { + const key = array[i]; + const value = this.sockets[key]; + + if (!value) continue; + value.close(); + }; + }; +}; + + +export default WebSocketManager; + + + +/** + * @typedef {string | { field: string; keys: Filters[] }} Filters + */ + + +/** @typedef {object} CALCULATE_PP + * @property {string} path Path to .osu file. Example: C:/osu/Songs/beatmap/file.osu + * @property {number} mode Osu = 0, Taiko = 1, Catch = 2, Mania = 3 + * @property {number} mods Mods id. Example: 64 - DT + * @property {number} acc Accuracy % from 0 to 100 + * @property {number} nGeki Amount of Geki (300g / MAX) + * @property {number} nKatu Amount of Katu (100k / 200) + * @property {number} n300 Amount of 300 + * @property {number} n100 Amount of 100 + * @property {number} n50 Amount of 50 + * @property {number} nMisses Amount of Misses + * @property {number} combo combo + * @property {number} passedObjects Sum of nGeki, nKatu, n300, n100, n50, nMisses + * @property {number} clockRate Map rate number. Example: 1.5 = DT + */ + + + +/** @typedef {object} CALCULATE_PP_RESPONSE + * @property {object} difficulty + * @property {number} difficulty.mode + * @property {number} difficulty.stars + * @property {boolean} difficulty.isConvert + * @property {number} difficulty.aim + * @property {number} difficulty.speed + * @property {number} difficulty.flashlight + * @property {number} difficulty.sliderFactor + * @property {number} difficulty.speedNoteCount + * @property {number} difficulty.od + * @property {number} difficulty.hp + * @property {number} difficulty.nCircles + * @property {number} difficulty.nSliders + * @property {number} difficulty.nSpinners + * @property {number} difficulty.ar + * @property {number} difficulty.maxCombo + * @property {object} state + * @property {number} state.maxCombo + * @property {number} state.nGeki + * @property {number} state.nKatu + * @property {number} state.n300 + * @property {number} state.n100 + * @property {number} state.n50 + * @property {number} state.misses + * @property {number} pp + * @property {number} ppAim + * @property {number} ppFlashlight + * @property {number} ppSpeed + * @property {number} ppAccuracy + * @property {number} effectiveMissCount + */ + + + +/** @typedef {object} WEBSOCKET_V1 + * @property {'stable' | 'lazer'} client + * @property {object} settings + * @property {boolean} settings.showInterface + * @property {object} settings.folders + * @property {string} settings.folders.game + * @property {string} settings.folders.skin + * @property {string} settings.folders.songs + * @property {object} menu + * @property {object} menu.mainMenu + * @property {number} menu.mainMenu.bassDensity + * @property {0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23} menu.state + * @property {0 | 1 | 2 | 3} menu.gameMode + * @property {0 | 1} menu.isChatEnabled + * @property {object} menu.bm + * @property {object} menu.bm.time + * @property {number} menu.bm.time.firstObj + * @property {number} menu.bm.time.current + * @property {number} menu.bm.time.full + * @property {number} menu.bm.time.mp3 + * @property {number} menu.bm.id + * @property {number} menu.bm.set + * @property {string} menu.bm.md5 + * @property {0 | 1 | 2 | 4 | 5 | 6 | 7} menu.bm.rankedStatus + * @property {object} menu.bm.metadata + * @property {string} menu.bm.metadata.artist + * @property {string} menu.bm.metadata.artistOriginal + * @property {string} menu.bm.metadata.title + * @property {string} menu.bm.metadata.titleOriginal + * @property {string} menu.bm.metadata.mapper + * @property {string} menu.bm.metadata.difficulty + * @property {object} menu.bm.stats + * @property {number} menu.bm.stats.AR + * @property {number} menu.bm.stats.CS + * @property {number} menu.bm.stats.OD + * @property {number} menu.bm.stats.HP + * @property {number} menu.bm.stats.SR + * @property {object} menu.bm.stats.BPM + * @property {number} menu.bm.stats.BPM.realtime + * @property {number} menu.bm.stats.BPM.common + * @property {number} menu.bm.stats.BPM.min + * @property {number} menu.bm.stats.BPM.max + * @property {number} menu.bm.stats.circles + * @property {number} menu.bm.stats.sliders + * @property {number} menu.bm.stats.spinners + * @property {number} menu.bm.stats.holds + * @property {number} menu.bm.stats.maxCombo + * @property {number} menu.bm.stats.fullSR + * @property {number} menu.bm.stats.memoryAR + * @property {number} menu.bm.stats.memoryCS + * @property {number} menu.bm.stats.memoryOD + * @property {number} menu.bm.stats.memoryHP + * @property {object} menu.bm.path + * @property {string} menu.bm.path.full + * @property {string} menu.bm.path.folder + * @property {string} menu.bm.path.file + * @property {string} menu.bm.path.bg + * @property {string} menu.bm.path.audio + * @property {object} menu.mods + * @property {number} menu.mods.num + * @property {string} menu.mods.str + * @property {object} menu.pp + * @property {number} menu.pp.90 + * @property {number} menu.pp.91 + * @property {number} menu.pp.92 + * @property {number} menu.pp.93 + * @property {number} menu.pp.94 + * @property {number} menu.pp.95 + * @property {number} menu.pp.96 + * @property {number} menu.pp.97 + * @property {number} menu.pp.98 + * @property {number} menu.pp.99 + * @property {number} menu.pp.100 + * @property {number[]} menu.pp.strains + * @property {object} menu.pp.strainsAll + * @property {object[]} menu.pp.strainsAll.series + * @property {'aim' | 'aimNoSliders' | 'flashlight' | 'speed' | 'color' | 'rhythm' | 'stamina' | 'movement' | 'strains'} menu.pp.strainsAll.series.name + * @property {number[]} menu.pp.strainsAll.series.data + * @property {number[]} menu.pp.strainsAll.xaxis + * @property {object} gameplay + * @property {0 | 1 | 2 | 3} gameplay.gameMode + * @property {string} gameplay.name + * @property {number} gameplay.score + * @property {number} gameplay.accuracy + * @property {object} gameplay.combo + * @property {number} gameplay.combo.current + * @property {number} gameplay.combo.max + * @property {object} gameplay.hp + * @property {number} gameplay.hp.normal + * @property {number} gameplay.hp.smooth + * @property {object} gameplay.hits + * @property {number} gameplay.hits.0 + * @property {number} gameplay.hits.50 + * @property {number} gameplay.hits.100 + * @property {number} gameplay.hits.300 + * @property {number} gameplay.hits.geki This is also used as the 320's count in the osu!mania ruleset + * @property {number} gameplay.hits.katu This is also used as the 200's count in the osu!mania ruleset + * @property {number} gameplay.hits.sliderBreaks + * @property {object} gameplay.hits.grade + * @property {'XH' | 'X' | 'SH' | 'S' | 'A' | 'B' | 'C' | 'D'} gameplay.hits.grade.current + * @property {'XH' | 'X' | 'SH' | 'S' | 'A' | 'B' | 'C' | 'D'} gameplay.hits.grade.maxThisPlay + * @property {number} gameplay.hits.unstableRate + * @property {number[]} gameplay.hits.hitErrorArray + * @property {object} gameplay.pp + * @property {number} gameplay.pp.current + * @property {number} gameplay.pp.fc + * @property {number} gameplay.pp.maxThisPlay + * @property {object} gameplay.keyOverlay + * @property {object} gameplay.keyOverlay.k1 + * @property {boolean} gameplay.keyOverlay.k1.isPressed + * @property {number} gameplay.keyOverlay.k1.count + * @property {object} gameplay.keyOverlay.k2 + * @property {boolean} gameplay.keyOverlay.k2.isPressed + * @property {number} gameplay.keyOverlay.k2.count + * @property {object} gameplay.keyOverlay.m1 + * @property {boolean} gameplay.keyOverlay.m1.isPressed + * @property {number} gameplay.keyOverlay.m1.count + * @property {object} gameplay.keyOverlay.m2 + * @property {boolean} gameplay.keyOverlay.m2.isPressed + * @property {number} gameplay.keyOverlay.m2.count + * @property {object} gameplay.leaderboard + * @property {boolean} gameplay.leaderboard.hasLeaderboard + * @property {boolean} gameplay.leaderboard.isVisible + * @property {object} gameplay.leaderboard.ourplayer + * @property {string} gameplay.leaderboard.ourplayer.name + * @property {number} gameplay.leaderboard.ourplayer.score + * @property {number} gameplay.leaderboard.ourplayer.combo + * @property {number} gameplay.leaderboard.ourplayer.maxCombo + * @property {string} gameplay.leaderboard.ourplayer.mods + * @property {number} gameplay.leaderboard.ourplayer.h300 + * @property {number} gameplay.leaderboard.ourplayer.h100 + * @property {number} gameplay.leaderboard.ourplayer.h50 + * @property {number} gameplay.leaderboard.ourplayer.h0 + * @property {number} gameplay.leaderboard.ourplayer.team + * @property {number} gameplay.leaderboard.ourplayer.position + * @property {number} gameplay.leaderboard.ourplayer.isPassing + * @property {object[]} gameplay.leaderboard.slots + * @property {string} gameplay.leaderboard.slots.name + * @property {number} gameplay.leaderboard.slots.score + * @property {number} gameplay.leaderboard.slots.combo + * @property {number} gameplay.leaderboard.slots.maxCombo + * @property {string} gameplay.leaderboard.slots.mods + * @property {number} gameplay.leaderboard.slots.h300 + * @property {number} gameplay.leaderboard.slots.h100 + * @property {number} gameplay.leaderboard.slots.h50 + * @property {number} gameplay.leaderboard.slots.h0 + * @property {number} gameplay.leaderboard.slots.team + * @property {number} gameplay.leaderboard.slots.position + * @property {number} gameplay.leaderboard.slots.isPassing + * @property {boolean} gameplay._isReplayUiHidden + * @property {object} resultsScreen + * @property {number} resultsScreen.0 + * @property {number} resultsScreen.50 + * @property {number} resultsScreen.100 + * @property {number} resultsScreen.300 + * @property {0 | 1 | 2 | 3} resultsScreen.mode + * @property {string} resultsScreen.name + * @property {number} resultsScreen.score + * @property {number} resultsScreen.accuracy + * @property {number} resultsScreen.maxCombo + * @property {object} resultsScreen.mods + * @property {number} resultsScreen.mods.num + * @property {string} resultsScreen.mods.str + * @property {number} resultsScreen.geki This is also used as the 320's count in the osu!mania ruleset + * @property {number} resultsScreen.katu This is also used as the 200's count in the osu!mania ruleset + * @property {'XH' | 'X' | 'SH' | 'S' | 'A' | 'B' | 'C' | 'D'} resultsScreen.grade + * @property {string} resultsScreen.createdAt + * @property {object} userProfile + * @property {0 | 256 | 257 | 65537 | 65793} userProfile.rawLoginStatus + * @property {string} userProfile.name + * @property {number} userProfile.accuracy + * @property {number} userProfile.rankedScore + * @property {number} userProfile.id + * @property {number} userProfile.level + * @property {number} userProfile.playCount + * @property {0 | 1 | 2 | 3} userProfile.playMode + * @property {number} userProfile.rank + * @property {number} userProfile.countryCode + * @property {number} userProfile.performancePoints + * @property {0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13} userProfile.rawBanchoStatus + * @property {string} userProfile.backgroundColour + * @property {object} tourney + * @property {object} tourney.manager + * @property {number} tourney.manager.ipcState + * @property {number} tourney.manager.bestOF + * @property {object} tourney.manager.teamName + * @property {string} tourney.manager.teamName.left + * @property {string} tourney.manager.teamName.right + * @property {object} tourney.manager.stars + * @property {number} tourney.manager.stars.left + * @property {number} tourney.manager.stars.right + * @property {object} tourney.manager.bools + * @property {boolean} tourney.manager.bools.scoreVisible + * @property {boolean} tourney.manager.bools.starsVisible + * @property {object[]} tourney.manager.chat + * @property {string} tourney.manager.chat.team + * @property {string} tourney.manager.chat.time + * @property {string} tourney.manager.chat.name + * @property {string} tourney.manager.chat.messageBody + * @property {object} tourney.manager.gameplay + * @property {object} tourney.manager.gameplay.score + * @property {number} tourney.manager.gameplay.score.left + * @property {number} tourney.manager.gameplay.score.right + * @property {object[]} tourney.ipcClients + * @property {string} tourney.ipcClients.team + * @property {object} tourney.ipcClients.spectating + * @property {string} tourney.ipcClients.spectating.name + * @property {string} tourney.ipcClients.spectating.country + * @property {number} tourney.ipcClients.spectating.userID + * @property {number} tourney.ipcClients.spectating.accuracy + * @property {number} tourney.ipcClients.spectating.rankedScore + * @property {number} tourney.ipcClients.spectating.playCount + * @property {number} tourney.ipcClients.spectating.globalRank + * @property {number} tourney.ipcClients.spectating.totalPP + * @property {object} tourney.ipcClients.gameplay + * @property {0 | 1 | 2 | 3} tourney.ipcClients.gameplay.gameMode + * @property {string} tourney.ipcClients.gameplay.name + * @property {number} tourney.ipcClients.gameplay.score + * @property {number} tourney.ipcClients.gameplay.accuracy + * @property {object} tourney.ipcClients.gameplay.combo + * @property {number} tourney.ipcClients.gameplay.combo.current + * @property {number} tourney.ipcClients.gameplay.combo.max + * @property {object} tourney.ipcClients.gameplay.hp + * @property {number} tourney.ipcClients.gameplay.hp.normal + * @property {number} tourney.ipcClients.gameplay.hp.smooth + * @property {object} tourney.ipcClients.gameplay.hits + * @property {number} tourney.ipcClients.gameplay.hits.0 + * @property {number} tourney.ipcClients.gameplay.hits.50 + * @property {number} tourney.ipcClients.gameplay.hits.100 + * @property {number} tourney.ipcClients.gameplay.hits.300 + * @property {number} tourney.ipcClients.gameplay.hits.geki This is also used as the 320's count in the osu!mania ruleset + * @property {number} tourney.ipcClients.gameplay.hits.katu This is also used as the 200's count in the osu!mania ruleset + * @property {number} tourney.ipcClients.gameplay.hits.sliderBreaks + * @property {number} tourney.ipcClients.gameplay.hits.unstableRate + * @property {number[]} tourney.ipcClients.gameplay.hits.hitErrorArray + * @property {object} tourney.ipcClients.gameplay.mods + * @property {number} tourney.ipcClients.gameplay.mods.num + * @property {string} tourney.ipcClients.gameplay.mods.str + */ + + + +/** @typedef {object} WEBSOCKET_V2 + * @property {'stable' | 'lazer'} client + * @property {string} server + * @property {object} state + * @property {0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23} state.number + * @property {'menu' | 'edit' | 'play' | 'exit' | 'selectEdit' | 'selectPlay' | 'selectDrawings' | 'resultScreen' | 'update' | 'busy' | 'unknown' | 'lobby' | 'matchSetup' | 'selectMulti' | 'rankingVs' | 'onlineSelection' | 'optionsOffsetWizard' | 'rankingTagCoop' | 'rankingTeam' | 'beatmapImport' | 'packageUpdater' | 'benchmark' | 'tourney' | 'charts'} state.name + * @property {object} session + * @property {number} session.playTime + * @property {number} session.playCount + * @property {object} settings + * @property {boolean} settings.interfaceVisible + * @property {boolean} settings.replayUIVisible + * @property {object} settings.chatVisibilityStatus + * @property {0 | 1 | 2} settings.chatVisibilityStatus.number + * @property {'hidden' | 'visible' | 'visibleWithFriendsList'} settings.chatVisibilityStatus.name + * @property {object} settings.leaderboard + * @property {boolean} settings.leaderboard.visible + * @property {object} settings.leaderboard.type + * @property {0 | 1 | 2 | 3 | 4} settings.leaderboard.type.number + * @property {'local' | 'global' | 'selectedmods' | 'friends' | 'country'} settings.leaderboard.type.name + * @property {object} settings.progressBar + * @property {0 | 1 | 2 | 3 | 4} settings.progressBar.number + * @property {'off' | 'pie' | 'topRight' | 'bottomRight' | 'bottom'} settings.progressBar.name + * @property {number} settings.bassDensity + * @property {object} settings.resolution + * @property {boolean} settings.resolution.fullscreen + * @property {number} settings.resolution.width + * @property {number} settings.resolution.height + * @property {number} settings.resolution.widthFullscreen + * @property {number} settings.resolution.heightFullscreen + * @property {object} settings.client + * @property {boolean} settings.client.updateAvailable + * @property {0 | 1 | 2 | 3} settings.client.branch - 0: Cutting Edge + * - 1: Stable + * - 2: Beta + * - 3: Stable (Fallback) + * @property {string} settings.client.version The full build version, e.g. `b20241029cuttingedge` + * @property {object} settings.scoreMeter + * @property {object} settings.scoreMeter.type + * @property {0 | 1 | 2} settings.scoreMeter.type.number + * @property {'none' | 'colour' | 'error'} settings.scoreMeter.type.name + * @property {number} settings.scoreMeter.size + * @property {object} settings.cursor + * @property {boolean} settings.cursor.useSkinCursor + * @property {boolean} settings.cursor.autoSize + * @property {number} settings.cursor.size + * @property {object} settings.mouse + * @property {boolean} settings.mouse.rawInput + * @property {boolean} settings.mouse.disableButtons + * @property {boolean} settings.mouse.disableWheel + * @property {number} settings.mouse.sensitivity + * @property {object} settings.mania + * @property {boolean} settings.mania.speedBPMScale + * @property {boolean} settings.mania.usePerBeatmapSpeedScale + * @property {object} settings.sort + * @property {0 | 1 | 2 | 3 | 4 | 5 | 6 | 7} settings.sort.number + * @property {'artist' | 'bpm' | 'creator' | 'date' | 'difficulty' | 'length' | 'rank' | 'title'} settings.sort.name + * @property {object} settings.group + * @property {0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19} settings.group.number + * @property {'none' | 'artist' | 'bPM' | 'creator' | 'date' | 'difficulty' | 'length' | 'rank' | 'myMaps' | 'search' | 'show_All' | 'title' | 'lastPlayed' | 'onlineFavourites' | 'maniaKeys' | 'mode' | 'collection' | 'rankedStatus'} settings.group.name Note: `search` and `show_All` share the same number - `12` + * @property {object} settings.skin + * @property {boolean} settings.skin.useDefaultSkinInEditor + * @property {boolean} settings.skin.ignoreBeatmapSkins + * @property {boolean} settings.skin.tintSliderBall + * @property {boolean} settings.skin.useTaikoSkin + * @property {string} settings.skin.name + * @property {object} settings.mode + * @property {0 | 1 | 2 | 3} settings.mode.number + * @property {'osu' | 'taiko' | 'fruits' | 'mania'} settings.mode.name + * @property {object} settings.audio + * @property {boolean} settings.audio.ignoreBeatmapSounds + * @property {boolean} settings.audio.useSkinSamples + * @property {object} settings.audio.volume + * @property {number} settings.audio.volume.master + * @property {number} settings.audio.volume.music + * @property {number} settings.audio.volume.effect + * @property {object} settings.audio.offset + * @property {number} settings.audio.offset.universal + * @property {object} settings.background + * @property {number} settings.background.dim + * @property {boolean} settings.background.video + * @property {boolean} settings.background.storyboard + * @property {object} settings.keybinds + * @property {object} settings.keybinds.osu + * @property {string} settings.keybinds.osu.k1 + * @property {string} settings.keybinds.osu.k2 + * @property {string} settings.keybinds.osu.smokeKey + * @property {object} settings.keybinds.fruits + * @property {string} settings.keybinds.fruits.k1 + * @property {string} settings.keybinds.fruits.k2 + * @property {string} settings.keybinds.fruits.Dash + * @property {object} settings.keybinds.taiko + * @property {string} settings.keybinds.taiko.innerLeft + * @property {string} settings.keybinds.taiko.innerRight + * @property {string} settings.keybinds.taiko.outerLeft + * @property {string} settings.keybinds.taiko.outerRight + * @property {string} settings.keybinds.quickRetry + * @property {object} profile + * @property {object} profile.userStatus + * @property {0 | 256 | 257 | 65537 | 65793} profile.userStatus.number + * @property {'reconnecting' | 'guest' | 'recieving_data' | 'disconnected' | 'connected'} profile.userStatus.name + * @property {object} profile.banchoStatus + * @property {0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13} profile.banchoStatus.number + * @property {'idle' | 'afk' | 'playing' | 'editing' | 'modding' | 'multiplayer' | 'watching' | 'unknown' | 'testing' | 'submitting' | 'paused' | 'lobby' | 'multiplaying' | 'osuDirect'} profile.banchoStatus.name + * @property {number} profile.id + * @property {string} profile.name + * @property {object} profile.mode + * @property {0 | 1 | 2 | 3} profile.mode.number + * @property {'osu' | 'taiko' | 'fruits' | 'mania'} profile.mode.name + * @property {number} profile.rankedScore + * @property {number} profile.level + * @property {number} profile.accuracy + * @property {number} profile.pp + * @property {number} profile.playCount + * @property {number} profile.globalRank + * @property {object} profile.countryCode + * @property {number} profile.countryCode.number + * @property {string} profile.countryCode.name + * @property {string} profile.backgroundColour + * @property {object} beatmap + * @property {boolean} beatmap.isKiai + * @property {boolean} beatmap.isBreak + * @property {boolean} beatmap.isConvert + * @property {object} beatmap.time + * @property {number} beatmap.time.live + * @property {number} beatmap.time.firstObject + * @property {number} beatmap.time.lastObject + * @property {number} beatmap.time.mp3Length + * @property {object} beatmap.status + * @property {0 | 1 | 2 | 4 | 5 | 6 | 7} beatmap.status.number + * @property {'unknown' | 'notSubmitted' | 'pending' | 'ranked' | 'approved' | 'qualified' | 'loved'} beatmap.status.name + * @property {string} beatmap.checksum + * @property {number} beatmap.id + * @property {number} beatmap.set + * @property {object} beatmap.mode + * @property {0 | 1 | 2 | 3} beatmap.mode.number + * @property {'osu' | 'taiko' | 'fruits' | 'mania'} beatmap.mode.name + * @property {string} beatmap.artist + * @property {string} beatmap.artistUnicode + * @property {string} beatmap.title + * @property {string} beatmap.titleUnicode + * @property {string} beatmap.mapper + * @property {string} beatmap.version + * @property {object} beatmap.stats + * @property {object} beatmap.stats.stars + * @property {number} beatmap.stats.stars.live + * @property {number} [beatmap.stats.stars.aim] This is available only in the osu! ruleset + * @property {number} [beatmap.stats.stars.speed] This is available only in the osu! ruleset + * @property {number} [beatmap.stats.stars.flashlight] This is available only in the osu! ruleset + * @property {number} [beatmap.stats.stars.sliderFactor] This is available only in the osu! ruleset + * @property {number} [beatmap.stats.stars.stamina] This is available only in the osu!taiko ruleset + * @property {number} [beatmap.stats.stars.rhythm] This is available only in the osu!taiko ruleset + * @property {number} [beatmap.stats.stars.color] This is available only in the osu!taiko ruleset + * @property {number} [beatmap.stats.stars.peak] This is available only in the osu!taiko ruleset + * @property {number} [beatmap.stats.stars.hitWindow] 300's hit window; this is available only in the osu!mania ruleset + * @property {number} beatmap.stats.stars.total + * @property {object} beatmap.stats.ar + * @property {number} beatmap.stats.ar.original + * @property {number} beatmap.stats.ar.converted + * @property {object} beatmap.stats.cs + * @property {number} beatmap.stats.cs.original + * @property {number} beatmap.stats.cs.converted + * @property {object} beatmap.stats.od + * @property {number} beatmap.stats.od.original + * @property {number} beatmap.stats.od.converted + * @property {object} beatmap.stats.hp + * @property {number} beatmap.stats.hp.original + * @property {number} beatmap.stats.hp.converted + * @property {object} beatmap.stats.bpm + * @property {number} beatmap.stats.bpm.realtime + * @property {number} beatmap.stats.bpm.common + * @property {number} beatmap.stats.bpm.min + * @property {number} beatmap.stats.bpm.max + * @property {object} beatmap.stats.objects + * @property {number} beatmap.stats.objects.circles + * @property {number} beatmap.stats.objects.sliders + * @property {number} beatmap.stats.objects.spinners + * @property {number} beatmap.stats.objects.holds + * @property {number} beatmap.stats.objects.total + * @property {number} beatmap.stats.maxCombo + * @property {object} play + * @property {string} play.playerName + * @property {object} play.mode + * @property {0 | 1 | 2 | 3} play.mode.number + * @property {'osu' | 'taiko' | 'fruits' | 'mania'} play.mode.name + * @property {number} play.score + * @property {number} play.accuracy + * @property {object} play.healthBar + * @property {number} play.healthBar.normal + * @property {number} play.healthBar.smooth + * @property {object} play.hits + * @property {number} play.hits.0 + * @property {number} play.hits.50 + * @property {number} play.hits.100 + * @property {number} play.hits.300 + * @property {number} play.hits.geki This is also used as the 320's count in the osu!mania ruleset + * @property {number} play.hits.katu This is also used as the 200's count in the osu!mania ruleset + * @property {number} play.hits.sliderBreaks + * @property {number} play.hits.sliderEndHits This is populated only when playing osu!(lazer) + * @property {number} play.hits.sliderTickHits This is populated only when playing osu!(lazer) + * @property {number[]} play.hitErrorArray + * @property {object} play.combo + * @property {number} play.combo.current + * @property {number} play.combo.max + * @property {object} play.mods + * @property {string} play.mods.checksum + * @property {number} play.mods.number + * @property {string} play.mods.name + * @property {object[]} play.mods.array + * @property {string} play.mods.array.acronym + * @property {object} [play.mods.array.settings] This exists only when playing osu!(lazer). You must get the settings manually, e.g. from the `/json/v2` response preview + * @property {number} play.mods.rate + * @property {object} play.rank + * @property {'XH' | 'X' | 'SH' | 'S' | 'A' | 'B' | 'C' | 'D'} play.rank.current + * @property {'XH' | 'X' | 'SH' | 'S' | 'A' | 'B' | 'C' | 'D'} play.rank.maxThisPlay + * @property {object} play.pp + * @property {number} play.pp.current + * @property {number} play.pp.fc + * @property {number} play.pp.maxAchieved + * @property {number} play.pp.maxAchievable + * @property {object} play.pp.detailed + * @property {object} play.pp.detailed.current + * @property {number} play.pp.detailed.current.aim + * @property {number} play.pp.detailed.current.speed + * @property {number} play.pp.detailed.current.accuracy + * @property {number} play.pp.detailed.current.difficulty + * @property {number} play.pp.detailed.current.flashlight + * @property {number} play.pp.detailed.current.total + * @property {object} play.pp.detailed.fc + * @property {number} play.pp.detailed.fc.aim + * @property {number} play.pp.detailed.fc.speed + * @property {number} play.pp.detailed.fc.accuracy + * @property {number} play.pp.detailed.fc.difficulty + * @property {number} play.pp.detailed.fc.flashlight + * @property {number} play.pp.detailed.fc.total + * @property {number} play.unstableRate + * @property {object[]} leaderboard + * @property {boolean} leaderboard.isFailed + * @property {number} leaderboard.position + * @property {number} leaderboard.team + * @property {number} leaderboard.id + * @property {string} leaderboard.name + * @property {number} leaderboard.score + * @property {number} leaderboard.accuracy + * @property {object} leaderboard.hits + * @property {number} leaderboard.hits.0 + * @property {number} leaderboard.hits.50 + * @property {number} leaderboard.hits.100 + * @property {number} leaderboard.hits.300 + * @property {number} leaderboard.hits.geki This is also used as the 320's count in the osu!mania ruleset + * @property {number} leaderboard.hits.katu This is also used as the 200's count in the osu!mania ruleset + * @property {object} leaderboard.combo + * @property {number} leaderboard.combo.current + * @property {number} leaderboard.combo.max + * @property {object} leaderboard.mods + * @property {string} leaderboard.mods.checksum + * @property {number} leaderboard.mods.number + * @property {string} leaderboard.mods.name + * @property {object[]} leaderboard.mods.array + * @property {string} leaderboard.mods.array.acronym + * @property {object} [leaderboard.mods.array.settings] This exists only when playing osu!(lazer). You must get the settings manually, e.g. from the `/json/v2` response preview + * @property {number} leaderboard.mods.rate + * @property {'XH' | 'X' | 'SH' | 'S' | 'A' | 'B' | 'C' | 'D'} leaderboard.rank + * @property {object} performance + * @property {object} performance.accuracy + * @property {number} performance.accuracy.90 + * @property {number} performance.accuracy.91 + * @property {number} performance.accuracy.92 + * @property {number} performance.accuracy.93 + * @property {number} performance.accuracy.94 + * @property {number} performance.accuracy.95 + * @property {number} performance.accuracy.96 + * @property {number} performance.accuracy.97 + * @property {number} performance.accuracy.98 + * @property {number} performance.accuracy.99 + * @property {number} performance.accuracy.100 + * @property {object} performance.graph + * @property {object[]} performance.graph.series + * @property {'aim' | 'aimNoSliders' | 'flashlight' | 'speed' | 'color' | 'rhythm' | 'stamina' | 'movement' | 'strains'} performance.graph.series.name + * @property {number[]} performance.graph.series.data + * @property {number[]} performance.graph.xaxis + * @property {object} resultsScreen + * @property {number} resultsScreen.scoreId + * @property {string} resultsScreen.playerName + * @property {object} resultsScreen.mode + * @property {0 | 1 | 2 | 3} resultsScreen.mode.number + * @property {'osu' | 'taiko' | 'fruits' | 'mania'} resultsScreen.mode.name + * @property {number} resultsScreen.score + * @property {number} resultsScreen.accuracy + * @property {object} resultsScreen.hits + * @property {number} resultsScreen.hits.0 + * @property {number} resultsScreen.hits.50 + * @property {number} resultsScreen.hits.100 + * @property {number} resultsScreen.hits.300 + * @property {number} resultsScreen.hits.geki This is also used as the 320's count in the osu!mania ruleset + * @property {number} resultsScreen.hits.katu This is also used as the 200's count in the osu!mania ruleset + * @property {number} resultsScreen.hits.sliderEndHits This is populated only when playing osu!(lazer) + * @property {number} resultsScreen.hits.sliderTickHits This is populated only when playing osu!(lazer) + * @property {object} resultsScreen.mods + * @property {string} resultsScreen.mods.checksum + * @property {number} resultsScreen.mods.number + * @property {string} resultsScreen.mods.name + * @property {object[]} resultsScreen.mods.array + * @property {string} resultsScreen.mods.array.acronym + * @property {object} [resultsScreen.mods.array.settings] This exists only when playing osu!(lazer). You must get the settings manually, e.g. from the `/json/v2` response preview + * @property {number} resultsScreen.mods.rate + * @property {number} resultsScreen.maxCombo + * @property {'XH' | 'X' | 'SH' | 'S' | 'A' | 'B' | 'C' | 'D'} resultsScreen.rank + * @property {object} resultsScreen.pp + * @property {number} resultsScreen.pp.current + * @property {number} resultsScreen.pp.fc + * @property {string} resultsScreen.createdAt + * @property {object} folders + * @property {string} folders.game + * @property {string} folders.skin + * @property {string} folders.songs + * @property {string} folders.beatmap + * @property {object} files + * @property {string} files.beatmap + * @property {string} files.background + * @property {string} files.audio + * @property {object} directPath + * @property {string} directPath.beatmapFile + * @property {string} directPath.beatmapBackground + * @property {string} directPath.beatmapAudio + * @property {string} directPath.beatmapFolder + * @property {string} directPath.skinFolder + * @property {object} tourney + * @property {boolean} tourney.scoreVisible + * @property {boolean} tourney.starsVisible + * @property {number} tourney.ipcState + * @property {number} tourney.bestOF + * @property {object} tourney.team + * @property {string} tourney.team.left + * @property {string} tourney.team.right + * @property {object} tourney.points + * @property {number} tourney.points.left + * @property {number} tourney.points.right + * @property {object[]} tourney.chat + * @property {string} tourney.chat.team + * @property {string} tourney.chat.name + * @property {string} tourney.chat.message + * @property {string} tourney.chat.timestamp + * @property {object} tourney.totalScore + * @property {number} tourney.totalScore.left + * @property {number} tourney.totalScore.right + * @property {object[]} tourney.clients + * @property {number} tourney.clients.ipcId + * @property {'left' | 'right'} tourney.clients.team + * @property {object} tourney.clients.user + * @property {number} tourney.clients.user.id + * @property {string} tourney.clients.user.name + * @property {string} tourney.clients.user.country + * @property {number} tourney.clients.user.accuracy + * @property {number} tourney.clients.user.rankedScore + * @property {number} tourney.clients.user.playCount + * @property {number} tourney.clients.user.globalRank + * @property {number} tourney.clients.user.totalPP + * @property {object} tourney.clients.beatmap + * @property {object} tourney.clients.beatmap.stats + * @property {object} tourney.clients.beatmap.stats.stars + * @property {number} tourney.clients.beatmap.stats.stars.live + * @property {number} [tourney.clients.beatmap.stats.stars.aim] This is available only in the osu! ruleset + * @property {number} [tourney.clients.beatmap.stats.stars.speed] This is available only in the osu! ruleset + * @property {number} [tourney.clients.beatmap.stats.stars.flashlight] This is available only in the osu! ruleset + * @property {number} [tourney.clients.beatmap.stats.stars.sliderFactor] This is available only in the osu! ruleset + * @property {number} [tourney.clients.beatmap.stats.stars.stamina] This is available only in the osu!taiko ruleset + * @property {number} [tourney.clients.beatmap.stats.stars.rhythm] This is available only in the osu!taiko ruleset + * @property {number} [tourney.clients.beatmap.stats.stars.color] This is available only in the osu!taiko ruleset + * @property {number} [tourney.clients.beatmap.stats.stars.peak] This is available only in the osu!taiko ruleset + * @property {number} [tourney.clients.beatmap.stats.stars.hitWindow] 300's hit window; this is available only in the osu!mania ruleset + * @property {number} tourney.clients.beatmap.stats.stars.total + * @property {object} tourney.clients.beatmap.stats.ar + * @property {number} tourney.clients.beatmap.stats.ar.original + * @property {number} tourney.clients.beatmap.stats.ar.converted + * @property {object} tourney.clients.beatmap.stats.cs + * @property {number} tourney.clients.beatmap.stats.cs.original + * @property {number} tourney.clients.beatmap.stats.cs.converted + * @property {object} tourney.clients.beatmap.stats.od + * @property {number} tourney.clients.beatmap.stats.od.original + * @property {number} tourney.clients.beatmap.stats.od.converted + * @property {object} tourney.clients.beatmap.stats.hp + * @property {number} tourney.clients.beatmap.stats.hp.original + * @property {number} tourney.clients.beatmap.stats.hp.converted + * @property {object} tourney.clients.beatmap.stats.bpm + * @property {number} tourney.clients.beatmap.stats.bpm.realtime + * @property {number} tourney.clients.beatmap.stats.bpm.common + * @property {number} tourney.clients.beatmap.stats.bpm.min + * @property {number} tourney.clients.beatmap.stats.bpm.max + * @property {object} tourney.clients.beatmap.stats.objects + * @property {number} tourney.clients.beatmap.stats.objects.circles + * @property {number} tourney.clients.beatmap.stats.objects.sliders + * @property {number} tourney.clients.beatmap.stats.objects.spinners + * @property {number} tourney.clients.beatmap.stats.objects.holds + * @property {number} tourney.clients.beatmap.stats.objects.total + * @property {number} tourney.clients.beatmap.stats.maxCombo + * @property {object} tourney.clients.play + * @property {string} tourney.clients.play.playerName + * @property {object} tourney.clients.play.mode + * @property {0 | 1 | 2 | 3} tourney.clients.play.mode.number + * @property {'osu' | 'taiko' | 'fruits' | 'mania'} tourney.clients.play.mode.name + * @property {number} tourney.clients.play.score + * @property {number} tourney.clients.play.accuracy + * @property {object} tourney.clients.play.healthBar + * @property {number} tourney.clients.play.healthBar.normal + * @property {number} tourney.clients.play.healthBar.smooth + * @property {object} tourney.clients.play.hits + * @property {number} tourney.clients.play.hits.0 + * @property {number} tourney.clients.play.hits.50 + * @property {number} tourney.clients.play.hits.100 + * @property {number} tourney.clients.play.hits.300 + * @property {number} tourney.clients.play.hits.geki This is also used as the 320's count in the osu!mania ruleset + * @property {number} tourney.clients.play.hits.katu This is also used as the 200's count in the osu!mania ruleset + * @property {number} tourney.clients.play.hits.sliderBreaks + * @property {number} tourney.clients.play.hits.sliderEndHits This is populated only when playing osu!(lazer) + * @property {number} tourney.clients.play.hits.sliderTickHits This is populated only when playing osu!(lazer) + * @property {number[]} tourney.clients.play.hitErrorArray + * @property {object} tourney.clients.play.combo + * @property {number} tourney.clients.play.combo.current + * @property {number} tourney.clients.play.combo.max + * @property {object} tourney.clients.play.mods + * @property {string} tourney.clients.play.mods.checksum + * @property {number} tourney.clients.play.mods.number + * @property {string} tourney.clients.play.mods.name + * @property {object[]} tourney.clients.play.mods.array + * @property {string} tourney.clients.play.mods.array.acronym + * @property {object} [tourney.clients.play.mods.array.settings] This exists only when playing osu!(lazer). You must get the settings manually, e.g. from the `/json/v2` response preview + * @property {number} tourney.clients.play.mods.rate + * @property {object} tourney.clients.play.rank + * @property {'XH' | 'X' | 'SH' | 'S' | 'A' | 'B' | 'C' | 'D'} tourney.clients.play.rank.current + * @property {'XH' | 'X' | 'SH' | 'S' | 'A' | 'B' | 'C' | 'D'} tourney.clients.play.rank.maxThisPlay + * @property {object} tourney.clients.play.pp + * @property {number} tourney.clients.play.pp.current + * @property {number} tourney.clients.play.pp.fc + * @property {number} tourney.clients.play.pp.maxAchieved + * @property {object} tourney.clients.play.pp.detailed + * @property {object} tourney.clients.play.pp.detailed.current + * @property {number} tourney.clients.play.pp.detailed.current.aim + * @property {number} tourney.clients.play.pp.detailed.current.speed + * @property {number} tourney.clients.play.pp.detailed.current.accuracy + * @property {number} tourney.clients.play.pp.detailed.current.difficulty + * @property {number} tourney.clients.play.pp.detailed.current.flashlight + * @property {number} tourney.clients.play.pp.detailed.current.total + * @property {object} tourney.clients.play.pp.detailed.fc + * @property {number} tourney.clients.play.pp.detailed.fc.aim + * @property {number} tourney.clients.play.pp.detailed.fc.speed + * @property {number} tourney.clients.play.pp.detailed.fc.accuracy + * @property {number} tourney.clients.play.pp.detailed.fc.difficulty + * @property {number} tourney.clients.play.pp.detailed.fc.flashlight + * @property {number} tourney.clients.play.pp.detailed.fc.total + * @property {number} tourney.clients.play.unstableRate + */ + + + +/** @typedef {object} WEBSOCKET_V2_PRECISE + * @property {number} currentTime + * @property {object} keys + * @property {object} keys.k1 + * @property {boolean} keys.k1.isPressed + * @property {number} keys.k1.count + * @property {object} keys.k2 + * @property {boolean} keys.k2.isPressed + * @property {number} keys.k2.count + * @property {object} keys.m1 + * @property {boolean} keys.m1.isPressed + * @property {number} keys.m1.count + * @property {object} keys.m2 + * @property {boolean} keys.m2.isPressed + * @property {number} keys.m2.count + * @property {number[]} hitErrors + * @property {object[]} tourney + * @property {number} tourney.ipcId + * @property {number[]} tourney.hitErrors + * @property {object} tourney.keys + * @property {object} tourney.keys.k1 + * @property {boolean} tourney.keys.k1.isPressed + * @property {number} tourney.keys.k1.count + * @property {object} tourney.keys.k2 + * @property {boolean} tourney.keys.k2.isPressed + * @property {number} tourney.keys.k2.count + * @property {object} tourney.keys.m1 + * @property {boolean} tourney.keys.m1.isPressed + * @property {number} tourney.keys.m1.count + * @property {object} tourney.keys.m2 + * @property {boolean} tourney.keys.m2.isPressed + * @property {number} tourney.keys.m2.count + */ \ No newline at end of file diff --git a/counters/Pups-Overlay by Puparia/index.css b/counters/Pups-Overlay by Puparia/index.css new file mode 100644 index 00000000..91357ba4 --- /dev/null +++ b/counters/Pups-Overlay by Puparia/index.css @@ -0,0 +1,952 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap'); + +/* OBS Browser Compatibility */ +* { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body, +html { + overflow: hidden; + background: transparent; + font-family: 'Inter', sans-serif; + color: #ffffff; + font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11'; +} + +#pups-container { + position: relative; + width: 100vw; + height: 100vh; + pointer-events: none; +} + +.progress-bar-top, +.progress-bar-bottom { + position: absolute; + left: 0; + width: 100%; + height: 3px; + background: transparent; + z-index: 10; + overflow: visible; +} + +.progress-bar-top { + top: 105px; +} + +.progress-bar-bottom { + bottom: 105px; +} + +/* Hide progress bars based on position and adjust position */ +.position-top .progress-bar-top { + display: none; +} + +.position-top .progress-bar-bottom { + bottom: 5px; +} + +.position-bottom .progress-bar-bottom { + display: none; +} + +.position-bottom .progress-bar-top { + top: 5px; +} + +.progress-fill { + height: 3px !important; + background: #a855f7 !important; + background-image: none !important; + box-shadow: none !important; + filter: none !important; + mix-blend-mode: normal !important; + width: 0%; + -webkit-transition: width 0.1s linear; + transition: width 0.1s linear; + position: relative; +} + +.progress-particle { + position: absolute; + width: 2px; + height: 2px; + background: rgb(235, 25, 25); + border-radius: 50%; + -webkit-animation: particleRise 2s ease-in infinite; + animation: particleRise 2s ease-in infinite; +} + +.progress-bar-top, +.progress-bar-bottom { + background: #0b0b0b !important; + /* rail neutre et opaque */ +} + +@-webkit-keyframes particleRise { + 0% { + -webkit-transform: translateY(0) scale(1); + transform: translateY(0) scale(1); + opacity: 1; + } + + 100% { + -webkit-transform: translateY(-15px) scale(0); + transform: translateY(-15px) scale(0); + opacity: 0; + } +} + +@keyframes particleRise { + 0% { + transform: translateY(0) scale(1); + opacity: 1; + } + + 100% { + transform: translateY(-15px) scale(0); + opacity: 0; + } +} + +.ambient-light { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: transparent; + -webkit-animation: ambientShift 20s ease-in-out infinite; + animation: ambientShift 20s ease-in-out infinite; + z-index: 1; +} + +@-webkit-keyframes ambientShift { + + 0%, + 100% { + opacity: 0.3; + -webkit-transform: scale(1); + transform: scale(1); + } + + 50% { + opacity: 0.6; + -webkit-transform: scale(1.02); + transform: scale(1.02); + } +} + +@keyframes ambientShift { + + 0%, + 100% { + opacity: 0.3; + -webkit-transform: scale(1); + transform: scale(1); + } + + 50% { + opacity: 0.6; + -webkit-transform: scale(1.02); + transform: scale(1.02); + } +} + +.glass-panel { + position: absolute; + left: 0; + width: 100vw; + height: 100px; + display: flex; + align-items: center; + background: rgba(255, 255, 255, 0.02); + background: rgba(255, 255, 255, 0.05); + -webkit-backdrop-filter: blur(20px); + backdrop-filter: blur(20px); + padding: 20px 24px; + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + z-index: 10; + -webkit-transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); + transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); + -webkit-transform: translateX(-100%) scale(0.95); + transform: translateX(-100%) scale(0.95); + opacity: 0; +} + +.glass-panel.position-top { + top: 0px; + bottom: auto; + border-top: 1px solid rgba(255, 255, 255, 0.08); + border-bottom: none; +} + +.glass-panel.position-bottom { + bottom: 0px; + top: auto; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + border-top: none; +} + +.glass-panel.active { + -webkit-transform: translateX(0) scale(1); + transform: translateX(0) scale(1); + opacity: 1; +} + +.glass-panel.menu-mode { + background: rgba(255, 255, 255, 0.03); + box-shadow: + 0 12px 40px rgba(168, 85, 247, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.15); + justify-content: center; +} + +.glass-panel.gameplay-mode { + justify-content: center; +} + +.gameplay-stats, +.gameplay-stats-end { + display: flex; + align-items: center; + gap: 24px; + -webkit-transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); + transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +.glass-panel.menu-mode .gameplay-stats { + opacity: 0; + pointer-events: none; + -webkit-transform: translateX(-100px); + transform: translateX(-100px); +} + +.glass-panel.menu-mode .gameplay-stats-end { + opacity: 0; + pointer-events: none; + -webkit-transform: translateX(100px); + transform: translateX(100px); +} + +.glass-panel.gameplay-mode .gameplay-stats { + opacity: 1; + pointer-events: auto; + position: absolute; + left: 24px; + -webkit-transform: translateX(0); + transform: translateX(0); +} + +.glass-panel.gameplay-mode .gameplay-stats-end { + opacity: 1; + pointer-events: auto; + position: absolute; + right: 24px; + -webkit-transform: translateX(0); + transform: translateX(0); + display: flex; + align-items: center; + gap: 50px; +} + +.glass-panel.menu-mode .map-info { + border-color: transparent; + text-align: center; + position: absolute; + left: 50%; + top: 50%; + -webkit-transform: translate(-50%, -50%) scale(1.1); + transform: translate(-50%, -50%) scale(1.1); + display: flex; + align-items: center; + justify-content: center; +} + +.glass-panel.gameplay-mode .map-info { + text-align: center; + position: absolute; + left: 50%; + top: 50%; + -webkit-transform: translate(-50%, -50%) scale(1.3); + transform: translate(-50%, -50%) scale(1.3); + display: flex; + align-items: center; + justify-content: center; +} + + +.glass-panel.menu-mode .map-content { + align-items: center; + width: 100%; + text-align: center; +} + +.glass-panel.gameplay-mode .map-content { + align-items: center; + width: 100%; + text-align: center; +} + +.glass-panel.menu-mode .map-title { + font-size: 20px; + font-weight: 700; + text-shadow: none; +} + +.glass-panel.menu-mode .map-artist { + font-size: 16px; + color: rgba(255, 255, 255, 0.95); +} + +.glass-panel.menu-mode .map-meta { + justify-content: center; + font-size: 12px; + -webkit-animation: metaGlow 2.5s ease-in-out infinite; + animation: metaGlow 2.5s ease-in-out infinite; +} + +@-webkit-keyframes metaGlow { + + 0%, + 100% { + opacity: 0.7; + } + + 50% { + opacity: 1; + } +} + +@keyframes metaGlow { + + 0%, + 100% { + opacity: 0.7; + } + + 50% { + opacity: 1; + } +} + +.glass-panel.menu-mode .map-difficulty { + color: rgba(168, 85, 247, 0.9); + font-weight: 700; +} + +.metric-item { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding: 0 12px; + position: relative; + z-index: 2; + -webkit-transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); + transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +.metric-value { + font-family: 'JetBrains Mono', monospace; + font-size: 18px; + font-weight: 600; + color: #ffffff; + margin-bottom: 2px; + text-shadow: 0 0 12px rgba(255, 255, 255, 0.1); +} + +.metric-label { + font-size: 9px; + font-weight: 500; + color: rgba(255, 255, 255, 0.6); + text-transform: uppercase; + letter-spacing: 1px; +} + +.hit-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 10px 8px; + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 12px; + width: 65px; + min-width: 65px; + height: 50px; + position: relative; + z-index: 2; + -webkit-transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); + transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +.hit-item:hover { + background: rgba(255, 255, 255, 0.04); + border-color: rgba(255, 255, 255, 0.1); + -webkit-transform: translateY(-1px); + transform: translateY(-1px); +} + +.hit-value { + font-family: 'JetBrains Mono', monospace; + font-size: 16px; + font-weight: 600; + color: #ffffff; + margin-bottom: 3px; +} + +.hit-label { + font-size: 10px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.hit-300 .hit-label { + color: #10b981; +} + +.hit-100 .hit-label { + color: #3b82f6; +} + +.hit-50 .hit-label { + color: #f59e0b; +} + +.hit-miss .hit-label { + color: #ef4444; +} + +.map-info { + flex: 1; + padding: 0 16px; + border-left: 1px solid rgba(255, 255, 255, 0.1); + border-right: 1px solid rgba(255, 255, 255, 0.1); + position: relative; + z-index: 2; + -webkit-transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); + transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); + height: 60px; + display: flex; + align-items: center; +} + +.no-map-container { + display: none; +} + +.no-map-container.active { + display: none; +} + +@-webkit-keyframes fadeInScale { + from { + opacity: 0; + -webkit-transform: scale(0.9); + transform: scale(0.9); + } + + to { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); + } +} + +@keyframes fadeInScale { + from { + opacity: 0; + -webkit-transform: scale(0.9); + transform: scale(0.9); + } + + to { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); + } +} + +.no-map-particles { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 60px; + pointer-events: none; + overflow: hidden; +} + +.particle { + position: absolute; + width: 4px; + height: 4px; + background: linear-gradient(135deg, rgba(168, 85, 247, 0.6), rgba(99, 102, 241, 0.6)); + border-radius: 50%; + -webkit-animation: particleFloat 6s ease-in-out infinite; + animation: particleFloat 6s ease-in-out infinite; +} + +.particle:nth-child(1) { + left: 10%; + -webkit-animation-delay: 0s; + animation-delay: 0s; + -webkit-animation-duration: 7s; + animation-duration: 7s; +} + +.particle:nth-child(2) { + left: 30%; + -webkit-animation-delay: 1s; + animation-delay: 1s; + -webkit-animation-duration: 8s; + animation-duration: 8s; +} + +.particle:nth-child(3) { + left: 50%; + -webkit-animation-delay: 2s; + animation-delay: 2s; + -webkit-animation-duration: 6s; + animation-duration: 6s; +} + +.particle:nth-child(4) { + left: 70%; + -webkit-animation-delay: 1.5s; + animation-delay: 1.5s; + -webkit-animation-duration: 7.5s; + animation-duration: 7.5s; +} + +.particle:nth-child(5) { + left: 90%; + -webkit-animation-delay: 0.5s; + animation-delay: 0.5s; + -webkit-animation-duration: 9s; + animation-duration: 9s; +} + +@-webkit-keyframes particleFloat { + 0% { + bottom: -10px; + opacity: 0; + } + + 10% { + opacity: 1; + } + + 90% { + opacity: 1; + } + + 100% { + bottom: 130px; + opacity: 0; + } +} + +@keyframes particleFloat { + 0% { + bottom: -10px; + opacity: 0; + } + + 10% { + opacity: 1; + } + + 90% { + opacity: 1; + } + + 100% { + bottom: 130px; + opacity: 0; + } +} + +.no-map-icon { + position: relative; + display: flex; + gap: 20px; + align-items: center; + justify-content: center; + height: 60px; +} + +.icon-note { + font-size: 40px; + color: rgba(168, 85, 247, 0.7); + -webkit-animation: noteFloat 2.5s ease-in-out infinite; + animation: noteFloat 2.5s ease-in-out infinite; + text-shadow: 0 0 20px rgba(168, 85, 247, 0.4); +} + +.icon-note.delay-1 { + -webkit-animation-delay: 0.8s; + animation-delay: 0.8s; + color: rgba(99, 102, 241, 0.7); + text-shadow: 0 0 20px rgba(99, 102, 241, 0.4); +} + +.icon-note.delay-2 { + -webkit-animation-delay: 1.6s; + animation-delay: 1.6s; + color: rgba(168, 85, 247, 0.7); + text-shadow: 0 0 20px rgba(168, 85, 247, 0.4); +} + +@-webkit-keyframes noteFloat { + + 0%, + 100% { + -webkit-transform: translateY(0) scale(1); + transform: translateY(0) scale(1); + opacity: 0.7; + } + + 50% { + -webkit-transform: translateY(-15px) scale(1.1); + transform: translateY(-15px) scale(1.1); + opacity: 1; + } +} + +@keyframes noteFloat { + + 0%, + 100% { + -webkit-transform: translateY(0) scale(1); + transform: translateY(0) scale(1); + opacity: 0.7; + } + + 50% { + -webkit-transform: translateY(-15px) scale(1.1); + transform: translateY(-15px) scale(1.1); + opacity: 1; + } +} + +.no-map-text { + display: flex; + flex-direction: column; + gap: 6px; +} + +.text-line { + font-size: 16px; + font-weight: 600; + color: rgba(255, 255, 255, 0.85); + text-transform: uppercase; + letter-spacing: 2px; + -webkit-animation: textGlow 2s ease-in-out infinite; + animation: textGlow 2s ease-in-out infinite; +} + +.text-line-sub { + font-size: 12px; + font-weight: 400; + color: rgba(255, 255, 255, 0.5); + letter-spacing: 1.5px; + font-style: italic; +} + +@-webkit-keyframes textGlow { + + 0%, + 100% { + text-shadow: 0 0 10px rgba(168, 85, 247, 0.3); + } + + 50% { + text-shadow: 0 0 20px rgba(168, 85, 247, 0.6); + } +} + +@keyframes textGlow { + + 0%, + 100% { + text-shadow: 0 0 10px rgba(168, 85, 247, 0.3); + } + + 50% { + text-shadow: 0 0 20px rgba(168, 85, 247, 0.6); + } +} + +.map-content { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + -webkit-transition: all 0.5s ease; + transition: all 0.5s ease; +} + +.map-content.hidden { + display: none; +} + +.map-title-wrapper, +.map-artist-wrapper { + width: 100%; + overflow: hidden; + position: relative; +} + +.map-title { + font-size: 13px; + font-weight: 600; + color: #ffffff; + margin-bottom: 2px; + white-space: nowrap; + -webkit-transition: all 0.5s ease; + transition: all 0.5s ease; +} + +.map-title.scrolling { + display: inline-block; + -webkit-animation: marqueeScroll 12s linear infinite; + animation: marqueeScroll 12s linear infinite; +} + +.map-artist { + font-size: 11px; + font-weight: 400; + color: rgba(255, 255, 255, 0.7); + margin-bottom: 4px; + white-space: nowrap; + -webkit-transition: all 0.5s ease; + transition: all 0.5s ease; +} + +.map-artist.scrolling { + display: inline-block; + -webkit-animation: marqueeScroll 12s linear infinite; + animation: marqueeScroll 12s linear infinite; +} + +@-webkit-keyframes marqueeScroll { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + + 100% { + -webkit-transform: translateX(-100%); + transform: translateX(-100%); + } +} + +@keyframes marqueeScroll { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + + 100% { + -webkit-transform: translateX(-100%); + transform: translateX(-100%); + } +} + +.map-meta { + display: flex; + align-items: center; + gap: 6px; + font-size: 9px; + color: rgba(255, 255, 255, 0.5); + -webkit-transition: all 0.5s ease; + transition: all 0.5s ease; +} + +.map-difficulty { + font-weight: 600; + color: #6366f1; +} + +.map-separator { + opacity: 0.4; +} + +.map-bpm { + font-weight: 500; +} + +.mods-container { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 10px; + position: relative; + z-index: 2; + -webkit-transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); + transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +.mod-icon { + width: 40px; + height: 40px; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + -webkit-transition: all 0.3s ease; + transition: all 0.3s ease; +} + +.mod-icon:hover { + -webkit-transform: scale(1.1); + transform: scale(1.1); +} + +.specs-container { + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; + -webkit-transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); + transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +.spec-item { + display: inline-flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 0 8px; +} + +.spec-value { + font-family: 'JetBrains Mono', monospace; + font-size: 18px; + font-weight: 600; + color: #ffffff; + margin-bottom: 2px; +} + +.spec-label { + font-size: 9px; + font-weight: 500; + color: rgba(255, 255, 255, 0.6); + text-transform: uppercase; + letter-spacing: 1px; + white-space: nowrap; + padding-left: 0.5px; + margin-right: -2px; +} + + +.pulse-animation { + -webkit-animation: valuePulse 0.6s cubic-bezier(0.4, 0, 0.2, 1); + animation: valuePulse 0.6s cubic-bezier(0.4, 0, 0.2, 1); +} + +@-webkit-keyframes valuePulse { + 0% { + -webkit-transform: scale(1); + transform: scale(1); + } + + 50% { + -webkit-transform: scale(1.05); + transform: scale(1.05); + } + + 100% { + -webkit-transform: scale(1); + transform: scale(1); + } +} + +@keyframes valuePulse { + 0% { + -webkit-transform: scale(1); + transform: scale(1); + } + + 50% { + -webkit-transform: scale(1.05); + transform: scale(1.05); + } + + 100% { + -webkit-transform: scale(1); + transform: scale(1); + } +} + +.glow-animation { + -webkit-animation: cardGlow 0.8s ease-in-out; + animation: cardGlow 0.8s ease-in-out; +} + +@-webkit-keyframes cardGlow { + 0% { + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + } + + 50% { + box-shadow: + 0 12px 48px rgba(99, 102, 241, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.15); + } + + 100% { + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + } +} + +@keyframes cardGlow { + 0% { + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + } + + 50% { + box-shadow: + 0 12px 48px rgba(99, 102, 241, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.15); + } + + 100% { + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + } +} + +.hidden { + -webkit-transform: translateX(-100%) scale(0.95) !important; + transform: translateX(-100%) scale(0.95) !important; + opacity: 0 !important; +} \ No newline at end of file diff --git a/counters/Pups-Overlay by Puparia/index.html b/counters/Pups-Overlay by Puparia/index.html new file mode 100644 index 00000000..3f68f1c8 --- /dev/null +++ b/counters/Pups-Overlay by Puparia/index.html @@ -0,0 +1,133 @@ + + + + + + + + + + Pups-Overlay + + + +
+
+
+
+ +
+ +
+
+
+
0/0
+
PP
+
+ +
+
100.00
+
ACCURACY
+
+ +
+
0
+
COMBO
+
+ +
+
0
+
300
+
+ +
+
0
+
100
+
+ +
+
0
+
50
+
+ +
+
0
+
MISS
+
+
+ +
+
+
+
+
+
+
+
+
+
+ + + +
+
+ Select a beatmap + to begin your journey +
+
+
+
+
No Map Selected
+
+
+
Unknown Artist
+
+
+ Unknown + + 0 BPM +
+
+
+ +
+
+
+
0.00
+
SR
+
+
+
0.0
+
CS
+
+
+
0.0
+
AR
+
+
+
0.0
+
OD
+
+
+
0.0
+
HP
+
+
+
0
+
BPM
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/counters/Pups-Overlay by Puparia/index.js b/counters/Pups-Overlay by Puparia/index.js new file mode 100644 index 00000000..00a407cb --- /dev/null +++ b/counters/Pups-Overlay by Puparia/index.js @@ -0,0 +1,374 @@ +import WebSocketManager from './deps/socket.js'; + +const socket = new WebSocketManager(window.location.host); + +const glassPanel = document.getElementById('glassPanel'); +const ambientLight = document.getElementById('ambientLight'); +const container = document.getElementById('pups-container'); + +let cache = {}; +let animations = {}; +let settings = {}; + +socket.sendCommand('getSettings', encodeURI(window.COUNTER_PATH)); +socket.commands((data) => { + try { + const { message } = data; + if (message['overlayPosition'] != null) { + settings.overlayPosition = message['overlayPosition']; + const positionClass = settings.overlayPosition ? 'position-top' : 'position-bottom'; + container.classList.add(positionClass); + glassPanel.classList.add(positionClass); + } + } catch (e) { + container.classList.add('position-top'); + glassPanel.classList.add('position-top'); + } +}); + +function initializeAnimations() { + const baseOpts = { useEasing: true, useGrouping: false, separator: " ", decimal: "." }; + animations.pp = new CountUp('ppValue', 0, 0, 0, 0.8, { ...baseOpts, decimalPlaces: 0 }); + animations.acc = new CountUp('accValue', 100, 100, 0, 0.5, { ...baseOpts, decimalPlaces: 2 }); + animations.combo = new CountUp('comboValue', 0, 0, 0, 0.4, { ...baseOpts, decimalPlaces: 0 }); + animations.hit300 = new CountUp('hit300', 0, 0, 0, 0.3, { ...baseOpts, decimalPlaces: 0 }); + animations.hit100 = new CountUp('hit100', 0, 0, 0, 0.3, { ...baseOpts, decimalPlaces: 0 }); + animations.hit50 = new CountUp('hit50', 0, 0, 0, 0.3, { ...baseOpts, decimalPlaces: 0 }); + animations.hitMiss = new CountUp('hitMiss', 0, 0, 0, 0.3, { ...baseOpts, decimalPlaces: 0 }); +} + +function showOverlay() { + glassPanel.classList.add('active'); + ambientLight.style.opacity = '0.6'; +} + +function setGameplayMode() { + glassPanel.classList.add('gameplay-mode'); + glassPanel.classList.remove('menu-mode'); +} + +function setMenuMode() { + glassPanel.classList.add('menu-mode'); + glassPanel.classList.remove('gameplay-mode'); +} + +function addPulseEffect(element) { + element.classList.add('pulse-animation'); + setTimeout(() => element.classList.remove('pulse-animation'), 600); +} + +function addGlowEffect(element) { + element.classList.add('glow-animation'); + setTimeout(() => element.classList.remove('glow-animation'), 800); +} + + +function updateModsDisplay(modsStr) { + const modsContainer = document.getElementById('modsContainer'); + modsContainer.innerHTML = ''; + + const modsImgs = { + 'ez': './static/easy.png', + 'nf': './static/nofail.png', + 'ht': './static/halftime.png', + 'hr': './static/hardrock.png', + 'sd': './static/suddendeath.png', + 'pf': './static/perfect.png', + 'dt': './static/doubletime.png', + 'nc': './static/nightcore.png', + 'hd': './static/hidden.png', + 'fl': './static/flashlight.png', + 'rx': './static/relax.png', + 'ap': './static/autopilot.png', + 'so': './static/spunout.png', + 'at': './static/autoplay.png', + 'cn': './static/cinema.png', + 'v2': './static/v2.png', + }; + + if (!modsStr || modsStr === "" || modsStr === "NM") return; + let modsApplied = modsStr.toLowerCase(); + if (modsApplied.includes('nc')) modsApplied = modsApplied.replace('dt', ''); + if (modsApplied.includes('pf')) modsApplied = modsApplied.replace('sd', ''); + + const modsArr = modsApplied.match(/.{1,2}/g); + if (!modsArr) return; + for (let i = 0; i < modsArr.length; i++) { + const modIcon = document.createElement('div'); + modIcon.className = 'mod-icon'; + modIcon.style.backgroundImage = `url('${modsImgs[modsArr[i]]}')`; + modIcon.title = modsArr[i].toUpperCase(); + modsContainer.appendChild(modIcon); + } +} + +socket.api_v2((data) => { + if (!animations.pp) { + initializeAnimations(); + } + + const { play, beatmap, state, resultsScreen } = data; + + if (!cache['hasShownOnce']) { + showOverlay(); + cache['hasShownOnce'] = true; + } + + const stateName = state?.name?.toLowerCase() || 'menu'; + const isInGame = stateName === 'play' || stateName === 'resultscreen'; + + if (cache['isInGame'] !== isInGame) { + cache['isInGame'] = isInGame; + isInGame ? setGameplayMode() : setMenuMode(); + } + + const artist = beatmap?.artist || beatmap?.artistUnicode || 'Unknown Artist'; + const title = beatmap?.title || beatmap?.titleUnicode || 'No Map Selected'; + const difficulty = beatmap?.version || 'Unknown'; + const bpm = beatmap?.stats?.bpm?.common || 0; + if (cache['artist'] !== artist || cache['title'] !== title) { + cache['artist'] = artist; + cache['title'] = title; + cache['difficulty'] = difficulty; + cache['bpm'] = bpm; + const mapTitleEl = document.getElementById('mapTitle'); + const mapArtistEl = document.getElementById('mapArtist'); + + mapTitleEl.textContent = title; + mapArtistEl.textContent = artist; + document.getElementById('mapDifficulty').textContent = difficulty; + document.getElementById('mapBpm').textContent = `${Math.round(bpm)} BPM`; + + setTimeout(() => { + const titleWrapper = mapTitleEl.parentElement; + const artistWrapper = mapArtistEl.parentElement; + + if (mapTitleEl.scrollWidth > titleWrapper.clientWidth) { + mapTitleEl.classList.add('scrolling'); + } else { + mapTitleEl.classList.remove('scrolling'); + } + + if (mapArtistEl.scrollWidth > artistWrapper.clientWidth) { + mapArtistEl.classList.add('scrolling'); + } else { + mapArtistEl.classList.remove('scrolling'); + } + }, 100); + } + + const currentTime = beatmap?.time?.live || 0; + const totalTime = beatmap?.time?.lastObject || 1; + const progress = Math.min(100, Math.max(0, (currentTime / totalTime) * 100)); + + const progressFillTop = document.getElementById('progressFillTop'); + const progressFillBottom = document.getElementById('progressFillBottom'); + + progressFillTop.style.width = `${progress}%`; + progressFillBottom.style.width = `${progress}%`; + + if (isInGame && Math.random() > 0.6 && progress > 0) { + [progressFillTop, progressFillBottom].forEach(fill => { + const particle = document.createElement('div'); + particle.className = 'progress-particle'; + particle.style.left = `calc(100% - ${Math.random() * 10}px)`; + particle.style.animationDelay = `${Math.random() * 0.3}s`; + fill.appendChild(particle); + + setTimeout(() => particle.remove(), 2000); + }); + } + + if (!isInGame) { + return; + } + + const isResultScreen = stateName === 'resultscreen'; + const source = isResultScreen ? resultsScreen : play; + + const pp = source?.pp || { current: 0, fc: 0 }; + const hits = source?.hits || {}; + const accuracy = source?.accuracy || 100; + const combo = source?.combo || { current: 0 }; + + if (cache['pp'] !== pp.current || cache['ppfc'] !== pp.fc) { + cache['pp'] = pp.current; + cache['ppfc'] = pp.fc; + + if (Number.isFinite(pp.current) && Number.isFinite(pp.fc)) { + const ppDisplay = `${Math.round(pp.current)}/${Math.round(pp.fc)}`; + document.getElementById('ppValue').textContent = ppDisplay; + addPulseEffect(document.getElementById('ppValue')); + } + } + + if (cache['acc'] !== accuracy) { + cache['acc'] = accuracy; + + if (Number.isFinite(accuracy)) { + animations.acc.update(accuracy); + addPulseEffect(document.getElementById('accValue')); + } + } + + if (cache['combo'] !== combo.current) { + cache['combo'] = combo.current; + + if (Number.isFinite(combo.current)) { + animations.combo.update(combo.current); + addPulseEffect(document.getElementById('comboValue')); + } + } + + if (cache['hit300'] !== hits['300']) { + cache['hit300'] = hits['300']; + animations.hit300.update(hits['300']); + addPulseEffect(document.getElementById('hit300')); + } + + if (cache['hit100'] !== hits['100']) { + cache['hit100'] = hits['100']; + animations.hit100.update(hits['100']); + addPulseEffect(document.getElementById('hit100')); + } + + if (cache['hit50'] !== hits['50']) { + cache['hit50'] = hits['50']; + animations.hit50.update(hits['50']); + addPulseEffect(document.getElementById('hit50')); + } + + if (cache['hitMiss'] !== hits['0']) { + cache['hitMiss'] = hits['0']; + animations.hitMiss.update(hits['0']); + addPulseEffect(document.getElementById('hitMiss')); + } + + const currentMods = source?.mods; + const modsStr = currentMods?.name || ""; + if (cache['mods'] !== modsStr) { + cache['mods'] = modsStr; + updateModsDisplay(modsStr); + } + + const stats = { + cs: beatmap?.stats?.cs?.converted || 0, + ar: beatmap?.stats?.ar?.converted || 0, + od: beatmap?.stats?.od?.converted || 0, + hp: beatmap?.stats?.hp?.converted || 0, + bpm: beatmap?.stats?.bpm?.realtime || beatmap?.stats?.bpm?.common || 0, + stars: beatmap?.stats?.stars?.total || 0 + }; + + const statsKey = JSON.stringify(stats); + if (cache['statsKey'] !== statsKey) { + cache['statsKey'] = statsKey; + + function formatStat(value, decimals = 1) { + const rounded = parseFloat(value.toFixed(decimals)); + if (rounded % 1 === 0) return rounded.toFixed(0); + return rounded.toFixed(decimals); + } + + document.getElementById('starRating').textContent = formatStat(stats.stars, 2); + document.getElementById('csValue').textContent = formatStat(stats.cs); + document.getElementById('arValue').textContent = formatStat(stats.ar); + document.getElementById('odValue').textContent = formatStat(stats.od); + document.getElementById('hpValue').textContent = formatStat(stats.hp); + document.getElementById('bpmValue').textContent = Math.round(stats.bpm); + } +}, [ + { + field: 'beatmap', + keys: [ + { + field: 'time', + keys: ['live', 'lastObject'] + }, + { + field: 'stats', + keys: [ + { + field: 'bpm', + keys: ['common', 'realtime'] + }, + { + field: 'cs', + keys: ['converted'] + }, + { + field: 'ar', + keys: ['converted'] + }, + { + field: 'od', + keys: ['converted'] + }, + { + field: 'hp', + keys: ['converted'] + }, + { + field: 'stars', + keys: ['total'] + } + ] + }, + 'artist', + 'artistUnicode', + 'title', + 'titleUnicode', + 'version' + ] + }, + { + field: 'play', + keys: [ + { + field: 'hits', + keys: ['300', '100', '50', '0'] + }, + { + field: 'pp', + keys: ['current', 'fc'] + }, + { + field: 'combo', + keys: ['current'] + }, + { + field: 'mods', + keys: ['name', 'array'] + }, + 'accuracy' + ] + }, + { + field: 'resultsScreen', + keys: [ + { + field: 'hits', + keys: ['300', '100', '50', '0'] + }, + { + field: 'pp', + keys: ['current', 'fc'] + }, + { + field: 'combo', + keys: ['current'] + }, + { + field: 'mods', + keys: ['name', 'array'] + }, + 'accuracy' + ] + }, + { + field: 'state', + keys: ['name'] + } +]); + +initializeAnimations(); \ No newline at end of file diff --git a/counters/Pups-Overlay by Puparia/metadata.txt b/counters/Pups-Overlay by Puparia/metadata.txt new file mode 100644 index 00000000..9ae5ec4b --- /dev/null +++ b/counters/Pups-Overlay by Puparia/metadata.txt @@ -0,0 +1,7 @@ +Usecase: ingame,obs-overlays +Name: Pups-Overlay +Version: 1.0 +Author: Puparia +CompatibleWith: tosu,streamCompanion +Resolution: 1920x105 +Description: Hyper-modern overlay featuring elegant transparency, subtlety, and high-end aesthetics with integrated difficulty spectrogram diff --git a/counters/Pups-Overlay by Puparia/settings.json b/counters/Pups-Overlay by Puparia/settings.json new file mode 100644 index 00000000..2aec9f3a --- /dev/null +++ b/counters/Pups-Overlay by Puparia/settings.json @@ -0,0 +1 @@ +[{"uniqueID":"overlayPosition","type":"checkbox","title":"Show overlay at bottom","description":"When enabled, overlay is positioned at the top with bottom progress bar visible. When disabled, overlay is at the bottom with top progress bar visible.","options":"","value":false}] \ No newline at end of file diff --git a/counters/Pups-Overlay by Puparia/static/autopilot.png b/counters/Pups-Overlay by Puparia/static/autopilot.png new file mode 100644 index 00000000..5785bd6f Binary files /dev/null and b/counters/Pups-Overlay by Puparia/static/autopilot.png differ diff --git a/counters/Pups-Overlay by Puparia/static/autoplay.png b/counters/Pups-Overlay by Puparia/static/autoplay.png new file mode 100644 index 00000000..99d653e3 Binary files /dev/null and b/counters/Pups-Overlay by Puparia/static/autoplay.png differ diff --git a/counters/Pups-Overlay by Puparia/static/cinema.png b/counters/Pups-Overlay by Puparia/static/cinema.png new file mode 100644 index 00000000..4d7240bb Binary files /dev/null and b/counters/Pups-Overlay by Puparia/static/cinema.png differ diff --git a/counters/Pups-Overlay by Puparia/static/doubletime.png b/counters/Pups-Overlay by Puparia/static/doubletime.png new file mode 100644 index 00000000..6d25e323 Binary files /dev/null and b/counters/Pups-Overlay by Puparia/static/doubletime.png differ diff --git a/counters/Pups-Overlay by Puparia/static/easy.png b/counters/Pups-Overlay by Puparia/static/easy.png new file mode 100644 index 00000000..ab3bc0d7 Binary files /dev/null and b/counters/Pups-Overlay by Puparia/static/easy.png differ diff --git a/counters/Pups-Overlay by Puparia/static/flashlight.png b/counters/Pups-Overlay by Puparia/static/flashlight.png new file mode 100644 index 00000000..f673ad26 Binary files /dev/null and b/counters/Pups-Overlay by Puparia/static/flashlight.png differ diff --git a/counters/Pups-Overlay by Puparia/static/halftime.png b/counters/Pups-Overlay by Puparia/static/halftime.png new file mode 100644 index 00000000..0a91e0f7 Binary files /dev/null and b/counters/Pups-Overlay by Puparia/static/halftime.png differ diff --git a/counters/Pups-Overlay by Puparia/static/hardrock.png b/counters/Pups-Overlay by Puparia/static/hardrock.png new file mode 100644 index 00000000..fe03d3f8 Binary files /dev/null and b/counters/Pups-Overlay by Puparia/static/hardrock.png differ diff --git a/counters/Pups-Overlay by Puparia/static/hidden.png b/counters/Pups-Overlay by Puparia/static/hidden.png new file mode 100644 index 00000000..bf4a9978 Binary files /dev/null and b/counters/Pups-Overlay by Puparia/static/hidden.png differ diff --git a/counters/Pups-Overlay by Puparia/static/mask.svg b/counters/Pups-Overlay by Puparia/static/mask.svg new file mode 100644 index 00000000..3fd0d7ec --- /dev/null +++ b/counters/Pups-Overlay by Puparia/static/mask.svg @@ -0,0 +1,3 @@ + + + diff --git a/counters/Pups-Overlay by Puparia/static/nightcore.png b/counters/Pups-Overlay by Puparia/static/nightcore.png new file mode 100644 index 00000000..012c8a13 Binary files /dev/null and b/counters/Pups-Overlay by Puparia/static/nightcore.png differ diff --git a/counters/Pups-Overlay by Puparia/static/nofail.png b/counters/Pups-Overlay by Puparia/static/nofail.png new file mode 100644 index 00000000..8180c6d6 Binary files /dev/null and b/counters/Pups-Overlay by Puparia/static/nofail.png differ diff --git a/counters/Pups-Overlay by Puparia/static/perfect.png b/counters/Pups-Overlay by Puparia/static/perfect.png new file mode 100644 index 00000000..47fa9733 Binary files /dev/null and b/counters/Pups-Overlay by Puparia/static/perfect.png differ diff --git a/counters/Pups-Overlay by Puparia/static/relax.png b/counters/Pups-Overlay by Puparia/static/relax.png new file mode 100644 index 00000000..fe987f43 Binary files /dev/null and b/counters/Pups-Overlay by Puparia/static/relax.png differ diff --git a/counters/Pups-Overlay by Puparia/static/spunout.png b/counters/Pups-Overlay by Puparia/static/spunout.png new file mode 100644 index 00000000..90623593 Binary files /dev/null and b/counters/Pups-Overlay by Puparia/static/spunout.png differ diff --git a/counters/Pups-Overlay by Puparia/static/suddendeath.png b/counters/Pups-Overlay by Puparia/static/suddendeath.png new file mode 100644 index 00000000..f48babee Binary files /dev/null and b/counters/Pups-Overlay by Puparia/static/suddendeath.png differ diff --git a/counters/Pups-Overlay by Puparia/static/target.png b/counters/Pups-Overlay by Puparia/static/target.png new file mode 100644 index 00000000..f2d782ab Binary files /dev/null and b/counters/Pups-Overlay by Puparia/static/target.png differ diff --git a/counters/Pups-Overlay by Puparia/static/v2.png b/counters/Pups-Overlay by Puparia/static/v2.png new file mode 100644 index 00000000..42ab9e03 Binary files /dev/null and b/counters/Pups-Overlay by Puparia/static/v2.png differ