Shows how many OCs are currently visible for each level above the OC list
// ==UserScript==
// @name Torn OC Level Counter
// @namespace Greasemonkey Scripts
// @match https://www.torn.com/factions.php?step=your*
// @grant none
// @version 1.1
// @license MIT
// @author Crognell [3208324]
// @description Shows how many OCs are currently visible for each level above the OC list
// ==/UserScript==
(function () {
'use strict';
const SCRIPT_VERSION = '1.1';
const TARGET_HASH_FRAGMENT = '/tab=crimes';
const SUMMARY_ID = 'tm-oc-level-summary';
const STYLE_ID = 'tm-oc-level-summary-style';
let updateTimer = null;
function isCrimesTab() {
return window.location.hash.includes(TARGET_HASH_FRAGMENT);
}
function isVisible(element) {
if (!element || !document.contains(element)) return false;
const style = window.getComputedStyle(element);
if (style.display === 'none' || style.visibility === 'hidden') {
return false;
}
const rect = element.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
function addStyles() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = `
#${SUMMARY_ID} {
margin: 12px 0 14px 0;
padding: 14px 16px;
border-radius: 12px;
background: linear-gradient(180deg, rgba(32,36,45,0.95), rgba(21,24,31,0.95));
border: 1px solid rgba(255,255,255,0.08);
box-shadow: 0 6px 18px rgba(0,0,0,0.28);
color: #e8edf5;
font-family: Arial, sans-serif;
}
#${SUMMARY_ID} .tm-oc-summary-title {
font-size: 15px;
font-weight: 700;
margin-bottom: 10px;
color: #ffffff;
letter-spacing: 0.2px;
}
#${SUMMARY_ID} .tm-oc-summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
}
#${SUMMARY_ID} .tm-oc-summary-card {
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 10px;
padding: 10px 12px;
text-align: center;
}
#${SUMMARY_ID} .tm-oc-summary-level {
font-size: 12px;
color: #aeb8c7;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.4px;
}
#${SUMMARY_ID} .tm-oc-summary-count {
font-size: 22px;
font-weight: 800;
line-height: 1.1;
color: #ffffff;
}
#${SUMMARY_ID} .tm-oc-summary-zero .tm-oc-summary-count {
color: #8f9bad;
}
#${SUMMARY_ID} .tm-oc-summary-note {
margin-top: 10px;
font-size: 12px;
color: #9eabba;
}
`;
document.head.appendChild(style);
}
function removeSummary() {
const existing = document.getElementById(SUMMARY_ID);
if (existing) existing.remove();
}
function getVisibleLevelCounts() {
const counts = {};
for (let i = 1; i <= 8; i++) counts[i] = 0;
const spans = document.querySelectorAll('span[class^="levelValue_"], span[class*=" levelValue_"]');
spans.forEach(span => {
if (!isVisible(span)) return;
const text = (span.textContent || '').trim();
const match = text.match(/\d+/);
if (!match) return;
const level = Number(match[0]);
if (level >= 1 && level <= 8) {
counts[level] += 1;
}
});
return counts;
}
function buildSummaryMarkup(counts) {
const total = Object.values(counts).reduce((sum, count) => sum + count, 0);
return `
<div class="tm-oc-summary-title">Current OC Levels Available</div>
<div class="tm-oc-summary-grid">
${Object.entries(counts).map(([level, count]) => `
<div class="tm-oc-summary-card ${count === 0 ? 'tm-oc-summary-zero' : ''}">
<div class="tm-oc-summary-level">OC Level ${level}</div>
<div class="tm-oc-summary-count">${count === 0 ? 'None' : count}</div>
</div>
`).join('')}
</div>
<div class="tm-oc-summary-note">Total visible OCs: ${total}</div>
`;
}
function findAnchorHr() {
return document.querySelector('hr.page-head-delimiter.m-top10.m-bottom10');
}
function getOrCreateSummary(anchorHr) {
let summary = document.getElementById(SUMMARY_ID);
if (summary) return summary;
summary = document.createElement('div');
summary.id = SUMMARY_ID;
anchorHr.insertAdjacentElement('afterend', summary);
return summary;
}
function renderSummary() {
if (!isCrimesTab()) {
removeSummary();
return;
}
addStyles();
const anchorHr = findAnchorHr();
if (!anchorHr || !anchorHr.parentElement) {
removeSummary();
return;
}
const counts = getVisibleLevelCounts();
const markup = buildSummaryMarkup(counts);
const summary = getOrCreateSummary(anchorHr);
if (summary.innerHTML !== markup) {
summary.innerHTML = markup;
}
}
function queueRender(delay = 250) {
clearTimeout(updateTimer);
updateTimer = window.setTimeout(renderSummary, delay);
}
function mutationNeedsRender(mutations) {
return mutations.some(mutation => {
const target = mutation.target.nodeType === Node.TEXT_NODE
? mutation.target.parentElement
: mutation.target;
if (!target) return false;
if (!(target instanceof Element)) return true;
if (target.id === SUMMARY_ID || target.id === STYLE_ID) return false;
if (target.closest(`#${SUMMARY_ID}`)) return false;
if (target.closest(`#${STYLE_ID}`)) return false;
return true;
});
}
const observer = new MutationObserver(mutations => {
if (mutationNeedsRender(mutations)) {
queueRender();
}
});
function startObserver() {
observer.disconnect();
if (!document.body) return;
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
characterData: true
});
}
function init() {
startObserver();
queueRender(0);
window.setTimeout(() => queueRender(0), 1200);
}
window.addEventListener('hashchange', () => queueRender(0));
window.addEventListener('load', () => queueRender(0));
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
console.log(`[Torn OC Level Counter v${SCRIPT_VERSION}] Loaded`);
})();