diff --git a/counters/Hit Count by Albert/catch.png b/counters/Hit Count by Albert/catch.png
new file mode 100644
index 0000000..c7ac9fb
Binary files /dev/null and b/counters/Hit Count by Albert/catch.png differ
diff --git a/counters/Hit Count by Albert/index.html b/counters/Hit Count by Albert/index.html
new file mode 100644
index 0000000..db2685c
--- /dev/null
+++ b/counters/Hit Count by Albert/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/counters/Hit Count by Albert/main.css b/counters/Hit Count by Albert/main.css
new file mode 100644
index 0000000..a620b11
--- /dev/null
+++ b/counters/Hit Count by Albert/main.css
@@ -0,0 +1,109 @@
+body, html {
+ padding: 0;
+ margin: 0;
+ overflow: hidden;
+ font-family: var(--main-font, Arial, sans-serif);
+}
+
+#hitcount_box {
+ position: absolute;
+ top: 0;
+ left: 0;
+ transition: opacity 500ms ease-in-out;
+ opacity: 0;
+}
+
+#countBox {
+ display: grid;
+ grid-template-columns: var(--label-width, 130px) 10px var(--val-width, 90px);
+ grid-auto-flow: dense;
+ line-height: var(--line-height, 1.25);
+ padding: 4px;
+}
+
+#countBox.swapped {
+ grid-template-columns: var(--val-width, 90px) 10px var(--label-width, 140px);
+}
+
+#countBox > div {
+ display: contents;
+}
+
+#countBox > div.hidden {
+ display: none !important;
+}
+
+.spacer {
+ display: block !important;
+ grid-column: 1 / -1;
+ height: 0.75em;
+}
+
+.spacer.hidden {
+ display: none !important;
+}
+
+.row-label, .row-val {
+ font-size: var(--scaled-font-size, 20pt);
+ white-space: nowrap;
+ color: inherit;
+ text-shadow: 1.5px 1.5px 0 #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
+}
+
+.row-label {
+ grid-column: 1;
+ text-align: left;
+ justify-self: start;
+}
+
+.row-val {
+ grid-column: 3;
+ text-align: right;
+ justify-self: end;
+ font-variant-numeric: tabular-nums;
+}
+
+.hidden {
+ display: none !important;
+}
+
+/* Swapped State Modifiers */
+#countBox.swapped .row-label {
+ grid-column: 3;
+ text-align: right;
+ justify-self: end;
+}
+
+#countBox.swapped .row-val {
+ grid-column: 1;
+ text-align: left;
+ justify-self: start;
+}
+
+/* Specific Variable Map for Labels and Numbers */
+#pp .row-label { color: var(--color-pp-label); }
+#pp .row-val { color: var(--color-pp-val); }
+#ur .row-label { color: var(--color-ur-label); }
+#ur .row-val { color: var(--color-ur-val); }
+#ratio .row-label { color: var(--color-ratio-label); }
+#ratio .row-val { color: var(--color-ratio-val); }
+#maxCombo .row-label { color: var(--color-combo-label); }
+#maxCombo .row-val { color: var(--color-combo-val); }
+
+#h300g .row-label { color: var(--color-300g-label); }
+#h300g .row-val { color: var(--color-300g-val); }
+#h300 .row-label { color: var(--color-300-label); }
+#h300 .row-val { color: var(--color-300-val); }
+#h200 .row-label { color: var(--color-200-label); }
+#h200 .row-val { color: var(--color-200-val); }
+#h100 .row-label { color: var(--color-100-label); }
+#h100 .row-val { color: var(--color-100-val); }
+#h50 .row-label { color: var(--color-50-label); }
+#h50 .row-val { color: var(--color-50-val); }
+#miss .row-label { color: var(--color-miss-label); }
+#miss .row-val { color: var(--color-miss-val); }
+
+#earlyCount .row-label { color: var(--color-early-label); }
+#earlyCount .row-val { color: var(--color-early-val); }
+#lateCount .row-label { color: var(--color-late-label); }
+#lateCount .row-val { color: var(--color-late-val); }
\ No newline at end of file
diff --git a/counters/Hit Count by Albert/main.js b/counters/Hit Count by Albert/main.js
new file mode 100644
index 0000000..6dcc058
--- /dev/null
+++ b/counters/Hit Count by Albert/main.js
@@ -0,0 +1,472 @@
+class WebSocketManager {
+ constructor(host) {
+ this.host = host;
+ this.sockets = {};
+ this.createConnection = this.createConnection.bind(this);
+ }
+
+ createConnection(url, callback, filters) {
+ let interval;
+ const counterPath = window.COUNTER_PATH || "";
+ const query = url.includes("?") ? `&l=${encodeURI(counterPath)}` : `?l=${encodeURI(counterPath)}`;
+ const fullUrl = `ws://${this.host}${url}${query}`;
+ this.sockets[url] = new WebSocket(fullUrl);
+
+ this.sockets[url].onopen = () => {
+ if (interval) clearInterval(interval);
+ if (filters) this.sockets[url].send(`applyFilters:${JSON.stringify(filters)}`);
+ };
+
+ this.sockets[url].onclose = () => {
+ delete this.sockets[url];
+ interval = setTimeout(() => this.createConnection(url, callback, filters), 1000);
+ };
+
+ this.sockets[url].onmessage = ({ data }) => {
+ if (!data) return;
+ try {
+ const parsed = JSON.parse(data);
+ if (parsed && typeof parsed === "object" && !("error" in parsed)) callback(parsed);
+ } catch (e) {}
+ };
+ }
+
+ api_v2(callback, filters) { this.createConnection("/websocket/v2", callback, filters); }
+ api_v2_precise(callback, filters) { this.createConnection("/websocket/v2/precise", callback, filters); }
+ commands(callback) { this.createConnection("/websocket/commands", callback); }
+
+ sendCommand(name, command, retry = 1) {
+ const socket = this.sockets["/websocket/commands"];
+ if (socket && socket.readyState === WebSocket.OPEN) {
+ socket.send(`${name}:${command}`);
+ } else if (retry <= 50) {
+ setTimeout(() => this.sendCommand(name, command, retry + 1), 100);
+ }
+ }
+}
+
+const string = {
+ global: { pp: "PP", ur: "UR", ratio: "Ratio", combo: "Max Combo", early: "Early", late: "Late", miss: "Miss" },
+ modes: {
+ mania: { h300g: "MAX", h300: "Perfect", h200: "Great", h100: "Good", h50: "Bad" },
+ catch: { h300: "Fruit", h100: "Drop", h50: "Droplet" },
+ fruits: { h300: "Fruit", h100: "Drop", h50: "Droplet" },
+ taiko: { h300: "Great", h100: "Ok" },
+ osu: { h300: "300", h100: "100", h50: "50" }
+ }
+};
+
+const getWindows = (mode, od, modsString = "") => {
+ let mOd = od;
+ if (mode === "mania") {
+ if (modsString.includes("EZ")) mOd = od * 0.5;
+ const hrMult = modsString.includes("HR") ? 5/7 : 1;
+ return [16, ((64 - 3 * mOd) * hrMult), ((97 - 3 * mOd) * hrMult), ((127 - 3 * mOd) * hrMult), ((151 - 3 * mOd) * hrMult)];
+ }
+
+ if (modsString.includes("EZ")) mOd = od / 2;
+ else if (modsString.includes("HR")) mOd = Math.min(od * 1.4, 10);
+
+ if (mode === "taiko") return [(50 - 3 * mOd), (mOd >= 5 ? 119.5 - 8 * mOd : 110 - 6 * mOd), (mOd >= 5 ? 135 - 8 * mOd : 120 - 5 * mOd)];
+ return [(80 - 6 * mOd), (140 - 8 * mOd), (200 - 10 * mOd)];
+};
+
+function updateRow(element, label, value) {
+ if (element) element.innerHTML = `${label} ${value} `;
+}
+
+function distributeDelta(newTotal = 0, display, preciseTally, isMiss = false) {
+ const delta = newTotal - display.t;
+ if (delta <= 0) return;
+
+ if (isMiss) {
+ const availableE = Math.max(0, preciseTally.e - display.e);
+ const addE = Math.min(delta, availableE);
+ display.e += addE;
+ display.l += (delta - addE);
+ } else {
+ const preciseTotal = preciseTally.e + preciseTally.l;
+ if (preciseTotal === 0) {
+ const half = Math.floor(delta / 2);
+ display.e += half;
+ display.l += (delta - half);
+ } else {
+ const expectedE = Math.round(newTotal * (preciseTally.e / preciseTotal));
+ const addE = Math.max(0, Math.min(delta, expectedE - display.e));
+ display.e += addE;
+ display.l += (delta - addE);
+ }
+ }
+ display.t = newTotal;
+}
+
+function getRatioText(mode, hits) {
+ const h300g = hits.geki || 0, h300 = hits[300] || 0, h200 = hits.katu || 0, h100 = hits[100] || 0, h50 = hits[50] || 0, h0 = hits[0] || 0;
+
+ if (mode === "mania") {
+ const total = h300g + h300 + h200 + h100 + h50 + h0;
+ if (total === 0) return "0:1";
+ return (total - h300g) === 0 ? "∞:1" : `${(h300g / (total - h300g)).toFixed(1)}:1`;
+ } else {
+ const total = h300 + h100 + h50 + h0;
+ if (total === 0) return "0:1";
+ const nonPerfect = h100 + h50 + h0;
+ return nonPerfect === 0 ? "∞:1" : `${(h300 / nonPerfect).toFixed(1)}:1`;
+ }
+}
+
+function syncDeltaTallies(mode, hits) {
+ if (mode === "mania") {
+ distributeDelta(hits[300] || 0, displayTally.mania[0], hitTally.mania[0]);
+ distributeDelta(hits.katu || 0, displayTally.mania[1], hitTally.mania[1]);
+ distributeDelta(hits[100] || 0, displayTally.mania[2], hitTally.mania[2]);
+ distributeDelta(hits[50] || 0, displayTally.mania[3], hitTally.mania[3]);
+ distributeDelta(hits[0] || 0, displayTally.mania[4], hitTally.mania[4], true);
+ } else if (mode === "taiko") {
+ distributeDelta(hits[100] || 0, displayTally.taiko[0], hitTally.taiko[0]);
+ distributeDelta(hits[0] || 0, displayTally.taiko[1], hitTally.taiko[1], true);
+ } else {
+ distributeDelta(hits[100] || 0, displayTally.std[0], hitTally.std[0]);
+ distributeDelta(hits[50] || 0, displayTally.std[1], hitTally.std[1]);
+ distributeDelta(hits[0] || 0, displayTally.std[2], hitTally.std[2], true);
+ }
+}
+
+function applyModeColors(mode, settings, fallback) {
+ const root = document.documentElement;
+ const applyHitCol = (prefix, labelVal, numVal) => {
+ root.style.setProperty(`--color-${prefix}-label`, settings.useCustomHitCountLabelColors ? (labelVal || fallback) : fallback);
+ root.style.setProperty(`--color-${prefix}-val`, settings.useCustomHitCountNumberColors ? (numVal || fallback) : fallback);
+ };
+
+ applyHitCol('miss', settings.colorMissLabel, settings.colorMissVal);
+
+ if (mode === "osu") {
+ applyHitCol('300g', fallback, fallback);
+ applyHitCol('300', settings.colorOsu300Label, settings.colorOsu300Val);
+ applyHitCol('200', fallback, fallback);
+ applyHitCol('100', settings.colorOsu100Label, settings.colorOsu100Val);
+ applyHitCol('50', settings.colorOsu50Label, settings.colorOsu50Val);
+ } else if (mode === "taiko") {
+ applyHitCol('300g', fallback, fallback);
+ applyHitCol('300', settings.colorTaiko300Label, settings.colorTaiko300Val);
+ applyHitCol('200', fallback, fallback);
+ applyHitCol('100', settings.colorTaiko100Label, settings.colorTaiko100Val);
+ applyHitCol('50', fallback, fallback);
+ } else if (mode === "catch" || mode === "fruits") {
+ applyHitCol('300g', fallback, fallback);
+ applyHitCol('300', settings.colorCatch300Label, settings.colorCatch300Val);
+ applyHitCol('200', fallback, fallback);
+ applyHitCol('100', settings.colorCatch100Label, settings.colorCatch100Val);
+ applyHitCol('50', settings.colorCatch50Label, settings.colorCatch50Val);
+ } else {
+ applyHitCol('300g', settings.colorMania300gLabel, settings.colorMania300gVal);
+ applyHitCol('300', settings.colorMania300Label, settings.colorMania300Val);
+ applyHitCol('200', settings.colorMania200Label, settings.colorMania200Val);
+ applyHitCol('100', settings.colorMania100Label, settings.colorMania100Val);
+ applyHitCol('50', settings.colorMania50Label, settings.colorMania50Val);
+ }
+}
+
+const toggleClass = (el, condition, className = "hidden") => {
+ if (el) condition ? el.classList.add(className) : el.classList.remove(className);
+};
+
+async function autoScaleFont() {
+ await document.fonts.ready;
+
+ let measurer = document.getElementById("font-measurer");
+ if (!measurer) {
+ measurer = document.createElement("span");
+ measurer.id = "font-measurer";
+ measurer.style.position = "absolute";
+ measurer.style.visibility = "hidden";
+ measurer.style.whiteSpace = "nowrap";
+ measurer.style.fontSize = "20pt";
+ document.body.appendChild(measurer);
+ }
+
+ measurer.style.fontFamily = document.documentElement.style.getPropertyValue('--main-font');
+
+ const targetLabelW = settings.labelColumnWidth || 140;
+ const targetValW = settings.valueColumnWidth || 90;
+
+ measurer.innerText = "Max Combo";
+ const actualLabelW = measurer.offsetWidth || targetLabelW;
+
+ measurer.innerText = "88888.8";
+ const actualValW = measurer.offsetWidth || targetValW;
+
+ const scaleLabel = targetLabelW / actualLabelW;
+ const scaleVal = targetValW / actualValW;
+
+ const minScale = Math.min(scaleLabel, scaleVal, 1);
+ let finalSize = 20 * minScale;
+
+ if (finalSize < 10) finalSize = 10;
+
+ document.documentElement.style.setProperty('--scaled-font-size', `${finalSize}pt`);
+}
+
+const wsManager = new WebSocketManager(window.location.host);
+
+const hitcountBox = document.getElementById("hitcount_box");
+const elPp = document.getElementById("pp"), elUr = document.getElementById("ur"), elRatio = document.getElementById("ratio"), elMaxCombo = document.getElementById("maxCombo");
+const elEarly = document.getElementById("earlyCount"), elLate = document.getElementById("lateCount");
+const el300g = document.getElementById("h300g"), el300 = document.getElementById("h300"), el200 = document.getElementById("h200");
+const el100 = document.getElementById("h100"), el50 = document.getElementById("h50"), elMiss = document.getElementById("miss");
+const brHits = document.getElementById("brHits"), brEarlyLate = document.getElementById("brEarlyLate");
+
+let cache = { state: "", mode: "osu", od: 0, mods: "", processedHits: 0, curTotalHits: 0, lastTime: 0 };
+let hitTally = {
+ mania: [ {e:0, l:0}, {e:0, l:0}, {e:0, l:0}, {e:0, l:0}, {e:0, l:0} ],
+ taiko: [ {e:0, l:0}, {e:0, l:0} ],
+ std: [ {e:0, l:0}, {e:0, l:0}, {e:0, l:0} ]
+};
+let displayTally = {
+ mania: [ {e:0, l:0, t:0}, {e:0, l:0, t:0}, {e:0, l:0, t:0}, {e:0, l:0, t:0}, {e:0, l:0, t:0} ],
+ taiko: [ {e:0, l:0, t:0}, {e:0, l:0, t:0} ],
+ std: [ {e:0, l:0, t:0}, {e:0, l:0, t:0}, {e:0, l:0, t:0} ]
+};
+
+let settings = {
+ labelColumnWidth: 130, valueColumnWidth: 90, lineHeight: 1.25,
+ fontName: "Arial", useCustomFont: false, customFontName: "font.ttf",
+ globalTextColor: "#ffffff", swapLabelValue: false,
+ hidePP: false, useCustomPPColors: false, colorPPLabel: "#ffffff", colorPPVal: "#ffffff",
+ hideUR: false, useCustomURColors: false, colorURLabel: "#ffffff", colorURVal: "#ffffff",
+ hideRatio: false, useCustomRatioColors: false, colorRatioLabel: "#ffffff", colorRatioVal: "#ffffff",
+ hideMaxCombo: false, useCustomComboColors: false, colorComboLabel: "#ffffff", colorComboVal: "#ffffff",
+ hideHitCounts: false, useCustomHitCountLabelColors: true, useCustomHitCountNumberColors: false,
+ colorOsu300Label: "#50b4ff", colorOsu300Val: "#50b4ff", colorOsu100Label: "#47e547", colorOsu100Val: "#47e547", colorOsu50Label: "#ffcc22", colorOsu50Val: "#ffcc22",
+ colorTaiko300Label: "#ffcc22", colorTaiko300Val: "#ffcc22", colorTaiko100Label: "#47e547", colorTaiko100Val: "#47e547",
+ colorCatch300Label: "#ffcc22", colorCatch300Val: "#ffcc22", colorCatch100Label: "#47e547", colorCatch100Val: "#47e547", colorCatch50Label: "#50b4ff", colorCatch50Val: "#50b4ff",
+ colorMania300gLabel: "#ffffff", colorMania300gVal: "#ffffff", colorMania300Label: "#ffcc22", colorMania300Val: "#ffcc22", colorMania200Label: "#47e547", colorMania200Val: "#47e547", colorMania100Label: "#50b4ff", colorMania100Val: "#50b4ff", colorMania50Label: "#888888", colorMania50Val: "#888888",
+ colorMissLabel: "#ff0000", colorMissVal: "#ff0000",
+ hideEarlyLate: false, useCustomEarlyLateColors: true, colorEarlyLabel: "#0000ff", colorEarlyVal: "#ffffff", colorLateLabel: "#ff0000", colorLateVal: "#ffffff"
+};
+
+wsManager.commands((data) => {
+ try {
+ if (data.command !== "getSettings") return;
+ Object.assign(settings, data.message);
+ applySettingsToUI();
+ } catch (e) {}
+});
+
+wsManager.sendCommand("getSettings", window.COUNTER_PATH ? encodeURI(window.COUNTER_PATH) : "");
+
+function applySettingsToUI() {
+ const root = document.documentElement;
+ const g = settings.globalTextColor || "#ffffff";
+ const mode = cache.mode || "osu";
+
+ root.style.setProperty('--label-width', `${settings.labelColumnWidth || 140}px`);
+ root.style.setProperty('--val-width', `${settings.valueColumnWidth || 90}px`);
+ root.style.setProperty('--line-height', settings.lineHeight || 1.25);
+
+ let fontStyle = document.getElementById("custom-font-style");
+ const systemFont = settings.fontName ? `"${settings.fontName}", sans-serif` : "Arial, sans-serif";
+
+ if (settings.useCustomFont && settings.customFontName) {
+ if (!fontStyle) {
+ fontStyle = document.createElement("style");
+ fontStyle.id = "custom-font-style";
+ document.head.appendChild(fontStyle);
+ }
+ fontStyle.innerHTML = `
+ @font-face {
+ font-family: 'CustomOverlayFont';
+ src: url('./${settings.customFontName}');
+ }
+ `;
+ root.style.setProperty('--main-font', `'CustomOverlayFont', ${systemFont}`);
+ } else {
+ if (fontStyle) fontStyle.innerHTML = "";
+ root.style.setProperty('--main-font', systemFont);
+ }
+
+ toggleClass(document.getElementById("countBox"), settings.swapLabelValue, "swapped");
+
+ toggleClass(elPp, settings.hidePP);
+ toggleClass(elUr, settings.hideUR);
+ toggleClass(elRatio, settings.hideRatio);
+ toggleClass(elMaxCombo, settings.hideMaxCombo);
+
+ if (settings.hideHitCounts) {
+ [el300g, el300, el200, el100, el50, elMiss, brHits].forEach(el => toggleClass(el, true));
+ } else {
+ toggleClass(elMiss, false); toggleClass(brHits, false);
+ }
+
+ const hideEL = settings.hideEarlyLate;
+ [elEarly, elLate, brEarlyLate].forEach(el => toggleClass(el, hideEL));
+
+ const applyStatColor = (labelProp, valProp, condition, lCol, vCol) => {
+ root.style.setProperty(labelProp, condition ? (lCol || g) : g);
+ root.style.setProperty(valProp, condition ? (vCol || g) : g);
+ };
+
+ applyStatColor('--color-pp-label', '--color-pp-val', settings.useCustomPPColors, settings.colorPPLabel, settings.colorPPVal);
+ applyStatColor('--color-ur-label', '--color-ur-val', settings.useCustomURColors, settings.colorURLabel, settings.colorURVal);
+ applyStatColor('--color-ratio-label', '--color-ratio-val', settings.useCustomRatioColors, settings.colorRatioLabel, settings.colorRatioVal);
+ applyStatColor('--color-combo-label', '--color-combo-val', settings.useCustomComboColors, settings.colorComboLabel, settings.colorComboVal);
+ applyStatColor('--color-early-label', '--color-early-val', settings.useCustomEarlyLateColors, settings.colorEarlyLabel, settings.colorEarlyVal);
+ applyStatColor('--color-late-label', '--color-late-val', settings.useCustomEarlyLateColors, settings.colorLateLabel, settings.colorLateVal);
+
+ applyModeColors(mode, settings, g);
+
+ autoScaleFont();
+}
+
+function resetCounters() {
+ if (!settings.hideUR) updateRow(elUr, string.global.ur, "0.00");
+ if (!settings.hideRatio) updateRow(elRatio, string.global.ratio, "0:1");
+ if (!settings.hideEarlyLate) { updateRow(elEarly, string.global.early, "0"); updateRow(elLate, string.global.late, "0"); }
+
+ hitTally.mania.forEach(t => { t.e = 0; t.l = 0; }); hitTally.taiko.forEach(t => { t.e = 0; t.l = 0; }); hitTally.std.forEach(t => { t.e = 0; t.l = 0; });
+ displayTally.mania.forEach(t => { t.e = 0; t.l = 0; t.t = 0; }); displayTally.taiko.forEach(t => { t.e = 0; t.l = 0; t.t = 0; }); displayTally.std.forEach(t => { t.e = 0; t.l = 0; t.t = 0; });
+ cache.processedHits = 0; cache.curTotalHits = 0;
+}
+
+wsManager.api_v2((data) => {
+ if (!data.state?.name) return;
+ const state = data.state.name;
+
+ if (cache.state !== state) {
+ if (hitcountBox) hitcountBox.style.opacity = (state === "play") ? 1 : 0;
+ if (state !== "play") resetCounters();
+ }
+
+ if (!settings.hidePP) {
+ const ppValue = (state === 'play' || state === 'resultScreen') ? (data.play?.pp?.current || 0) : (data.performance?.pp?.current || data.play?.pp?.current || 0);
+ updateRow(elPp, string.global.pp, Math.round(ppValue) + 'pp');
+ }
+
+ if (state === "play") {
+ const mode = data.play?.mode?.name ?? cache.mode;
+
+ if (!settings.hideMaxCombo) updateRow(elMaxCombo, string.global.combo, data.play?.combo?.max || 0);
+
+ let od = cache.od;
+ if (data.beatmap?.stats?.od !== undefined) {
+ od = (typeof data.beatmap.stats.od === "object" && data.beatmap.stats.od.original !== undefined) ? data.beatmap.stats.od.original : data.beatmap.stats.od;
+ }
+
+ let mods = cache.mods;
+ if (data.play?.mods) {
+ mods = (typeof data.play.mods === "string") ? data.play.mods : (typeof data.play.mods.name === "string" ? data.play.mods.name : data.play.mods.join(""));
+ }
+
+ if (cache.mode !== mode || cache.od !== od || cache.mods !== mods) {
+ cache.mode = mode; cache.od = od; cache.mods = mods;
+ cache.windows = getWindows(cache.mode, cache.od, cache.mods);
+ resetCounters();
+ applySettingsToUI();
+ }
+
+ const hits = data.play?.hits || {};
+ const modeLabels = string.modes[mode] || string.modes.osu;
+
+ const updateHitRow = (el, key, val) => {
+ if (settings.hideHitCounts) { toggleClass(el, true); return; }
+ if (modeLabels[key]) { toggleClass(el, false); updateRow(el, modeLabels[key], val); }
+ else { toggleClass(el, true); }
+ };
+
+ updateHitRow(el300g, 'h300g', hits.geki || 0);
+ updateHitRow(el300, 'h300', hits[300] || 0);
+ updateHitRow(el200, 'h200', hits.katu || 0);
+ updateHitRow(el100, 'h100', hits[100] || 0);
+ updateHitRow(el50, 'h50', hits[50] || 0);
+
+ if (!settings.hideHitCounts) updateRow(elMiss, string.global.miss, hits[0] || 0);
+ if (!settings.hideRatio) updateRow(elRatio, string.global.ratio, getRatioText(mode, hits));
+
+ const isCatch = (mode === "catch" || mode === "fruits");
+ const hideEL = isCatch || settings.hideEarlyLate;
+ [elEarly, elLate, brEarlyLate].forEach(el => toggleClass(el, hideEL));
+
+ const totalHits = (hits.geki || 0) + (hits[300] || 0) + (hits.katu || 0) + (hits[100] || 0) + (hits[50] || 0) + (hits[0] || 0);
+ if (totalHits === 0 && cache.curTotalHits > 0) resetCounters();
+
+ if (totalHits >= cache.curTotalHits) {
+ cache.curTotalHits = totalHits;
+ syncDeltaTallies(mode, hits);
+
+ if (!hideEL) {
+ let totalEarly = 0, totalLate = 0;
+ const currentTallyArr = mode === "mania" ? displayTally.mania : (mode === "taiko" ? displayTally.taiko : displayTally.std);
+ currentTallyArr.forEach(t => { totalEarly += t.e; totalLate += t.l; });
+ updateRow(elEarly, string.global.early, totalEarly);
+ updateRow(elLate, string.global.late, totalLate);
+ }
+ }
+ }
+ cache.state = state;
+}, ["state", { field: "play", keys: ["mode", "mods", "hits", "combo", "pp"] }, { field: "beatmap", keys: ["stats"] }, { field: "performance", keys: ["pp"] }]);
+
+wsManager.api_v2_precise((data) => {
+ if (cache.state !== "play") return;
+ const hitErrors = data.hitErrors || [];
+
+ if (data.currentTime < (cache.lastTime || 0) - 50) {
+ resetCounters(); cache.lastTime = data.currentTime; cache.processedHits = hitErrors.length; return;
+ }
+ cache.lastTime = data.currentTime;
+
+ if (hitErrors.length < cache.processedHits) {
+ if (hitErrors.length === 0 || (cache.processedHits - hitErrors.length > 5)) {
+ resetCounters(); cache.processedHits = hitErrors.length;
+ }
+ return;
+ }
+
+ if (hitErrors.length > cache.processedHits) {
+ const newHits = hitErrors.slice(cache.processedHits);
+ cache.processedHits = hitErrors.length;
+ const mode = cache.mode, windows = cache.windows;
+
+ newHits.forEach(ms => {
+ const msAbs = Math.abs(ms);
+ const isEarly = ms < 0;
+
+ if (mode === "mania") {
+ const t = hitTally.mania;
+ if (msAbs <= windows[0]) {}
+ else if (msAbs <= windows[1]) { isEarly ? t[0].e++ : t[0].l++; }
+ else if (msAbs <= windows[2]) { isEarly ? t[1].e++ : t[1].l++; }
+ else if (msAbs <= windows[3]) { isEarly ? t[2].e++ : t[2].l++; }
+ else if (msAbs <= windows[4]) { isEarly ? t[3].e++ : t[3].l++; }
+ else { isEarly ? t[4].e++ : t[4].l++; }
+ } else if (mode === "taiko") {
+ const t = hitTally.taiko;
+ if (msAbs <= windows[0]) {}
+ else if (msAbs <= windows[1]) { isEarly ? t[0].e++ : t[0].l++; }
+ else { isEarly ? t[1].e++ : t[1].l++; }
+ } else {
+ const t = hitTally.std;
+ if (msAbs <= windows[0]) {}
+ else if (msAbs <= windows[1]) { isEarly ? t[0].e++ : t[0].l++; }
+ else if (msAbs <= windows[2]) { isEarly ? t[1].e++ : t[1].l++; }
+ else { isEarly ? t[2].e++ : t[2].l++; }
+ }
+ });
+ }
+
+ if (!settings.hideUR) {
+ if (hitErrors.length > 0) {
+ let sum = 0, sumSq = 0;
+ const len = hitErrors.length;
+ for (let i = 0; i < len; i++) {
+ sum += hitErrors[i];
+ sumSq += hitErrors[i] * hitErrors[i];
+ }
+ const mean = sum / len;
+ const ur = Math.sqrt((sumSq / len) - (mean * mean)) * 10;
+ updateRow(elUr, string.global.ur, ur.toFixed(2));
+ } else {
+ updateRow(elUr, string.global.ur, "0.00");
+ }
+ }
+}, ["hitErrors", "currentTime"]);
\ No newline at end of file
diff --git a/counters/Hit Count by Albert/mania.png b/counters/Hit Count by Albert/mania.png
new file mode 100644
index 0000000..7401f4e
Binary files /dev/null and b/counters/Hit Count by Albert/mania.png differ
diff --git a/counters/Hit Count by Albert/metadata.txt b/counters/Hit Count by Albert/metadata.txt
new file mode 100644
index 0000000..bbf69fa
--- /dev/null
+++ b/counters/Hit Count by Albert/metadata.txt
@@ -0,0 +1,7 @@
+ Usecase: Ingame Overlay, OBS Overlay
+Name: Hit Count
+Version: 1.0.0
+Author: Albert
+CompatibleWith: Tosu
+Resolution: 240x400
+authorLinks: https://github.com/AlberttFrgk/
\ No newline at end of file
diff --git a/counters/Hit Count by Albert/osu.png b/counters/Hit Count by Albert/osu.png
new file mode 100644
index 0000000..5711a7d
Binary files /dev/null and b/counters/Hit Count by Albert/osu.png differ
diff --git a/counters/Hit Count by Albert/settings.json b/counters/Hit Count by Albert/settings.json
new file mode 100644
index 0000000..82ca9a0
--- /dev/null
+++ b/counters/Hit Count by Albert/settings.json
@@ -0,0 +1,86 @@
+[
+ { "uniqueID": "sep1", "type": "label", "title": "━━━━━━━━ GLOBAL SETTINGS ━━━━━━━━", "description": "", "options": [], "value": "" },
+ { "uniqueID": "globalTextColor", "type": "color", "title": "Global Text Color", "description": "Base color for all text on the overlay.", "options": [], "value": "#ffffff" },
+ { "uniqueID": "swapLabelValue", "type": "checkbox", "title": "Swap Labels & Counters Position", "description": "Moves counters to the left and labels to the right.", "options": [], "value": false },
+ { "uniqueID": "fontName", "type": "text", "title": "Font Name", "description": "Name font installed on Windows (e.g., Verdana, Arial, etc).", "options": [], "value": "Arial" },
+ { "uniqueID": "useCustomFont", "type": "checkbox", "title": "Use Custom Font", "description": "Loads a custom font file from the overlay folder.", "options": [], "value": false },
+ { "uniqueID": "customFontName", "type": "text", "title": "Custom Font Filename", "description": "Example: font.ttf", "options": [], "value": "font.ttf" },
+ { "uniqueID": "labelColumnWidth", "type": "number", "title": "Label Column Width (px)", "description": "Width for text labels. If swapped, this applies to the right side.", "options": [], "value": 130 },
+ { "uniqueID": "valueColumnWidth", "type": "number", "title": "Value Column Width (px)", "description": "Width for numbers. If swapped, this applies to the left side.", "options": [], "value": 90 },
+ { "uniqueID": "lineHeight", "type": "number", "title": "Line Height", "description": "Vertical spacing between rows (e.g. 1.25, 1.4).", "options": [], "value": 1.25 },
+
+ { "uniqueID": "sep2", "type": "label", "title": "━━━━━━━━ PP SETTINGS ━━━━━━━━", "description": "", "options": [], "value": "" },
+ { "uniqueID": "hidePP", "type": "checkbox", "title": "Hide PP", "description": "Hides the PP display.", "options": [], "value": false },
+ { "uniqueID": "useCustomPPColors", "type": "checkbox", "title": "Use Custom PP Colors", "description": "Enable specific colors for PP.", "options": [], "value": false },
+ { "uniqueID": "colorPPLabel", "type": "color", "title": "PP Label Color", "description": "", "options": [], "value": "#ffffff" },
+ { "uniqueID": "colorPPVal", "type": "color", "title": "PP Number Color", "description": "", "options": [], "value": "#ffffff" },
+
+ { "uniqueID": "sep3", "type": "label", "title": "━━━━━━━━ UR SETTINGS ━━━━━━━━", "description": "", "options": [], "value": "" },
+ { "uniqueID": "hideUR", "type": "checkbox", "title": "Hide UR", "description": "Hides the Unstable Rate display.", "options": [], "value": false },
+ { "uniqueID": "useCustomURColors", "type": "checkbox", "title": "Use Custom UR Colors", "description": "Enable specific colors for UR.", "options": [], "value": false },
+ { "uniqueID": "colorURLabel", "type": "color", "title": "UR Label Color", "description": "", "options": [], "value": "#ffffff" },
+ { "uniqueID": "colorURVal", "type": "color", "title": "UR Number Color", "description": "", "options": [], "value": "#ffffff" },
+
+ { "uniqueID": "sep4", "type": "label", "title": "━━━━━━━━ RATIO SETTINGS ━━━━━━━━", "description": "", "options": [], "value": "" },
+ { "uniqueID": "hideRatio", "type": "checkbox", "title": "Hide Ratio", "description": "Hides the hit ratio display.", "options": [], "value": false },
+ { "uniqueID": "useCustomRatioColors", "type": "checkbox", "title": "Use Custom Ratio Colors", "description": "Enable specific colors for Ratio.", "options": [], "value": false },
+ { "uniqueID": "colorRatioLabel", "type": "color", "title": "Ratio Label Color", "description": "", "options": [], "value": "#ffffff" },
+ { "uniqueID": "colorRatioVal", "type": "color", "title": "Ratio Number Color", "description": "", "options": [], "value": "#ffffff" },
+
+ { "uniqueID": "sep5", "type": "label", "title": "━━━━━━━━ MAX COMBO SETTINGS ━━━━━━━━", "description": "", "options": [], "value": "" },
+ { "uniqueID": "hideMaxCombo", "type": "checkbox", "title": "Hide Max Combo", "description": "Hides the max combo display.", "options": [], "value": false },
+ { "uniqueID": "useCustomComboColors", "type": "checkbox", "title": "Use Custom Combo Colors", "description": "Enable specific colors for Combo.", "options": [], "value": false },
+ { "uniqueID": "colorComboLabel", "type": "color", "title": "Combo Label Color", "description": "", "options": [], "value": "#ffffff" },
+ { "uniqueID": "colorComboVal", "type": "color", "title": "Combo Number Color", "description": "", "options": [], "value": "#ffffff" },
+
+ { "uniqueID": "sep6", "type": "label", "title": "━━━━━━━━ EARLY / LATE SETTINGS ━━━━━━━━", "description": "", "options": [], "value": "" },
+ { "uniqueID": "hideEarlyLate", "type": "checkbox", "title": "Hide Early/Late", "description": "Hides the early and late hit counts.", "options": [], "value": false },
+ { "uniqueID": "useCustomEarlyLateColors", "type": "checkbox", "title": "Use Custom Early/Late Colors", "description": "Enable specific colors for Early and Late.", "options": [], "value": true },
+ { "uniqueID": "colorEarlyLabel", "type": "color", "title": "Early Label Color", "description": "", "options": [], "value": "#0000ff" },
+ { "uniqueID": "colorEarlyVal", "type": "color", "title": "Early Number Color", "description": "", "options": [], "value": "#ffffff" },
+ { "uniqueID": "colorLateLabel", "type": "color", "title": "Late Label Color", "description": "", "options": [], "value": "#ff0000" },
+ { "uniqueID": "colorLateVal", "type": "color", "title": "Late Number Color", "description": "", "options": [], "value": "#ffffff" },
+
+ { "uniqueID": "sep7", "type": "label", "title": "━━━━━━━━ HIT COUNTS SETTINGS ━━━━━━━━", "description": "", "options": [], "value": "" },
+ { "uniqueID": "hideHitCounts", "type": "checkbox", "title": "Hide Hit Counts", "description": "Hides 300g, 300, 200, 100, 50, and Miss counts.", "options": [], "value": false },
+ { "uniqueID": "useCustomHitCountLabelColors", "type": "checkbox", "title": "Use Custom Hit Count Label Colors", "description": "Enable specific colors for judgement labels.", "options": [], "value": true },
+ { "uniqueID": "useCustomHitCountNumberColors", "type": "checkbox", "title": "Use Custom Hit Count Number Colors", "description": "Enable specific colors for judgement numbers.", "options": [], "value": false },
+
+ { "uniqueID": "sep8", "type": "label", "title": "▶ osu! Colors", "description": "", "options": [], "value": "" },
+ { "uniqueID": "colorOsu300Label", "type": "color", "title": "osu! 300 Label Color", "description": "", "options": [], "value": "#50b4ff" },
+ { "uniqueID": "colorOsu300Val", "type": "color", "title": "osu! 300 Number Color", "description": "", "options": [], "value": "#50b4ff" },
+ { "uniqueID": "colorOsu100Label", "type": "color", "title": "osu! 100 Label Color", "description": "", "options": [], "value": "#47e547" },
+ { "uniqueID": "colorOsu100Val", "type": "color", "title": "osu! 100 Number Color", "description": "", "options": [], "value": "#47e547" },
+ { "uniqueID": "colorOsu50Label", "type": "color", "title": "osu! 50 Label Color", "description": "", "options": [], "value": "#ffcc22" },
+ { "uniqueID": "colorOsu50Val", "type": "color", "title": "osu! 50 Number Color", "description": "", "options": [], "value": "#ffcc22" },
+
+ { "uniqueID": "sep9", "type": "label", "title": "▶ Taiko Colors", "description": "", "options": [], "value": "" },
+ { "uniqueID": "colorTaiko300Label", "type": "color", "title": "Taiko Great Label Color", "description": "", "options": [], "value": "#ffcc22" },
+ { "uniqueID": "colorTaiko300Val", "type": "color", "title": "Taiko Great Number Color", "description": "", "options": [], "value": "#ffcc22" },
+ { "uniqueID": "colorTaiko100Label", "type": "color", "title": "Taiko Ok Label Color", "description": "", "options": [], "value": "#47e547" },
+ { "uniqueID": "colorTaiko100Val", "type": "color", "title": "Taiko Ok Number Color", "description": "", "options": [], "value": "#47e547" },
+
+ { "uniqueID": "sep10", "type": "label", "title": "▶ Catch Colors", "description": "", "options": [], "value": "" },
+ { "uniqueID": "colorCatch300Label", "type": "color", "title": "Catch Fruit Label Color", "description": "", "options": [], "value": "#ffcc22" },
+ { "uniqueID": "colorCatch300Val", "type": "color", "title": "Catch Fruit Number Color", "description": "", "options": [], "value": "#ffcc22" },
+ { "uniqueID": "colorCatch100Label", "type": "color", "title": "Catch Drop Label Color", "description": "", "options": [], "value": "#47e547" },
+ { "uniqueID": "colorCatch100Val", "type": "color", "title": "Catch Drop Number Color", "description": "", "options": [], "value": "#47e547" },
+ { "uniqueID": "colorCatch50Label", "type": "color", "title": "Catch Droplet Label Color", "description": "", "options": [], "value": "#50b4ff" },
+ { "uniqueID": "colorCatch50Val", "type": "color", "title": "Catch Droplet Number Color", "description": "", "options": [], "value": "#50b4ff" },
+
+ { "uniqueID": "sep11", "type": "label", "title": "▶ Mania Colors", "description": "", "options": [], "value": "" },
+ { "uniqueID": "colorMania300gLabel", "type": "color", "title": "Mania MAX Label Color", "description": "", "options": [], "value": "#ffffff" },
+ { "uniqueID": "colorMania300gVal", "type": "color", "title": "Mania MAX Number Color", "description": "", "options": [], "value": "#ffffff" },
+ { "uniqueID": "colorMania300Label", "type": "color", "title": "Mania Perfect Label Color", "description": "", "options": [], "value": "#ffcc22" },
+ { "uniqueID": "colorMania300Val", "type": "color", "title": "Mania Perfect Number Color", "description": "", "options": [], "value": "#ffcc22" },
+ { "uniqueID": "colorMania200Label", "type": "color", "title": "Mania Great Label Color", "description": "", "options": [], "value": "#47e547" },
+ { "uniqueID": "colorMania200Val", "type": "color", "title": "Mania Great Number Color", "description": "", "options": [], "value": "#47e547" },
+ { "uniqueID": "colorMania100Label", "type": "color", "title": "Mania Good Label Color", "description": "", "options": [], "value": "#50b4ff" },
+ { "uniqueID": "colorMania100Val", "type": "color", "title": "Mania Good Number Color", "description": "", "options": [], "value": "#50b4ff" },
+ { "uniqueID": "colorMania50Label", "type": "color", "title": "Mania Bad Label Color", "description": "", "options": [], "value": "#888888" },
+ { "uniqueID": "colorMania50Val", "type": "color", "title": "Mania Bad Number Color", "description": "", "options": [], "value": "#888888" },
+
+ { "uniqueID": "sep12", "type": "label", "title": "▶ Miss Color (All Modes)", "description": "", "options": [], "value": "" },
+ { "uniqueID": "colorMissLabel", "type": "color", "title": "Miss Label Color", "description": "", "options": [], "value": "#ff0000" },
+ { "uniqueID": "colorMissVal", "type": "color", "title": "Miss Number Color", "description": "", "options": [], "value": "#ff0000" }
+]
\ No newline at end of file
diff --git a/counters/Hit Count by Albert/taiko.png b/counters/Hit Count by Albert/taiko.png
new file mode 100644
index 0000000..fca65f2
Binary files /dev/null and b/counters/Hit Count by Albert/taiko.png differ