Sheepies Modular AIO
// ==UserScript==
// @name SheepieAIO v3 Core
// @version 3.0.20
// @namespace https://sheepie.ca/
// @author SheepPrincess [2679129]
// @icon https://sheepie.ca/img/SheepieWorld64.png
// @description Sheepies Modular AIO
// @license MIT
// @match https://www.torn.com/*
// @match torn.com/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_info
// @grant GM_registerMenuCommand
// @grant unsafeWindow
// @connect sheepie.ca
// @connect www.sheepie.ca
// @connect tornprobability.com
// @connect script.google.com
// @connect script.googleusercontent.com
// @connect api.torn.com
// ==/UserScript==
let sheepColour = "#734180"
const sheepAPI = localStorage.getItem("sheepAPI");
if (sheepAPI === null) {
if (window.location.href === "https://www.torn.com/preferences.php#tab=api") {createButtonAtPositionAPI("API", 40, 550);} else {
createButtonAtPositionAPI2("API", 40, 550);}}
function createButtonAtPositionAPI(text, topPx, leftPx) {
var customButton = document.createElement("button");
customButton.style.width = "300px";
customButton.style.height = "150px";
customButton.innerHTML = 'Click Here To Enter Public API';
customButton.style.position = 'fixed';
customButton.style.top = topPx + 'px';
customButton.style.left = leftPx + 'px';
customButton.style.backgroundColor = sheepColour;
customButton.style.color = 'white';
customButton.style.border = 'none';
customButton.style.cursor = 'pointer';
customButton.style.border = "1px solid purple";
customButton.style.zIndex = '99999999999999';
customButton.addEventListener('click', function() {
let sheepAPI = localStorage.getItem("sheepAPI");
if (sheepAPI === null) {
sheepAPI = prompt("Please enter your Public API key. Please note your api key is stored unencrypted in your browsers localstorage, its used for everything from DAILY player verification, gathering members OC roleweights, confirming subscriptions... to the market pricer. Yeah that ended off underwhelmingly lol. anyway a temp token will be created upon validation allowing access to the dashboard or script for 24hrs before verifying your ID and Faction again automatically. Sheepiecult, Sheepie.ca and SheepPrincess does not store your API key on our backend.");
// Check if input is 16 characters and only letters/numbers
if (/^[a-zA-Z0-9]{16}$/.test(sheepAPI)) {
localStorage.setItem("sheepAPI", sheepAPI);
window.location.reload();
} else {
alert("Invalid API key.");
// Optionally, you can prompt again
}
}
});
document.body.appendChild(customButton);
}
function createButtonAtPositionAPI2(text, topPx, leftPx) {
var customButton = document.createElement("button");
customButton.style.width = "300px";
customButton.style.height = "150px";
customButton.innerHTML = 'Click Here To Get Public API key';
customButton.style.position = 'fixed';
customButton.style.top = topPx + 'px';
customButton.style.left = leftPx + 'px';
customButton.style.backgroundColor = sheepColour;
customButton.style.color = 'white';
customButton.style.border = 'none';
customButton.style.cursor = 'pointer';
customButton.style.border = "1px solid purple";
customButton.style.zIndex = '99999999999999';
customButton.addEventListener('click', function() {
const sheepAPI = localStorage.getItem("sheepAPI");
if (sheepAPI === null) {
window.open('https://www.torn.com/preferences.php#tab=api').focus();
} else {
}
});
document.body.appendChild(customButton);
}
function isTornPDA() {
return /TornPDA/i.test(navigator.userAgent);
}
// ================================
// 🐑 Sheepie Ghetto YT Player w/ Persistence
// ================================
function initYouTube(){
'use strict';
if (window.SHEEPIE_YT) return;
window.SHEEPIE_YT = true;
const STORAGE_KEY = "tmYTState";
const VISIBILITY_KEY = "tmYTVisible";
const POSITION_KEY = "tmYTPosition"; // original unminimized position/size
const AUTO_SAVE_MS = 1000;
let accumulatedTime = 0;
let sessionStart = null;
let autoSaveInterval = null;
let isMinimized = false;
let hasInteracted = false;
/* ---------- Buttons ---------- */
const openBtn = document.createElement("button");
openBtn.textContent = "YT";
openBtn.style.cssText = `
position: fixed; bottom: 40px; left: 0px; z-index: 10000001;
background: red; color: white; font-size: 24px; border: 1px solid purple;
border-radius: 4px; padding: 6px 10px; cursor: pointer;
`
const closeBtn = document.createElement("button");
closeBtn.textContent = "YT";
closeBtn.style.cssText = `
position: fixed; bottom: 40px; left: 0px; z-index: 9999999;
background: red; color: white; font-size: 24px; border: 1px solid purple;
border-radius: 4px; padding: 6px 10px; cursor: pointer; display: none;
}`
;
requestIdleCallback(() => {
document.body.appendChild(openBtn);
document.body.appendChild(closeBtn);
});
/* ---------- Player Wrapper ---------- */
const wrapper = document.createElement("div");
wrapper.style.cssText = `
position: fixed; bottom: 60px; right: 40px; width: 420px; height: 340px;
background: #0f0f0f; border: 1px solid #333; border-radius: 8px;
z-index: 9999998; display: none; flex-direction: column; font-family: Arial;
box-shadow: 0 8px 20px rgba(0,0,0,0.6); overflow: hidden; cursor: default;
`;
wrapper.inert = true;
wrapper.innerHTML = `
<div id="tm-header" style="background:#1b1b1b;padding:6px;color:#ccc;display:flex;justify-content:space-between;cursor:move">
<span id="tm-title">YT 00:00</span>
<button id="tm-minimize" style="background:none;border:none;color:#ccc;cursor:pointer">–</button>
</div>
<div id="tm-content" style="display:flex;flex-direction:column;flex:1;overflow:hidden;">
<div style="display:flex;">
<input id="tm-input" placeholder="Paste YouTube link"
style="flex:1;border:none;padding:6px;font-size:12px;outline:none;color:#ccc;background:#151515">
<button id="tm-go" style="padding:6px 8px;font-size:12px;background:#333;color:#ccc;border:none;cursor:pointer">Go</button>
</div>
<iframe id="tm-frame" style="flex:1;border:none"
allow="autoplay; encrypted-media" allowfullscreen></iframe>
<div style="display:flex;justify-content:flex-end;background:#1b1b1b;padding:4px;gap:4px">
<button id="tm-save" style="padding:4px 6px;font-size:12px;background:#333;color:#ccc;border:none;border-radius:4px;cursor:pointer">Save Position</button>
<button id="tm-clear" style="padding:4px 6px;font-size:12px;background:#444;color:#ccc;border:none;border-radius:4px;cursor:pointer">Clear Saved Video</button>
</div>
</div>
<div id="tm-resize"
style="width:12px;height:12px;background:#444;position:absolute;bottom:2px;right:2px;cursor:se-resize">
</div>
`;
document.body.appendChild(wrapper);
const frame = wrapper.querySelector("#tm-frame");
const input = wrapper.querySelector("#tm-input");
const saveBtn = wrapper.querySelector("#tm-save");
const goBtn = wrapper.querySelector("#tm-go");
const clearBtn = wrapper.querySelector("#tm-clear");
const minimizeBtn = wrapper.querySelector("#tm-minimize");
const contentDiv = wrapper.querySelector("#tm-content");
const header = wrapper.querySelector("#tm-header");
const titleSpan = wrapper.querySelector("#tm-title");
/* ---------- Storage Helpers ---------- */
function saveState(videoId, playlistId, time) {
localStorage.setItem(STORAGE_KEY, JSON.stringify({ videoId, playlistId, time }));
}
function loadState() {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
}
function clearState() {
localStorage.removeItem(STORAGE_KEY);
accumulatedTime = 0;
sessionStart = null;
}
function saveVisibility(visible) {
localStorage.setItem(VISIBILITY_KEY, visible ? "1" : "0");
}
function loadVisibility() {
return localStorage.getItem(VISIBILITY_KEY) === "1";
}
function saveWrapperPosition(pos) {
localStorage.setItem(POSITION_KEY, JSON.stringify(pos));
}
function loadWrapperPosition() {
return JSON.parse(localStorage.getItem(POSITION_KEY) || "{}");
}
/* ---------- Video Load ---------- */
function extractIds(url) {
const videoMatch = url.match(/(?:v=|youtu\.be\/)([\w-]+)/);
const playlistMatch = url.match(/[?&]list=([\w-]+)/);
return { videoId: videoMatch ? videoMatch[1] : null, playlistId: playlistMatch ? playlistMatch[1] : null };
}
function buildEmbedURL(url, startTime = 0, muted = true) {
const { videoId, playlistId } = extractIds(url);
const muteParam = muted ? "&mute=1" : "";
if (playlistId && !videoId) return `https://www.youtube.com/embed/videoseries?list=${playlistId}&autoplay=1&start=${startTime}${muteParam}`;
if (videoId) {
if (playlistId) return `https://www.youtube.com/embed/${videoId}?list=${playlistId}&autoplay=1&start=${startTime}${muteParam}`;
return `https://www.youtube.com/embed/${videoId}?autoplay=1&start=${startTime}${muteParam}`;
}
return null;
}
function loadVideo(url) {
const state = loadState();
const { videoId, playlistId } = extractIds(url);
const embedURL = buildEmbedURL(url, state.time || 0, !hasInteracted);
if (!embedURL) return;
if (!frame.src || !frame.src.includes(videoId || playlistId)) frame.src = embedURL;
saveState(videoId, playlistId, state.time || 0);
}
/* ---------- Timer ---------- */
function formatTime(s) {
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = s % 60;
return `${h>0?h+":" : ""}${m.toString().padStart(2,"0")}:${sec.toString().padStart(2,"0")}`;
}
function updateTitleTimestamp() {
const elapsed = sessionStart ? Math.floor((Date.now() - sessionStart)/1000) : 0;
titleSpan.textContent = `YT ${formatTime(accumulatedTime + elapsed)}`;
}
function startAutoSave() {
if (autoSaveInterval) return;
sessionStart = Date.now();
autoSaveInterval = setInterval(() => {
const elapsed = Math.floor((Date.now() - sessionStart)/1000);
const totalTime = accumulatedTime + elapsed;
const state = loadState();
saveState(state.videoId, state.playlistId, totalTime);
updateTitleTimestamp();
}, AUTO_SAVE_MS);
}
function stopSessionTimer() {
if (!sessionStart) return;
const elapsed = Math.floor((Date.now() - sessionStart)/1000);
accumulatedTime += elapsed;
sessionStart = null;
if (autoSaveInterval) {
clearInterval(autoSaveInterval);
autoSaveInterval = null;
}
}
/* ---------- Restore State ---------- */
const lastState = loadState();
accumulatedTime = lastState.time || 0;
let savedPos = loadWrapperPosition();
const defaultWidth = 420;
const defaultHeight = 340;
wrapper.style.left = savedPos.left != null ? Math.min(savedPos.left, window.innerWidth - (savedPos.width || defaultWidth)) + "px" : "40px";
wrapper.style.top = savedPos.top != null ? Math.min(savedPos.top, window.innerHeight - (savedPos.height || defaultHeight)) + "px" : "60px";
wrapper.style.width = (savedPos.width || defaultWidth) + "px";
wrapper.style.height = (savedPos.height || defaultHeight) + "px";
if (!frame.src && (lastState.videoId || lastState.playlistId)) {
const url = lastState.videoId ? `https://www.youtube.com/watch?v=${lastState.videoId}` : `https://www.youtube.com/playlist?list=${lastState.playlistId}`;
loadVideo(url);
}
/* ---------- Show/Hide ---------- */
function updateButtons() {
if (wrapper.style.display === "none") {
openBtn.style.display = "block";
closeBtn.style.display = "none";
} else {
openBtn.style.display = "none";
closeBtn.style.display = "block";
}
}
function minimizePlayer() {
isMinimized = true;
contentDiv.style.display = "none";
wrapper.style.height = "30px";
}
function restorePlayer() {
isMinimized = false;
contentDiv.style.display = "flex";
wrapper.style.width = (savedPos.width || defaultWidth) + "px";
wrapper.style.height = (savedPos.height || defaultHeight) + "px";
}
function showPlayer(visible) {
wrapper.style.display = visible ? "flex" : "none";
wrapper.inert = !visible;
saveVisibility(visible);
if (visible) {
restorePlayer();
startAutoSave();
} else {
stopSessionTimer();
}
updateButtons();
}
openBtn.addEventListener("click", () => showPlayer(true));
closeBtn.addEventListener("click", () => {
showPlayer(false);
clearState();
});
minimizeBtn.addEventListener("click", () => {
if (contentDiv.style.display === "none") restorePlayer();
else minimizePlayer();
});
/* ---------- Input ---------- */
function handleInput() {
const url = input.value.trim();
if (!url) return;
loadVideo(url);
input.value = "";
hasInteracted = true;
}
input.addEventListener("keypress", e => { if (e.key === "Enter") handleInput(); });
goBtn.addEventListener("click", handleInput);
/* ---------- Save / Clear ---------- */
saveBtn.addEventListener("click", () => {
const elapsed = sessionStart ? Math.floor((Date.now() - sessionStart)/1000) : 0;
const totalTime = accumulatedTime + elapsed;
const state = loadState();
saveState(state.videoId, state.playlistId, totalTime);
alert(`Position saved at ~${totalTime} seconds`);
});
clearBtn.addEventListener("click", () => {
clearState();
frame.src = "";
alert("Saved video data cleared.");
});
/* ---------- Drag ---------- */
let dragging = false, offsetX, offsetY;
header.addEventListener("mousedown", (e) => { dragging = true; offsetX = e.clientX - wrapper.offsetLeft; offsetY = e.clientY - wrapper.offsetTop; });
document.addEventListener("mousemove", (e) => {
if (!dragging) return;
wrapper.style.left = e.clientX - offsetX + "px";
wrapper.style.top = e.clientY - offsetY + "px";
wrapper.style.right = "auto";
wrapper.style.bottom = "auto";
});
document.addEventListener("mouseup", () => {
if (dragging) {
dragging = false;
if (!isMinimized) {
savedPos.left = parseInt(wrapper.style.left,10);
savedPos.top = parseInt(wrapper.style.top,10);
savedPos.width = wrapper.offsetWidth;
savedPos.height = wrapper.offsetHeight;
saveWrapperPosition(savedPos);
}
}
});
/* ---------- Resize ---------- */
const resize = wrapper.querySelector("#tm-resize");
let resizing = false, startW, startH, startX, startY;
resize.addEventListener("mousedown", (e) => { resizing = true; startW = wrapper.offsetWidth; startH = wrapper.offsetHeight; startX = e.clientX; startY = e.clientY; e.preventDefault(); });
document.addEventListener("mousemove", (e) => {
if (!resizing) return;
wrapper.style.width = startW + (e.clientX - startX) + "px";
wrapper.style.height = startH + (e.clientY - startY) + "px";
});
document.addEventListener("mouseup", () => {
if (resizing) {
resizing = false;
if (!isMinimized) {
savedPos.width = wrapper.offsetWidth;
savedPos.height = wrapper.offsetHeight;
savedPos.left = parseInt(wrapper.style.left,10);
savedPos.top = parseInt(wrapper.style.top,10);
saveWrapperPosition(savedPos);
}
}
});
/* ---------- INITIAL ---------- */
showPlayer(loadVisibility());
updateTitleTimestamp();
}
// ================================
// 🐑 Sheepie TCT Timezone Engine w/ Persistence
// ================================
function initTimezoneEngine(){
'use strict';
if (window.SHEEPIE_TIMEZONE) return;
window.SHEEPIE_TIMEZONE = true;
if (document.getElementById("sheepieTctWrapper")) return;
// =====================
// Base Zones
// =====================
const DEFAULT_ZONES = [
{ label: "Netherlands", zone: "Europe/Amsterdam" },
{ label: "Syria", zone: "Asia/Damascus" },
{ label: "Thailand", zone: "Asia/Bangkok" },
{ label: "Malaysia", zone: "Asia/Kuala_Lumpur" },
{ label: "Halifax", zone: "America/Halifax" },
{ label: "Toronto", zone: "America/Toronto" },
{ label: "Winnipeg", zone: "America/Winnipeg" },
{ label: "Calgary", zone: "America/Edmonton" },
{ label: "Vancouver", zone: "America/Vancouver" }
];
const STORAGE_KEY = "sheepieTctZones";
let activeZones = [];
// Load saved zones if exist
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
try {
activeZones = JSON.parse(saved);
} catch {
activeZones = [...DEFAULT_ZONES];
}
} else {
activeZones = [...DEFAULT_ZONES];
}
const allTimezones = Intl.supportedValuesOf("timeZone");
let interval = null;
let manualMode = false;
// =====================
// Wrapper
// =====================
const wrapper = document.createElement("div");
wrapper.id = "sheepieTctWrapper";
wrapper.style.position = "fixed";
wrapper.style.bottom = "40px";
wrapper.style.right = "0px";
wrapper.style.zIndex = "9999";
wrapper.style.fontFamily = "Verdana, sans-serif";
requestIdleCallback(() => {
document.body.appendChild(wrapper);
});
const toggle = document.createElement("button");
toggle.textContent = "⏰";
toggle.style.background = sheepColour;
toggle.style.color = "#000";
toggle.style.border = "none";
toggle.style.cursor = "pointer";
toggle.style.borderRadius = "4px";
toggle.style.fontSize = "100%";
wrapper.appendChild(toggle);
const panel = document.createElement("div");
panel.style.display = "none";
panel.style.marginTop = "6px";
panel.style.background = "#111";
panel.style.border = `1px solid ${sheepColour}`;
panel.style.padding = "10px";
panel.style.borderRadius = "6px";
panel.style.width = "150px";
wrapper.appendChild(panel);
// =====================
// Manual TCT Time
// =====================
const manualInput = document.createElement("input");
manualInput.type = "time";
manualInput.step = "1";
manualInput.style.width = "100%";
manualInput.style.marginBottom = "6px";
panel.appendChild(manualInput);
const manualToggle = document.createElement("button");
manualToggle.textContent = "Live Mode";
manualToggle.style.width = "100%";
manualToggle.style.marginBottom = "10px";
manualToggle.style.cursor = "pointer";
panel.appendChild(manualToggle);
// =====================
// Sheepie Dropdown Manual Time Picker
// =====================
const pickerContainer = document.createElement("div");
pickerContainer.style.display = "none";
pickerContainer.style.position = "absolute";
pickerContainer.style.top = "25px"; // just below manual input
pickerContainer.style.left = "10";
pickerContainer.style.background = "#111";
pickerContainer.style.border = `1px solid ${sheepColour}`;
pickerContainer.style.borderRadius = "6px";
pickerContainer.style.padding = "6px";
pickerContainer.style.zIndex = "999";
pickerContainer.style.display = "flex";
pickerContainer.style.gap = "6px";
pickerContainer.style.fontSize = "12px";
// attach to the parent of manualInput
manualInput.parentElement.style.position = "relative";
manualInput.parentElement.appendChild(pickerContainer);
// helper to create a select element
function createSelect(min, max) {
const sel = document.createElement("select");
sel.style.background = "#222";
sel.style.color = "white";
sel.style.border = `1px solid ${sheepColour}`;
sel.style.borderRadius = "4px";
sel.style.padding = "2px 4px";
sel.style.cursor = "pointer";
for (let i = min; i <= max; i++) {
const opt = document.createElement("option");
opt.value = i.toString().padStart(2, "0");
opt.textContent = i.toString().padStart(2, "0");
sel.appendChild(opt);
}
return sel;
}
// create hour, minute, second selects
const hourSelect = createSelect(0, 23);
const minuteSelect = createSelect(0, 59);
const secondSelect = createSelect(0, 59);
pickerContainer.appendChild(hourSelect);
pickerContainer.appendChild(minuteSelect);
pickerContainer.appendChild(secondSelect);
// sync picker -> manualInput
function updateManualInputFromPicker() {
const h = hourSelect.value;
const m = minuteSelect.value;
const s = secondSelect.value;
manualInput.value = `${h}:${m}:${s}`;
updateDisplay(); // call your existing update function
}
// sync manualInput -> picker
function syncPickerToInput() {
const [h, m, s] = manualInput.value.split(":").map(v => v || "00");
hourSelect.value = h.padStart(2, "0");
minuteSelect.value = m.padStart(2, "0");
secondSelect.value = s.padStart(2, "0");
}
// show picker when manualInput is focused
manualInput.addEventListener("focus", () => {
pickerContainer.style.display = "flex";
syncPickerToInput();
manualMode = true;
manualToggle.textContent = "Manual Mode Active";
manualToggle.style.background = sheepColour;
manualToggle.style.color = "#000";
});
[hourSelect, minuteSelect, secondSelect].forEach(sel => {
sel.addEventListener("change", () => {
manualMode = true; // pause live updates
manualToggle.textContent = "Manual Mode Active";
manualToggle.style.background = sheepColour;
manualToggle.style.color = "#000";
updateManualInputFromPicker();
});
// Also trigger manual mode if user clicks the select
sel.addEventListener("mousedown", () => {
manualMode = true;
manualToggle.textContent = "Manual Mode Active";
manualToggle.style.background = sheepColour;
manualToggle.style.color = "#000";
});
});
// hide picker if click outside
document.addEventListener("click", (e) => {
if (!pickerContainer.contains(e.target) && e.target !== manualInput) {
pickerContainer.style.display = "none";
}
});
// update manualInput when pickers change
[hourSelect, minuteSelect, secondSelect].forEach(sel => {
sel.addEventListener("change", updateManualInputFromPicker);
});
// =====================
// Search Bar
// =====================
const searchInput = document.createElement("input");
searchInput.type = "text";
searchInput.placeholder = "Search timezone...";
searchInput.style.width = "100%";
searchInput.style.marginBottom = "6px";
panel.appendChild(searchInput);
const searchResults = document.createElement("div");
searchResults.style.maxHeight = "120px";
searchResults.style.overflowY = "auto";
searchResults.style.fontSize = "11px";
searchResults.style.marginBottom = "10px";
panel.appendChild(searchResults);
// =====================
// Output
// =====================
const output = document.createElement("div");
panel.appendChild(output);
// =====================
// Helpers
// =====================
function saveZones() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(activeZones));
}
function getTctDate() {
const now = new Date();
if (manualMode && manualInput.value) {
const [h, m, s] = manualInput.value.split(":");
const utc = new Date(Date.UTC(
now.getUTCFullYear(),
now.getUTCMonth(),
now.getUTCDate(),
h, m, s || 0
));
return utc;
}
return now;
}
function formatTime(date, zone) {
return date.toLocaleTimeString("en-GB", {
timeZone: zone,
hour12: false
});
}
function updateDisplay() {
const tct = getTctDate();
output.innerHTML = "";
// =====================
// Your Time (Local)
// =====================
const localZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const yourRow = document.createElement("div");
yourRow.style.color = "#ccc";
yourRow.style.fontSize = "12px";
yourRow.style.marginBottom = "4px";
yourRow.innerHTML = `
<span style="color:${sheepColour};">YourTime</span>
<span style="float:right;">${formatTime(tct, localZone)}</span>
`;
output.appendChild(yourRow);
// =====================
// Saved Timezones
// =====================
activeZones.forEach((tz, i) => {
const row = document.createElement("div");
row.style.color = "#ccc";
row.style.fontSize = "12px";
row.style.marginBottom = "4px";
row.innerHTML = `
<span style="color:${sheepColour};">${tz.label}</span>
<span style="float:right;">${formatTime(tct, tz.zone)}</span>
`;
row.style.cursor = "pointer";
row.title = "Click to remove this timezone";
row.addEventListener("click", () => {
activeZones.splice(i, 1);
saveZones();
updateDisplay();
});
output.appendChild(row);
});
}
function startUpdating() {
if (interval) return;
updateDisplay();
interval = setInterval(updateDisplay, 1000);
}
function stopUpdating() {
clearInterval(interval);
interval = null;
}
function addTimezone(zoneName) {
if (activeZones.find(z => z.zone === zoneName)) return;
activeZones.push({
label: zoneName.split("/").pop().replace("_", " "),
zone: zoneName
});
saveZones();
updateDisplay();
}
// =====================
// Events
// =====================
toggle.addEventListener("click", () => {
if (panel.style.display === "none") {
panel.style.display = "block";
startUpdating();
} else {
panel.style.display = "none";
stopUpdating();
}
});
manualToggle.addEventListener("click", () => {
manualMode = !manualMode;
if (manualMode) {
manualToggle.textContent = "Manual Mode Active";
manualToggle.style.background = sheepColour;
manualToggle.style.color = "#000";
} else {
manualToggle.textContent = "Live Mode";
manualToggle.style.background = "";
manualToggle.style.color = "";
}
updateDisplay();
});
manualInput.addEventListener("input", () => {
if (manualMode) updateDisplay();
});
searchInput.addEventListener("input", () => {
const value = searchInput.value.toLowerCase();
searchResults.innerHTML = "";
if (!value) return;
const matches = allTimezones
.filter(z => z.toLowerCase().includes(value))
.slice(0, 15);
matches.forEach(zone => {
const item = document.createElement("div");
item.textContent = zone;
item.style.cursor = "pointer";
item.style.padding = "2px 0";
item.style.color = "#aaa";
item.addEventListener("click", () => {
addTimezone(zone);
searchInput.value = "";
searchResults.innerHTML = "";
});
searchResults.appendChild(item);
});
});
// =====================
// Reset Button
// =====================
const resetBtn = document.createElement("button");
resetBtn.textContent = "Reset Zones";
resetBtn.style.width = "100%";
resetBtn.style.marginBottom = "10px";
resetBtn.style.cursor = "pointer";
resetBtn.style.background = "#555";
resetBtn.style.color = "white";
panel.appendChild(resetBtn);
resetBtn.addEventListener("click", () => {
if (!confirm("Are you sure you want to reset all saved zones?")) return;
localStorage.removeItem(STORAGE_KEY);
activeZones = [...DEFAULT_ZONES];
updateDisplay();
});
}
// ================================
// 🐑 Sheepie OC Helper w/ Cache
// ================================
function initOCTracker(){
'use strict';
if (window.SHEEPIE_OCHelper) return;
window.SHEEPIE_OCHelper = true;
const API_URL = "https://tornprobability.com:3000/api/GetRoleWeights";
const CACHE_KEY = "oc_role_weights_cache";
const CACHE_TIME_KEY = "oc_role_weights_cache_time";
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
let roleWeightsAPI = null;
/* -----------------------------
STYLE (NO ANIMATION)
------------------------------*/
const style = document.createElement('style');
style.textContent = `
.oc-solid-red {
outline: 4px solid red !important;
outline-offset: 0px !important;
}
.oc-has-contrib {
position: relative !important;
padding-bottom: 22px !important;
}
.oc-contrib-linewrap {
position: absolute !important;
left: 8px !important;
right: 8px !important;
bottom: 6px !important;
display: flex !important;
align-items: center !important;
gap: 8px !important;
pointer-events: none !important;
}
.oc-contrib-linewrap::before,
.oc-contrib-linewrap::after {
content: "";
flex: 1;
height: 1px;
background: rgba(115,65,128,0.95);
}
.oc-contrib-linetext {
font-weight: 900;
font-size: 12px;
color: 'sheepColour';
}`;
document.head.appendChild(style);
/* -----------------------------
YOUR ORIGINAL OC DATA
(UNCHANGED)
------------------------------*/
const defaultLevel6 = 67;
const defaultLevel5 = 65;
const defaultLevel4 = 65;
const defaultLevel3 = 65;
const defaultLevel2 = 65;
const defaultDecline = 700;
const ocRoles = [
{
OCName: "Blast From The Past",
Positions: {
"MUSCLE": 70,
"ENGINEER": 70,
"BOMBER": 69,
"PICKLOCK #1": 69,
"HACKER": 69,
"PICKLOCK #2": 60
}
},{
OCName: "Stacking the Deck",
Positions: {
"HACKER": 60,
"IMITATOR": 64,
"CAT BURGLAR": 60,
"DRIVER": 50
}
},
{
OCName: "Ace in the Hole",
Positions: {
"HACKER": 65,
"DRIVER": 55,
"MUSCLE #1": 62,
"IMITATOR": 65,
"MUSCLE #2": 63
}
},
{
OCName: "Break the Bank",
Positions: {
"MUSCLE #1": 64,
"MUSCLE #2": 60,
"MUSCLE #3": 64,
"THIEF #1": 60,
"THIEF #2": 67,
"ROBBER": 64
}
},
{
OCName: "Bidding War",
Positions: {
"ROBBER #1": 60,
"ROBBER #2": 69,
"ROBBER #3": 70,
"BOMBER #1": 60,
"BOMBER #2": 67,
"DRIVER": 63
}
},
{
OCName: "Clinical Precision",
Positions: {
"IMITATOR": 68,
"CLEANER": 67,
"CAT BURGLAR": 67,
"ASSASSIN": 67
}
},
{
OCName: "Honey Trap",
Positions: {
"MUSCLE #1": 67,
"MUSCLE #2": 67,
"ENFORCER": 67}
},
{
OCName: "Sneaky Git Grab",
Positions: {
"PICKPOCKET": 70,
"IMITATOR": 65,
"TECHIE": 65,
"HACKER": 66}
},
{
OCName: "Leave No Trace",
Positions: {
"IMITATOR": 66,
"NEGOTIATOR": 65,
"TECHIE": 65
}
},
{
OCName: "Counter Offer",
Positions: {
"ROBBER": 66,
"ENGINEER": 65,
"PICKLOCK": 65,
"HACKER": 65,
"LOOTER": 65}
},
{
OCName: "Stage Fright",
Positions: {
"SNIPER": 70,
"MUSCLE #1": 67,
"ENFORCER": 67,
"MUSCLE #3": 67,
"LOOKOUT": 62,
"MUSCLE #2": 1
}
}, {
OCName: "Snow Blind",
Positions: {
"HUSTLER": 70,
"IMITATOR": 67,
"MUSCLE #1": 60,
"MUSCLE #2": 60
}
},
{
OCName: "Gaslight The Way",
Positions: {
"IMITATOR #1": 64,
"IMITATOR #2": 66,
"IMITATOR #3": 70,
"LOOTER #1": 60,
"LOOTER #2": 50,
"LOOTER #3": 66}
},
{
OCName: "Pet Project",
Positions: `default_${defaultLevel2}`
},
{
OCName: "Cash Me If You Can",
Positions: `default_${defaultLevel2}`
},
{
OCName: "Smoke and Wing Mirrors",
Positions: {
"CAR THIEF": 70,
"IMITATOR": 65,
"HUSTLER #2": 65,
"HUSTLER #1": 62
}
},
{
OCName: "Best of the Lot",
Positions: `default_${defaultLevel2}`
},
{
OCName: "Market Forces",
Positions: {
"ENFORCER": 67,
"NEGOTIATOR": 67,
"MUSCLE": 65,
"LOOKOUT": 65,
"ARSONIST": 50
}
},
{
OCName: "Guardian Ángels",
Positions: {
"HUSTLER": 67,
"ENGINEER": 65,
"ENFORCER": 65
}
},
{
OCName: "No Reserve",
Positions: {
"TECHIE": 68,
"ENGINEER": 68,
"CAR THIEF": 68
}
},
{
OCName: "Manifest Cruelty",
Positions: `default_${defaultDecline}`
}
];
/* -----------------------------
PROCESS PANEL ONCE
------------------------------*/
function processScenario(panel) {
if (panel.dataset.ocProcessed === "true") return;
panel.dataset.ocProcessed = "true";
const ocName = panel.querySelector('.panelTitle___aoGuV')?.innerText.trim();
if (!ocName) return;
const slots = panel.querySelectorAll('.wrapper___Lpz_D');
slots.forEach(slot => {
if (slot.dataset.ocProcessed === "true") return;
slot.dataset.ocProcessed = "true";
const roleElem = slot.querySelector('.title___UqFNy');
const chanceElem = slot.querySelector('.successChance___ddHsR');
if (!roleElem || !chanceElem) return;
const rawRole = roleElem.innerText.trim();
const successChance = parseInt(chanceElem.textContent.trim(), 10) || 0;
const joinBtn = slot.querySelector("button[class^='torn-btn joinButton']");
/* -----------------------------
DISPLAY ROLE WEIGHT (FROM CACHE)
------------------------------*/
if (roleWeightsAPI) {
const cleanOCName = ocName.split("\n")[0].trim();
const ocKey = cleanOCName.toLowerCase().replace(/[^a-z0-9\u00C0-\u017F]/gi, "");
const roleKey = rawRole.toLowerCase().replace(/[^a-z0-9\u00C0-\u017F]/gi, "");
let pct;
if (roleWeightsAPI) {
const cleanOCName = ocName.split("\n")[0].trim();
const ocKeyCompact = cleanOCName
.toLowerCase()
.replace(/[^a-z0-9\u00C0-\u017F]/gi, "");
const roleKeyCompact = rawRole
.toLowerCase()
.replace(/[^a-z0-9\u00C0-\u017F]/gi, "");
// find matching OC
const ocEntry = Object.entries(roleWeightsAPI).find(([k]) =>
k.toLowerCase() === ocKeyCompact
);
if (ocEntry) {
const roleEntry = Object.entries(ocEntry[1] || {}).find(([k]) =>
k.toLowerCase() === roleKeyCompact
);
if (roleEntry && typeof roleEntry[1] === "number") {
pct = Math.round(roleEntry[1]);
}
}
if (typeof pct === "number") {
slot.classList.add("oc-has-contrib");
if (!slot.querySelector(".oc-contrib-linewrap")) {
const wrap = document.createElement("div");
wrap.className = "oc-contrib-linewrap";
const text = document.createElement("span");
text.className = "oc-contrib-linetext";
text.textContent = pct + "%";
wrap.appendChild(text);
slot.appendChild(wrap);
}
}
}
if (typeof pctRaw === "number") {
slot.classList.add("oc-has-contrib");
if (!slot.querySelector(".oc-contrib-linewrap")) {
const wrap = document.createElement("div");
wrap.className = "oc-contrib-linewrap";
const text = document.createElement("span");
text.className = "oc-contrib-linetext";
text.textContent = Math.round(pctRaw) + "%";
wrap.appendChild(text);
slot.appendChild(wrap);
}
}
}
/* -----------------------------
YOUR ORIGINAL MIN/MAX LOGIC
(UNCHANGED)
------------------------------*/
const ocData = ocRoles.find(o => o.OCName.toLowerCase() === ocName.toLowerCase());
if (!ocData) return;
let required = null;
if (typeof ocData.Positions === 'string' && ocData.Positions.startsWith('default_')) {
required = parseInt(ocData.Positions.split('_')[1], 10);
} else if (typeof ocData.Positions === 'object') {
required = ocData.Positions[rawRole];
}
if (required === undefined || required === null) return;
let maxAllowed = required + 15;
if (ocName.toLowerCase() === "stage fright" && rawRole === "MUSCLE #2") {
maxAllowed = 70;
}
const honorTexts = slot.querySelectorAll('.honor-text');
const userName = honorTexts.length > 1 ? honorTexts[1].textContent.trim() : null;
if (!userName) {
if (successChance < required) {
slot.style.backgroundColor = '#ff000061';
} else if (successChance > maxAllowed) {
slot.style.backgroundColor = '#F7b500';
} else {
slot.style.backgroundColor = '#21a61c61';
}
if (joinBtn) {
if (successChance < required || successChance > maxAllowed) {
joinBtn.setAttribute('disabled', '');
} else {
joinBtn.removeAttribute('disabled');
}
}
} else if (successChance < required) {
slot.classList.add('oc-solid-red');
}
});
}
/* -----------------------------
OBSERVER (LIGHTWEIGHT)
------------------------------*/
let lastRun = 0;
const MIN_INTERVAL = 1000; // adjust (150–300ms ideal)
const observer = new MutationObserver(() => {
const now = Date.now();
if (now - lastRun < MIN_INTERVAL) {
return; // too soon, skip
}
lastRun = now;
document.querySelectorAll('.wrapper___U2Ap7')
.forEach(processScenario);
});
observer.observe(document.body, { childList: true, subtree: true });
/* -----------------------------
24HR CACHE SYSTEM
------------------------------*/
function loadWeights() {
const cached = localStorage.getItem(CACHE_KEY);
const cacheTime = localStorage.getItem(CACHE_TIME_KEY);
if (cached && cacheTime && (Date.now() - cacheTime < CACHE_DURATION)) {
roleWeightsAPI = JSON.parse(cached);
return;
}
GM_xmlhttpRequest({
method: "GET",
url: API_URL,
onload: (res) => {
try {
roleWeightsAPI = JSON.parse(res.responseText);
localStorage.setItem(CACHE_KEY, JSON.stringify(roleWeightsAPI));
localStorage.setItem(CACHE_TIME_KEY, Date.now());
;
} catch (e) {
console.warn("Failed parsing weights", e);
}
}
});
}
loadWeights();
}
// ================================
// 🐑 Sheepies Crime Morale original by tobytorn [1617955]
// ================================
function initCrimeMorale(){
'use strict';
if (window.SHEEPIE_Crime) return;
window.SHEEPIE_Crime = true;
// Avoid duplicate injection in TornPDA
if (window.CRIME_MORALE_INJECTED) {
return;
}
window.CRIME_MORALE_INJECTED = true;
const LOCAL_STORAGE_PREFIX = 'CRIME_MORALE_';
const STORAGE_MORALE = 'morale';
const STYLE_ELEMENT_ID = 'CRIME-MORALE-STYLE';
function getLocalStorage(key, defaultValue) {
const value = window.localStorage.getItem(LOCAL_STORAGE_PREFIX + key);
try {
return JSON.parse(value) ?? defaultValue;
} catch (err) {
return defaultValue;
}
}
function setLocalStorage(key, value) {
window.localStorage.setItem(LOCAL_STORAGE_PREFIX + key, JSON.stringify(value));
}
const isPda = window.GM_info?.scriptHandler?.toLowerCase().includes('tornpda');
const [getValue, setValue] =
isPda || typeof window.GM_getValue !== 'function' || typeof window.GM_setValue !== 'function'
? [getLocalStorage, setLocalStorage]
: [window.GM_getValue, window.GM_setValue];
function addStyle(css) {
const style =
document.getElementById(STYLE_ELEMENT_ID) ??
(function () {
const style = document.createElement('style');
style.id = STYLE_ELEMENT_ID;
document.head.appendChild(style);
return style;
})();
style.appendChild(document.createTextNode(css));
}
function formatLifetime(seconds) {
const hours = Math.floor(seconds / 3600);
const text =
hours >= 72
? `${Math.floor(hours / 24)}d`
: hours > 0
? `${hours}h`
: seconds >= 0
? `${Math.floor(seconds / 60)}m`
: '';
const color = hours >= 24 ? 't-gray-c' : hours >= 12 ? 't-yellow' : hours >= 0 ? 't-red' : '';
return { seconds, hours, text, color };
}
async function checkDemoralization(data) {
const demMod = (data.DB || {}).demMod;
if (typeof demMod !== 'number') {
return;
}
const morale = 100 - demMod;
updateMorale(morale);
await setValue(STORAGE_MORALE, morale);
}
class BurglaryObserver {
constructor() {
this.data = getValue('burglary', {});
this.data.favorite = this.data.favorite ?? [];
this.properties = null;
this.crimeOptions = null;
this.observer = new MutationObserver((mutations) => {
const isAdd = mutations.some((mutation) => {
for (const added of mutation.addedNodes) {
if (added instanceof HTMLElement) {
return true;
}
}
return false;
});
if (!isAdd) {
return;
}
for (const element of this.crimeOptions) {
if (!element.classList.contains('cm-bg-seen')) {
element.classList.add('cm-bg-seen');
this._refreshCrimeOption(element);
}
}
});
}
start() {
if (this.crimeOptions) {
return;
}
this.crimeOptions = document.body.getElementsByClassName('crime-option');
this.observer.observe($('.burglary-root')[0], { subtree: true, childList: true });
}
stop() {
this.crimeOptions = null;
this.observer.disconnect();
}
onNewData(data) {
this.start();
this.properties = data.DB?.crimesByType?.properties;
this._refreshCrimeOptions();
}
_refreshCrimeOptions() {
for (const element of this.crimeOptions) {
this._refreshCrimeOption(element);
}
}
_refreshCrimeOption(element) {
if (!this.properties) {
return;
}
const $element = $(element);
const $title = $element.find('[class*=crimeOptionSection___]').first();
$title.find('.cm-bg-lifetime').remove();
const guessedProperty = this._guessCrimeOptionData($element);
const property = this._checkCrimeOptionData($element, guessedProperty);
if (!property) {
$element.removeAttr('data-cm-id');
return;
}
$element.attr('data-cm-id', property.subID);
const now = Math.floor(Date.now() / 1000);
const lifetime = formatLifetime(property.expire - now);
if (lifetime.hours >= 0) {
$title.css('position', 'relative');
$title.append(`<div class="cm-bg-lifetime ${lifetime.color}">${lifetime.text}</div>`);
}
$element.find('.cm-bg-favor').remove();
const $favor = $('<div class="cm-bg-favor"></div>');
$favor.toggleClass('cm-bg-active', this.data.favorite.includes(property.title));
$element.find('.crime-image').append($favor);
$favor.on('click', () => {
this._toggleFavorite(property.title);
this._refreshCrimeOptions();
});
}
_guessCrimeOptionData($crimeOption) {
const savedId = $crimeOption.attr('data-cm-id');
if (savedId) {
return this.properties.find((x) => x.subID === savedId);
}
const $item = $crimeOption.closest('.virtual-item');
if ($item.prev().hasClass('lastOfGroup___YNUeQ')) {
return this.properties[0];
}
let prevId = undefined;
$item.prevAll().each(function () {
prevId = $(this).find('.crime-option[data-cm-id]').attr('data-cm-id');
if (prevId) {
return false; // break the loop
}
});
const prevIndex = this.properties.findIndex((x) => prevId && x.subID === prevId);
if (prevIndex >= 0) {
// Since we always scan crime options in document order,
// $prevItemWithId and $item should correspond to adjacent data entries.
return this.properties[prevIndex + 1];
}
if ($item.index() === 0) {
const $nextOptionWithId = $item.nextAll().find('.crime-option[data-cm-id]').first();
const nextId = $nextOptionWithId.attr('data-cm-id');
const nextIndex = this.properties.findIndex((x) => x.subID && x.subID === nextId);
const nextPos = $nextOptionWithId.closest('.virtual-item').index();
if (nextIndex >= 0 && nextPos >= 0) {
return this.properties[nextIndex - nextPos];
}
}
return undefined;
}
_checkCrimeOptionData($crimeOption, property) {
if (property === undefined) {
return undefined;
}
const { title, titleType } = this._getCrimeOptionTitle($crimeOption);
return titleType && property[titleType] === title ? property : undefined;
}
_getCrimeOptionTitle($crimeOption) {
const mobileTitle = $crimeOption.find('.title___kOWyb').text();
if (mobileTitle !== '') {
return { title: mobileTitle, titleType: 'mobileTitle' };
}
const textNode = $crimeOption.find('.crimeOptionSection___hslpu')[0]?.firstChild;
if (textNode?.nodeType === Node.TEXT_NODE) {
return { title: textNode.textContent, titleType: 'title' };
}
return { title: null, titleType: null };
}
_toggleFavorite(title) {
const index = this.data.favorite.indexOf(title);
if (index >= 0) {
this.data.favorite.splice(index, 1);
} else {
this.data.favorite.push(title);
}
setValue('burglary', this.data);
}
}
const burglaryObserver = new BurglaryObserver();
async function checkBurglary(crimeType, data) {
if (crimeType !== '7') {
burglaryObserver.stop();
return;
}
burglaryObserver.onNewData(data);
}
const PP_CYCLING = 0;
const PP_DISTRACTED = 34; // eslint-disable-line no-unused-vars
const PP_MUSIC = 102;
const PP_LOITERING = 136;
const PP_PHONE = 170;
const PP_RUNNING = 204;
const PP_SOLICITING = 238; // eslint-disable-line no-unused-vars
const PP_STUMBLING = 272;
const PP_WALKING = 306;
const PP_BEGGING = 340;
const PP_SKINNY = 'Skinny';
const PP_AVERAGE = 'Average';
const PP_ATHLETIC = 'Athletic';
const PP_MUSCULAR = 'Muscular';
const PP_HEAVYSET = 'Heavyset';
const PP_ANY_BUILD = [PP_SKINNY, PP_AVERAGE, PP_ATHLETIC, PP_MUSCULAR, PP_HEAVYSET];
const categoryColorMap = {
"Safe": "#37b24d",
"Moderately Unsafe": "#74b816",
"Unsafe": "#f59f00",
"Risky": "#f76707",
"Dangerous": "#f03e3e",
"Very Dangerous": "#7048e8",
};
var sideColorMap = {
"Safe": "#37b24d",
"Moderately Unsafe": "#74b816",
"Unsafe": "#f59f00",
"Risky": "#f76707",
"Dangerous": "#f03e3e",
"Very Dangerous": "#7048e8",
}
const tier1 = {
"Safe": "#37b24d",
"Moderately Unsafe": "#f76707",
"Unsafe": "#f03e3e",
"Risky": "#f03e3e",
"Dangerous": "#f03e3e",
"Very Dangerous": "#7048e8",
}
const tier2 = {
"Safe": "#37b24d",
"Moderately Unsafe": "#37b24d",
"Unsafe": "#f76707",
"Risky": "#f03e3e",
"Dangerous": "#f03e3e",
"Very Dangerous": "#7048e8",
}
const tier3 = {
"Safe": "#37b24d",
"Moderately Unsafe": "#37b24d",
"Unsafe": "#37b24d",
"Risky": "#f76707",
"Dangerous": "#f03e3e",
"Very Dangerous": "#7048e8",
}
const tier4 = {
"Safe": "#37b24d",
"Moderately Unsafe": "#37b24d",
"Unsafe": "#37b24d",
"Risky": "#37b24d",
"Dangerous": "#f76707",
"Very Dangerous": "#7048e8",
}
const tier5 = {
"Safe": "#37b24d",
"Moderately Unsafe": "#37b24d",
"Unsafe": "#37b24d",
"Risky": "#37b24d",
"Dangerous": "#37b24d",
"Very Dangerous": "#7048e8",
}
const markGroups = {
"Safe": ["Drunk man", "Drunk woman", "Homeless person", "Junkie", "Elderly man", "Elderly woman"],
"Moderately Unsafe": ["Classy lady", "Laborer", "Postal worker", "Young man", "Young woman", "Student"],
"Unsafe": ["Rich kid", "Sex worker", "Thug"],
"Risky": ["Jogger", "Businessman", "Businesswoman", "Gang member", "Mobster"],
"Dangerous": ["Cyclist"],
"Very Dangerous": ["Police officer"],
};
const PP_MARKS = {
'Drunk Man': { level: 1, status: [PP_STUMBLING], build: PP_ANY_BUILD },
'Drunk Woman': { level: 1, status: [PP_STUMBLING], build: PP_ANY_BUILD },
'Homeless Person': { level: 1, status: [PP_BEGGING], build: [PP_AVERAGE] },
Junkie: { level: 1, status: [PP_STUMBLING], build: PP_ANY_BUILD },
'Elderly Man': { level: 1, status: [PP_WALKING], build: [PP_SKINNY, PP_AVERAGE, PP_ATHLETIC, PP_HEAVYSET] },
'Elderly Woman': { level: 1, status: [PP_WALKING], build: [PP_SKINNY, PP_AVERAGE, PP_ATHLETIC, PP_HEAVYSET] },
'Young Man': { level: 2, status: [PP_MUSIC], build: [PP_SKINNY, PP_AVERAGE, PP_ATHLETIC] },
'Young Woman': { level: 2, status: [PP_PHONE], build: [PP_SKINNY, PP_AVERAGE, PP_HEAVYSET] },
Student: { level: 2, status: [PP_PHONE], build: [PP_SKINNY, PP_AVERAGE] },
'Classy Lady': {
level: 2,
status: [PP_PHONE, PP_WALKING],
build: [PP_SKINNY, PP_HEAVYSET],
bestBuild: [PP_HEAVYSET],
},
Laborer: { level: 2, status: [PP_PHONE], build: PP_ANY_BUILD },
'Postal Worker': { level: 2, status: [PP_WALKING], build: [PP_AVERAGE] },
'Rich Kid': {
level: 3,
status: [PP_WALKING, PP_PHONE],
build: [PP_SKINNY, PP_ATHLETIC, PP_HEAVYSET],
bestBuild: [PP_ATHLETIC],
},
'Sex Worker': { level: 3, status: [PP_PHONE], build: [PP_SKINNY, PP_AVERAGE], bestBuild: [PP_AVERAGE] },
Thug: { level: 3, status: [PP_RUNNING], build: [PP_SKINNY, PP_AVERAGE, PP_ATHLETIC], bestBuild: [PP_SKINNY] },
Businessman: {
level: 4,
status: [PP_PHONE],
build: [PP_AVERAGE, PP_MUSCULAR, PP_HEAVYSET],
bestBuild: [PP_MUSCULAR, PP_HEAVYSET],
},
Businesswoman: {
level: 4,
status: [PP_PHONE],
build: [PP_SKINNY, PP_AVERAGE, PP_ATHLETIC],
bestBuild: [PP_ATHLETIC],
},
'Gang Member': {
level: 4,
status: [PP_LOITERING],
build: [PP_AVERAGE, PP_ATHLETIC, PP_MUSCULAR],
bestBuild: [PP_AVERAGE],
},
Jogger: { level: 4, status: [PP_WALKING], build: [PP_ATHLETIC, PP_MUSCULAR], bestBuild: [PP_MUSCULAR] },
Mobster: { level: 4, status: [PP_WALKING], build: [PP_SKINNY] },
Cyclist: { level: 5, status: [PP_CYCLING], build: ['1.52 m', `5'0"`, '1.62 m', `5'4"`] },
'Police Officer': {
level: 6,
status: [PP_RUNNING],
build: PP_ANY_BUILD,
bestBuild: [PP_SKINNY, '1.52 m', `5'0"`, '1.62 m', `5'4"`],
},
};
let pickpocketingOb = null;
let pickpocketingExitOb = null;
let pickpocketingInterval = 0;
async function checkPickpocketing(crimeType) {
if (crimeType !== '5') {
stopPickpocketing();
return;
}
const $wrapper = $('.pickpocketing-root');
if ($wrapper.length === 0) {
if (pickpocketingInterval === 0) {
// This is the first fetch.
pickpocketingInterval = setInterval(() => {
const $wrapperInInterval = $('.pickpocketing-root');
if ($wrapperInInterval.length === 0) {
return;
}
clearInterval(pickpocketingInterval);
pickpocketingInterval = 0;
startPickpocketing($wrapperInInterval);
}, 1000);
}
} else {
startPickpocketing($wrapper);
}
}
function refreshPickpocketing() {
const $wrapper = $('.pickpocketing-root');
const now = Date.now();
// Releasing reference to removed elements to avoid memory leak
pickpocketingExitOb.disconnect();
let isBelowExiting = false;
$wrapper.find('.crime-option').each(function () {
const $this = $(this);
const top = Math.floor($this.position().top);
const oldTop = parseInt($this.attr('data-cm-top'));
if (top !== oldTop) {
$this.attr('data-cm-top', top.toString());
$this.attr('data-cm-timestamp', now.toString());
}
const timestamp = parseInt($this.attr('data-cm-timestamp')) || now;
const isLocked = $this.is('[class*=locked___]');
const isExiting = $this.is('[class*=exitActive___]');
const isRecentlyMoved = now - timestamp <= 1000;
// ===== APPLY CATEGORY COLORING =====
const markText = $this.find('[class*=titleAndProps___] > *:first-child').text().trim();
const buildText = $this.find('[class*=physicalProps___]').text().trim();
const combinedText = (markText + ' ' + buildText).toLowerCase();
let category = null;
for (const cat in markGroups) {
if (markGroups[cat].some(group => combinedText.includes(group.toLowerCase()))) {
category = cat;
break;
}
}
if (category) {
$this.find('[class*=titleAndProps___] > *:first-child')
.css('color', categoryColorMap[category]);
const skillEl = $('.crimes-app-header .value___FdkAT.copyTrigger___fsdzI').first();
const skill = parseInt(skillEl.text() || "0");
if (skill < 10) sideColorMap = tier1;
else if (skill < 35) sideColorMap = tier2;
else if (skill < 65) sideColorMap = tier3;
else if (skill < 80) sideColorMap = tier4;
else sideColorMap = tier5;
$this.css('border-left', `3px solid ${sideColorMap[category]}`);
}
$this
.find('[class*=commitButtonSection___]')
.toggleClass('cm-overlay', !isLocked && (isBelowExiting || isRecentlyMoved))
.toggleClass('cm-overlay-fade', !isLocked && !isBelowExiting && isRecentlyMoved);
isBelowExiting = isBelowExiting || isExiting;
if (!$this.is('[class*=cm-pp-level-]')) {
const markAndTime = $this.find('[class*=titleAndProps___] > *:first-child').text().trim().toLowerCase();
const iconPosStr = $this.find('[class*=timerCircle___] [class*=icon___]').css('background-position-y');
const iconPosMatch = iconPosStr?.match(/(-?\d+)px/);
const iconPos = -parseInt(iconPosMatch?.[1] ?? '');
const build = $this.find('[class*=physicalProps___]').text().trim().toLowerCase();
for (const [mark, markInfo] of Object.entries(PP_MARKS)) {
if (markAndTime.startsWith(mark.toLowerCase())) {
if (markInfo.status.includes(iconPos) && markInfo.build.some((b) => build.includes(b.toLowerCase()))) {
$this.addClass(`cm-pp-level-${markInfo.level}`);
if (markInfo.bestBuild?.some((b) => build.includes(b.toLowerCase()))) {
$this.addClass(`cm-pp-best-build`);
}
}
break;
}
}
}
pickpocketingExitOb.observe(this, { attributes: true, attributeFilter: ['class'], attributeOldValue: true });
});
}
function startPickpocketing($wrapper) {
if (!pickpocketingOb) {
pickpocketingOb = new MutationObserver(refreshPickpocketing);
pickpocketingExitOb = new MutationObserver(function (mutations) {
for (const mutation of mutations) {
if (
mutation.oldValue.indexOf('exitActive___') < 0 &&
mutation.target.className.indexOf('exitActive___') >= 0
) {
refreshPickpocketing();
return;
}
}
});
}
pickpocketingOb.observe($wrapper[0], {
childList: true,
characterData: true,
subtree: true,
});
}
function stopPickpocketing() {
if (!pickpocketingOb) {
return;
}
pickpocketingOb.disconnect();
pickpocketingOb = null;
pickpocketingExitOb.disconnect();
pickpocketingExitOb = null;
}
// Maximize extra exp (capitalization exp - total cost)
class ScammingSolver {
get BASE_ACTION_COST() {
return this.algo === 'meritGrift' ? 0.001 : 0.02;
}
get FAILURE_COST_MAP() {
return this.algo === 'merit' || this.algo === 'meritGrift'
? {
1: 0,
20: 0,
40: 0,
60: 0,
80: 0,
}
: {
1: 1,
20: 1,
40: 1,
60: 0.5,
80: 0.33,
};
}
get CONCERN_SUCCESS_RATE_MAP() {
return {
'young adult': 0.55,
'middle-aged': 0.5,
senior: 0.45,
professional: 0.4,
affluent: 0.35,
'': 0.5,
};
}
get CELL_VALUE_MAP() {
return this.algo === 'merit'
? {
low: 2,
medium: 2,
high: 2,
fail: -20,
}
: this.algo === 'meritGrift'
? {
low: 0,
medium: 1,
high: 1,
fail: 0,
}
: {
low: 0.5,
medium: 1.5,
high: 2.5,
fail: -20, // The penalty should be -10. I add a bit to it for demoralization and chain bonus lost.
};
}
get SAFE_CELL_SET() {
return new Set(['neutral', 'low', 'medium', 'high', 'temptation']);
}
get DISPLACEMENT() {
// prettier-ignore
return {
1: {
strong: [[10, 19], [15, 29], [18, 35], [21, 39], [22, 42], [23, 44]],
soft: [[3, 7], [5, 11], [6, 13], [6, 14], [7, 15], [7, 16]],
back: [[-4, -2], [-6, -3], [-7, -4], [-8, -4], [-9, -4], [-9, -5]],
},
20: {
strong: [[8, 15], [12, 23], [15, 28], [16, 31], [18, 33], [18, 35]],
soft: [[3, 7], [5, 11], [6, 13], [6, 14], [7, 15], [7, 16]],
back: [[-4, -2], [-6, -3], [-7, -4], [-8, -4], [-9, -4], [-9, -5]],
},
40: {
strong: [[7, 13], [11, 20], [13, 24], [14, 27], [15, 29], [16, 30]],
soft: [[3, 6], [5, 9], [6, 11], [6, 12], [7, 13], [7, 14]],
back: [[-4, -2], [-6, -3], [-7, -4], [-8, -4], [-9, -4], [-9, -5]],
},
60: {
strong: [[6, 11], [9, 17], [11, 20], [12, 23], [13, 24], [14, 25]],
soft: [[2, 4], [3, 6], [4, 7], [4, 8], [4, 9], [5, 9]],
back: [[-4, -2], [-6, -3], [-7, -4], [-8, -4], [-9, -4], [-9, -5]],
},
80: {
strong: [[5, 9], [8, 14], [9, 17], [10, 19], [11, 20], [12, 21]],
soft: [[2, 3], [3, 5], [4, 6], [4, 6], [4, 7], [5, 7]],
back: [[-3, -2], [-5, -3], [-6, -4], [-6, -4], [-7, -4], [-7, -5]],
},
};
}
get MERIT_MASK_MAP() {
return {
temptation: 1n << 50n,
sensitivity: 1n << 51n,
hesitation: 1n << 52n,
concern: 1n << 53n,
};
}
get MERIT_REQUIREMENT_MASK() {
return 0xfn << 50n;
}
/**
* @param {'exp' | 'merit' | 'meritGrift'} algo
* @param {('neutral' | 'low' | 'medium' | 'high' | 'temptation' | 'sensitivity' | 'hesitation' | 'concern' | 'fail')[]} bar
* @param {1 | 20 | 40 | 60 | 80} targetLevel
* @param {number} round
* @param {number} suspicion
* @param {'young adult' | 'middle-aged' | 'senior' | 'professional' | 'affluent' | ''} mark
*/
constructor(algo, bar, targetLevel, round, suspicion, mark) {
this.algo = algo;
this.bar = bar;
this.targetLevel = targetLevel;
this.failureCost = this.FAILURE_COST_MAP[this.targetLevel];
this.initialRound = round;
this.initialSuspicion = suspicion;
this.mark = mark;
this.driftArrayMap = new Map(); // (resolvingBitmap) => number[50]
this.dp = new Map(); // (resolvingBitmap | round) => {value: number, action: string, multi: number}[50]
this.resolvingMasks = new Array(50);
for (let pip = 0; pip < 50; pip++) {
if (this.resolvingMasks[pip]) {
continue;
}
if (this.bar[pip] !== 'hesitation' && this.bar[pip] !== 'concern') {
this.resolvingMasks[pip] = 0n;
continue;
}
let mask = this.algo === 'merit' ? this.MERIT_MASK_MAP[this.bar[pip]] : 0n;
for (let endPip = pip; endPip < 50 && this.bar[endPip] === this.bar[pip]; endPip++) {
mask += 1n << BigInt(endPip);
}
for (let endPip = pip; endPip < 50 && this.bar[endPip] === this.bar[pip]; endPip++) {
this.resolvingMasks[endPip] = mask;
}
}
}
/**
* @param {number} driftBitmap 1 for temptation triggered, 2 for sensitivity triggered
*/
solve(round, pip, resolvingBitmap, multiplierUsed, driftBitmap) {
if (this.algo === 'merit') {
for (let pip = 0; pip < 50; pip++) {
if (this._isResolved(pip, resolvingBitmap)) {
resolvingBitmap |= this.MERIT_MASK_MAP[this.bar[pip]] ?? 0n;
}
}
resolvingBitmap |= BigInt(driftBitmap) << 50n;
}
const result = this._visit(round - multiplierUsed, resolvingBitmap, multiplierUsed, pip);
return result[pip];
}
/**
* @param {number} round
* @param {bigint} resolvingBitmap
* @param {number} minMulti
* @param {number | undefined} singlePip
*/
_visit(round, resolvingBitmap, minMulti, singlePip = undefined) {
const dpKey = BigInt(round) | (resolvingBitmap << 6n);
// Cached solutions do not respect `minMulti`.
if (minMulti === 0) {
const visited = this.dp.get(dpKey);
if (visited) {
return visited;
}
}
const result = new Array(50);
this.dp.set(dpKey, result);
if (this._estimateSuspicion(round) >= 50) {
for (let pip = 0; pip < 50; pip++) {
result[pip] = this._getCellResult(pip, resolvingBitmap);
}
return result;
}
const driftArray = this._getDriftArray(resolvingBitmap);
const [pipBegin, pipEnd] = singlePip !== undefined ? [singlePip, singlePip + 1] : [0, 50];
for (let pip = pipBegin; pip < pipEnd; pip++) {
const best = this._getCellResult(pip, resolvingBitmap);
if (this.bar[pip] === 'fail') {
result[pip] = best;
continue;
}
if (!this._isResolved(pip, resolvingBitmap)) {
if (this.bar[pip] === 'hesitation') {
const resolvedResult = this._visit(round, resolvingBitmap | this.resolvingMasks[pip], 0);
result[pip] = resolvedResult[pip];
continue;
}
if (this.bar[pip] === 'concern') {
const resolvedResult = this._visit(round + 1, resolvingBitmap | this.resolvingMasks[pip], 0);
const unresolvedResult = this._visit(round + 1, resolvingBitmap, 0);
const concernSuccessRate = this.CONCERN_SUCCESS_RATE_MAP[this.mark] ?? this.CONCERN_SUCCESS_RATE_MAP[''];
const value =
resolvedResult[pip].value * concernSuccessRate +
(unresolvedResult[pip].value - this.failureCost) * (1 - concernSuccessRate) -
this.BASE_ACTION_COST;
result[pip] = {
value: Math.max(0, value),
action: value > 0 ? 'resolve' : 'abandon',
multi: 0,
};
continue;
}
}
for (let multi = minMulti; multi <= 5; multi++) {
const suspicionAfterMulti = this._estimateSuspicion(round + multi);
const nextRoundResult = this._visit(round + multi + 1, resolvingBitmap, 0);
const feasibleActions = pip > 0 ? ['strong', 'soft', 'back'] : ['strong', 'soft'];
for (const action of feasibleActions) {
const displacementArray = this.DISPLACEMENT[this.targetLevel.toString()]?.[action]?.[multi];
if (!displacementArray) {
continue;
}
const [minDisplacement, maxDisplacement] = displacementArray;
let totalValue = 0;
for (let disp = minDisplacement; disp <= maxDisplacement; disp++) {
const landingPip = Math.max(Math.min(pip + disp, 49), 0);
const newPip = driftArray[landingPip];
if (landingPip < suspicionAfterMulti || newPip < suspicionAfterMulti) {
totalValue += this.CELL_VALUE_MAP.fail;
} else {
if (!this.SAFE_CELL_SET.has(this.bar[landingPip]) && !this._isResolved(landingPip, resolvingBitmap)) {
totalValue -= this.failureCost;
}
totalValue -= this.BASE_ACTION_COST;
const landingResult =
this.algo === 'merit' && newPip !== landingPip
? this._visit(round + multi + 1, resolvingBitmap | this.MERIT_MASK_MAP[this.bar[landingPip]], 0)
: nextRoundResult;
totalValue += landingResult[newPip].value;
}
}
const avgValue = totalValue / (maxDisplacement - minDisplacement + 1) - this.BASE_ACTION_COST * multi;
if (avgValue > best.value) {
best.value = avgValue;
best.action = action;
best.multi = multi;
}
}
}
result[pip] = best;
}
return result;
}
_getDriftArray(resolvingBitmap) {
const cached = this.driftArrayMap.get(resolvingBitmap);
if (cached) {
return cached;
}
const driftArray = new Array(50);
this.driftArrayMap.set(resolvingBitmap, driftArray);
for (let pip = 0; pip < 50; pip++) {
let newPip = pip;
switch (this.bar[pip]) {
case 'temptation':
while (
newPip + 1 < 50 &&
(!this.SAFE_CELL_SET.has(this.bar[newPip]) || this.bar[newPip] === 'temptation') &&
!this._isResolved(newPip, resolvingBitmap)
) {
newPip++;
}
break;
case 'sensitivity':
while (newPip > 0 && this.bar[newPip] !== 'neutral' && !this._isResolved(newPip, resolvingBitmap)) {
newPip--;
}
break;
}
driftArray[pip] = newPip;
}
return driftArray;
}
_getCellResult(pip, resolvingBitmap) {
let value = this.CELL_VALUE_MAP[this.bar[pip]] ?? 0;
if (this.algo === 'merit' && (resolvingBitmap & this.MERIT_REQUIREMENT_MASK) !== this.MERIT_REQUIREMENT_MASK) {
value = Math.min(value, 0);
}
const action = this.bar[pip] === 'fail' ? 'fail' : value > 0 ? 'capitalize' : 'abandon';
return { value, action, multi: 0 };
}
_estimateSuspicion(round) {
if (round <= this.initialRound) {
return this.initialSuspicion;
}
const predefined = [0, 0, 0, 0, 2, 5, 8, 11, 16, 23, 34, 50][round] ?? 50;
const current = Math.floor(this.initialSuspicion * 1.5 ** (round - this.initialRound));
return Math.max(predefined, current);
}
_isResolved(pip, resolvingBitmap) {
return ((1n << BigInt(pip)) & resolvingBitmap) !== 0n;
}
}
class ScammingStore {
get TARGET_LEVEL_MAP() {
return {
'delivery scam': 1,
'family scam': 1,
'prize scam': 1,
'charity scam': 20,
'tech support scam': 20,
'vacation scam': 40,
'tax scam': 40,
'advance-fee scam': 60,
'job scam': 60,
'romance scam': 80,
'investment scam': 80,
};
}
get SPAM_ID_MAP() {
return {
295: 'delivery',
293: 'family',
291: 'prize',
297: 'charity',
299: 'tech support',
301: 'vacation',
303: 'tax',
305: 'advance-fee',
307: 'job',
309: 'romance',
311: 'investment',
};
}
constructor() {
this.data = getValue('scamming', {});
this.data.targets = this.data.targets ?? {};
this.data.farms = this.data.farms ?? {};
this.data.spams = this.data.spams ?? {};
this.data.defaultAlgo = this.data.defaultAlgo ?? 'exp';
this.data.algoNotice = this.data.algoNotice ?? {};
this.unsyncedSet = new Set(Object.keys(this.data.targets));
this.solvers = {};
this.lastSolutions = {};
this.cash = undefined;
}
update(data) {
this._updateTargets(data.DB?.crimesByType?.targets);
this._updateFarms(data.DB?.additionalInfo?.currentOngoing);
this._updateSpams(data.DB?.currentUserStats?.crimesByIDAttempts, data.DB?.crimesByType?.methods);
this.cash = data.DB?.user?.money;
this._save();
}
setDefaultAlgo(algo) {
this.data.defaultAlgo = algo;
this._save();
}
changeAlgo(target) {
target.algos.push(target.algos.shift());
target.solution = null;
this._solve(target);
this._save();
}
setAlgoNoticeRead(algo) {
this.data.algoNotice[algo] = true;
this._save();
}
_save() {
setValue('scamming', this.data);
}
_updateTargets(targets) {
if (!targets) {
return;
}
for (const target of targets) {
const stored = this.data.targets[target.subID];
if (stored && !target.new && target.bar) {
stored.driftBitmap = stored.driftBitmap ?? 0; // data migration for v1.4.6
stored.turns = stored.turns ?? target.turns ?? 0; // data migration for v1.4.10
stored.mark = (target.target ?? '').toLowerCase();
let updated = false;
if (
stored.multiplierUsed !== target.multiplierUsed ||
stored.pip !== target.pip ||
stored.turns !== (target.turns ?? 0)
) {
stored.multiplierUsed = target.multiplierUsed;
stored.pip = target.pip;
stored.turns = target.turns ?? 0;
stored.expire = target.expire;
updated = true;
}
if (updated && this.unsyncedSet.has(stored.id)) {
stored.unsynced = true; // replied on another device
}
this.unsyncedSet.delete(stored.id);
if (stored.bar) {
for (let pip = 0; pip < 50; pip++) {
if (target.bar[pip] === stored.bar[pip]) {
continue;
}
if (target.bar[pip] === 'fail' && stored.suspicion <= pip) {
stored.suspicion = pip + 1;
updated = true;
}
if (target.bar[pip] === 'neutral' && (BigInt(stored.resolvingBitmap) & (1n << BigInt(pip))) === 0n) {
stored.resolvingBitmap = (BigInt(stored.resolvingBitmap) | (1n << BigInt(pip))).toString();
updated = true;
}
}
if (target.firstPip) {
if (stored.bar[target.firstPip] === 'temptation') {
stored.driftBitmap |= 1;
}
if (stored.bar[target.firstPip] === 'sensitivity') {
stored.driftBitmap |= 2;
}
}
}
if (updated) {
// Round is not accurate for concern and hesitation.
stored.round = stored.unsynced ? this._estimateRound(target) : stored.round + 1;
}
if (!stored.bar) {
stored.bar = target.bar;
updated = true;
}
if (updated || !stored.solution) {
this._solve(stored);
}
} else {
const multiplierUsed = target.multiplierUsed ?? 0;
const pip = target.pip ?? 0;
const round = multiplierUsed === 0 && pip === 0 ? 0 : Math.max(1, multiplierUsed);
const stored = {
id: target.subID,
email: target.email,
level: this.TARGET_LEVEL_MAP[target.scamMethod.toLowerCase()] ?? 999,
mark: '',
round,
turns: target.turns ?? 0,
multiplierUsed,
pip,
expire: target.expire,
bar: target.bar ?? null,
suspicion: 0,
resolvingBitmap: '0',
driftBitmap: 0,
algos: null,
solution: null,
unsynced: round > 0,
};
this.data.targets[target.subID] = stored;
this._solve(stored);
}
}
const now = Math.floor(Date.now() / 1000);
for (const target of Object.values(this.data.targets)) {
if (target.expire < now) {
delete this.data.targets[target.id];
}
}
}
_updateFarms(currentOngoing) {
if (typeof currentOngoing !== 'object' || !(currentOngoing.length > 0)) {
return;
}
for (const item of currentOngoing) {
if (!item.type) {
continue;
}
this.data.farms[item.type] = { expire: item.timeEnded };
}
}
_updateSpams(crimesByIDAttempts, methods) {
if (!crimesByIDAttempts || !methods) {
return;
}
const now = Math.floor(Date.now() / 1000);
for (const [id, count] of Object.entries(crimesByIDAttempts)) {
const type = this.SPAM_ID_MAP[id];
const method = methods.find((x) => String(x.crimeID) === id);
if (!type || !method) {
continue;
}
const stored = this.data.spams[id];
if (stored) {
if (count !== stored.count) {
stored.count = count;
stored.accurate = now - stored.ts < 3600;
stored.since = now;
}
stored.ts = now;
stored.depreciation = method.depreciation;
} else {
this.data.spams[id] = {
count,
accurate: false,
since: null,
ts: now,
depreciation: method.depreciation,
};
}
}
}
_solve(target) {
if (!target.bar) {
return;
}
this.lastSolutions[target.id] = target.solution;
let solver = this.solvers[target.id];
if (!solver || solver.algo !== target.algos?.[0] || target.suspicion > 0) {
if (!target.algos) {
target.algos = ['exp'];
if (this._isDecepticonFeasible(target)) {
target.algos.push('merit');
}
if (this._isGriftHorseFeasible(target)) {
target.algos.push('meritGrift');
}
const defaultIndex = target.algos.indexOf(this.data.defaultAlgo);
if (defaultIndex > 0) {
target.algos = [...target.algos.slice(defaultIndex), ...target.algos.slice(0, defaultIndex)];
}
}
solver = new ScammingSolver(
target.algos[0],
target.bar,
target.level,
target.round,
target.suspicion,
target.mark,
);
this.solvers[target.id] = solver;
}
target.solution = solver.solve(
target.round,
target.pip,
BigInt(target.resolvingBitmap),
target.multiplierUsed,
target.driftBitmap,
);
}
_estimateRound(target) {
// This "turns" from the server gets +2 from temptation and sensitivity (round +1 in these cases) and
// gets +1 from hesitation (round +2 in this case).
// The "*Attempt" fields from the server are at most 1 even with multiple attempts.
return Math.max(
0,
(target.turns ?? 0) -
(target.temptationAttempt ?? 0) -
(target.sensitivityAttempt ?? 0) +
(target.hesitationAttempt ?? 0),
);
}
_isDecepticonFeasible(target) {
const cells = new Set(target.bar);
return cells.has('temptation') && cells.has('sensitivity') && cells.has('hesitation') && cells.has('concern');
}
_isGriftHorseFeasible(target) {
return target.mark === 'affluent';
}
}
class ScammingObserver {
constructor() {
this.store = new ScammingStore();
this.crimeOptions = null;
this.farmIcons = null;
this.spamOptions = null;
this.virtualLists = null;
this.observer = new MutationObserver((mutations) => {
const isAdd = mutations.some((mutation) => {
for (const added of mutation.addedNodes) {
if (added instanceof HTMLElement) {
return true;
}
}
return false;
});
if (!isAdd) {
return;
}
for (const element of this.crimeOptions) {
if (!element.classList.contains('cm-sc-seen')) {
element.classList.add('cm-sc-seen');
this._refreshCrimeOption(element);
}
}
for (const element of this.farmIcons) {
if (!element.classList.contains('cm-sc-seen')) {
element.classList.add('cm-sc-seen');
this._refreshFarm(element);
}
}
for (const element of this.spamOptions) {
if (!element.classList.contains('cm-sc-seen')) {
element.classList.add('cm-sc-seen');
this._refreshSpam(element);
}
}
for (const element of this.virtualLists) {
if (!element.classList.contains('cm-sc-seen')) {
element.classList.add('cm-sc-seen');
this._refreshSettings(element);
}
}
});
}
start() {
if (this.crimeOptions) {
return;
}
this.crimeOptions = document.body.getElementsByClassName('crime-option');
this.farmIcons = document.body.getElementsByClassName('scraperPhisher___oy1Wn');
this.spamOptions = document.body.getElementsByClassName('optionWithLevelRequirement___cHH35');
this.virtualLists = document.body.getElementsByClassName('virtualList___noLef');
this.observer.observe($('.scamming-root')[0], { subtree: true, childList: true });
}
stop() {
this.crimeOptions = null;
this.observer.disconnect();
}
onNewData() {
this.start();
for (const element of this.crimeOptions) {
this._refreshCrimeOption(element);
}
for (const element of this.farmIcons) {
this._refreshFarm(element);
}
for (const element of this.spamOptions) {
this._refreshSpam(element);
}
}
_buildHintHtml(target, solution, lastSolution, showGriftNotice) {
const actionText =
{
strong: 'Fast Fwd',
soft: 'Soft Fwd',
back: 'Back',
capitalize: '$$$',
abandon: 'Abandon',
resolve: 'Resolve',
}[solution.action] ?? 'N/A';
const algo = target.algos?.[0];
const algoText =
{
exp: 'Exp',
merit: 'Decep',
meritGrift: 'Grift',
}[algo] ?? 'Score';
const score = Math.floor(solution.value * 100);
const scoreText = `${score}${algo === 'meritGrift' ? '%' : ''}`;
let scoreColor = '';
if (algo === 'meritGrift') {
scoreColor = score < 30 ? 't-red' : score < 60 ? 't-yellow' : 't-green';
} else {
scoreColor = score < 30 ? 't-red' : score < 100 ? 't-yellow' : 't-green';
}
const scoreDiff = lastSolution ? score - Math.floor(lastSolution.value * 100) : 0;
const scoreDiffColor = scoreDiff > 0 ? 't-green' : 't-red';
const scoreDiffText = scoreDiff !== 0 ? `(${scoreDiff > 0 ? '+' : ''}${scoreDiff})` : '';
let rspText = solution.multi > target.multiplierUsed ? 'Accel' : actionText;
let rspColor = '';
let fullRspText = solution.multi > 0 ? `(${target.multiplierUsed}/${solution.multi} + ${actionText})` : '';
if (target.unsynced) {
rspText = 'Unsynced';
rspColor = 't-gray-c';
fullRspText = fullRspText !== '' ? fullRspText : `(${actionText})`;
}
const $wrapper = $('<span class="cm-sc-info cm-sc-hint cm-sc-hint-content"></span>');
if (showGriftNotice) {
$wrapper.append(`<span><span class="cm-sc-algo">${algoText}</span></span>`);
$wrapper.append('<span class="cm-sc-notice t-blue">Click to read about this strategy</span>');
$wrapper.children('.cm-sc-notice').on('click', () => {
const msg =
'Warning: The "Grift Horse" strategy is highly aggressive and does NOT avoid critical failures. ' +
'You may lose a significant amount of crime experience.\n\n' +
'Click OK to proceed with this risky strategy, or Cancel to choose a safer alternative.';
if (confirm(msg)) {
this.store.setAlgoNoticeRead(algo);
location.reload();
}
});
} else {
$wrapper.append(
`<span><span class="cm-sc-algo">${algoText}</span>: <span class="${scoreColor}">${scoreText}</span><span class="${scoreDiffColor}">${scoreDiffText}</span></span>`,
);
$wrapper.append(
`<span class="cm-sc-hint-action"><span class="${rspColor}">${rspText}</span> <span class="t-gray-c">${fullRspText}</span></span>`,
);
}
$wrapper.append(`<span class="cm-sc-hint-button t-blue">Lv${target.level}</span>`);
return $wrapper;
}
_refreshCrimeOption(element) {
this._refreshTarget(element);
this._refreshFarmButton(element);
}
_refreshTarget(element) {
const $crimeOption = $(element);
const $email = $crimeOption.find('span.email___gVRXx');
const email = $email.text();
const target = Object.values(this.store.data.targets).find((x) => x.email === email);
if (!target) {
return;
}
// clear old info elements
const hasHint = $crimeOption.find('.cm-sc-hint-content').length > 0;
$crimeOption.find('.cm-sc-info').remove();
$email.parent().addClass('cm-sc-info-wrapper');
$email.parent().children().addClass('cm-sc-orig-info');
// hint
const solution = target.solution;
if (solution) {
if (!hasHint) {
$email.parent().removeClass('cm-sc-hint-hidden');
}
const algo = target.algos?.[0];
const showGriftNotice = algo === 'meritGrift' && !this.store.data.algoNotice[algo];
const actionAttr = showGriftNotice
? ''
: solution.multi > target.multiplierUsed
? 'accelerate'
: solution.action;
$crimeOption.attr('data-cm-action', actionAttr);
$crimeOption.toggleClass('cm-sc-unsynced', !showGriftNotice && (target.unsynced ?? false));
const lastSolution = this.store.lastSolutions[target.id];
$email.parent().append(this._buildHintHtml(target, solution, lastSolution, showGriftNotice));
$email.parent().append(`<span class="cm-sc-info cm-sc-orig-info cm-sc-hint-button t-blue">Hint</div>`);
$crimeOption.find('.cm-sc-hint-button').on('click', () => {
$email.parent().toggleClass('cm-sc-hint-hidden');
});
if (target.algos?.length > 1) {
const $algo = $crimeOption.find('.cm-sc-algo');
$algo.addClass('t-blue');
$algo.addClass('cm-sc-active');
$algo.on('click', () => {
this.store.changeAlgo(target);
this._refreshTarget(element);
});
}
} else {
$email.parent().addClass('cm-sc-hint-hidden');
}
// lifetime
const now = Math.floor(Date.now() / 1000);
const lifetime = formatLifetime(target.expire - now);
$email.before(`<span class="cm-sc-info ${lifetime.color}">${lifetime.text}</div>`);
// scale
const $cells = $crimeOption.find('.cell___AfwZm');
if ($cells.length >= 50) {
$cells.find('.cm-sc-scale').remove();
// Ignore cells after the first 50, which are faded out soon
for (let i = 0; i < 50; i++) {
const dist = i - target.pip;
const label = dist % 5 !== 0 || dist === 0 || dist < -5 ? '' : dist % 10 === 0 ? (dist / 10).toString() : "'";
let $scale = $cells.eq(i).children('.cm-sc-scale');
if ($scale.length === 0) {
$scale = $('<div class="cm-sc-scale"></div>');
$cells.eq(i).append($scale);
}
$scale.text(label);
}
}
// multiplier
const $accButton = $crimeOption.find('.response-type-button').eq(3);
$accButton.find('.cm-sc-multiplier').remove();
if (target.multiplierUsed > 0) {
$accButton.append(`<div class="cm-sc-multiplier">${target.multiplierUsed}</div>`);
}
}
_refreshFarmButton(element) {
const $element = $(element);
if ($element.find('.emailAddresses___ky_qG').length === 0) {
return;
}
$element.find('.commitButtonSection___wJfnI button').toggleClass('cm-sc-low-cash', this.store.cash < 10000);
}
_refreshFarm(element) {
const $element = $(element);
const label = $element.attr('aria-label') ?? '';
const farm = Object.entries(this.store.data.farms).find(([type]) => label.toLowerCase().includes(type))?.[1];
if (!farm) {
return;
}
const now = Math.floor(Date.now() / 1000);
const lifetime = formatLifetime(farm.expire - now);
$element.find('.cm-sc-farm-lifetime').remove();
$element.append(`<div class="cm-sc-farm-lifetime ${lifetime.color}">${lifetime.text}</div>`);
}
_refreshSpam(element) {
const $spamOption = $(element);
if ($spamOption.closest('.dropdownList').length === 0) {
return;
}
const label = $spamOption
.contents()
.filter((_, x) => x.nodeType === Node.TEXT_NODE)
.text();
const spam = Object.entries(this.store.data.spams).find(([id]) =>
label.toLowerCase().includes(this.store.SPAM_ID_MAP[id]),
)?.[1];
$spamOption.addClass('cm-sc-spam-option');
$spamOption.find('.cm-sc-spam-elapsed').remove();
if (!spam || !spam.since || spam.depreciation) {
return;
}
const now = Math.floor(Date.now() / 1000);
const elapsed = formatLifetime(now - spam.since);
if (!spam.accurate) {
elapsed.text = '> ' + elapsed.text;
}
if (elapsed.hours >= 24 * 8) {
elapsed.text = '> 7d';
}
if (elapsed.hours >= 24 && elapsed.hours < 72) {
elapsed.color = 't-green';
}
$spamOption.append(`<div class="cm-sc-spam-elapsed ${elapsed.color}">${elapsed.text}</div>`);
}
_refreshSettings(element) {
const store = this.store;
const defaultAlgo = store.data.defaultAlgo;
const $settings = $(`<div class="cm-sc-settings">
<span>Default Strategy:</span>
<span class="cm-sc-algo-option t-blue" data-cm-value="exp">Exp</span>
<span class="cm-sc-algo-option t-blue" data-cm-value="merit">Decepticon</span>
<span class="cm-sc-algo-option t-blue" data-cm-value="meritGrift">Grift Horse</span>
</div>`);
$settings.children(`[data-cm-value="${defaultAlgo}"]`).addClass('cm-sc-active');
$settings.children('.cm-sc-algo-option').on('click', function () {
const $this = $(this);
store.setDefaultAlgo($this.attr('data-cm-value'));
$this.siblings().removeClass('cm-sc-active');
$this.addClass('cm-sc-active');
});
$settings.insertBefore(element);
}
}
const scammingObserver = new ScammingObserver();
async function checkScamming(crimeType, data) {
if (crimeType !== '12') {
scammingObserver.stop();
return;
}
scammingObserver.store.update(data);
scammingObserver.onNewData();
}
async function onCrimeData(crimeType, data) {
await checkDemoralization(data);
await checkBurglary(crimeType, data);
await checkPickpocketing(crimeType);
await checkScamming(crimeType, data);
}
function interceptFetch() {
const targetWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
const origFetch = targetWindow.fetch;
targetWindow.fetch = async (...args) => {
const rsp = await origFetch(...args);
try {
const url = new URL(args[0], location.origin);
const params = new URLSearchParams(url.search);
const reqBody = args[1]?.body;
const crimeType = params.get('typeID') ?? reqBody?.get('typeID');
if (url.pathname === '/page.php' && params.get('sid') === 'crimesData' && crimeType) {
const clonedRsp = rsp.clone();
await onCrimeData(crimeType, await clonedRsp.json());
}
} catch {
// ignore
}
return rsp;
};
}
function renderMorale() {
const interval = setInterval(async function () {
if (!$) {
return; // JQuery is not loaded in TornPDA yet
}
const $container = $('.crimes-app-header');
if ($container.length === 0) {
return;
}
clearInterval(interval);
$container.append(`<span>Morale: <span id="crime-morale-value">-</span>%</span>`);
const morale = parseInt(await getValue(STORAGE_MORALE));
if (!isNaN(morale)) {
updateMorale(morale);
}
// Show hidden debug button on double-click
let lastClick = 0; // dblclick event doesn't work well on mobile
$('#crime-morale-value')
.parent()
.on('click', function () {
if (Date.now() - lastClick > 1000) {
lastClick = Date.now();
return;
}
const data = {
morale: getValue(STORAGE_MORALE),
burglary: getValue('burglary'),
scamming: getValue('scamming'),
};
const export_uri = `data:application/json;charset=utf-8,${encodeURIComponent(JSON.stringify(data))}`;
$(this).replaceWith(`<a download="crime-morale-debug.json" href="${export_uri}"
class="torn-btn" style="display:inline-block;">Export Debug Data</a>`);
});
}, 500);
}
function updateMorale(morale) {
$('#crime-morale-value').text(morale.toString());
}
function renderStyle() {
addStyle(`
.cm-bg-lifetime {
position: absolute;
top: 0;
right: 0;
padding: 2px;
background: var(--default-bg-panel-color);
border: 1px solid darkgray;
}
.cm-bg-favor {
position: absolute;
right: 0;
bottom: 0;
background: #fffc;
height: 20px;
width: 20px;
font-size: 20px;
line-height: 1;
cursor: pointer;
pointer-events: auto !important;
}
.cm-bg-favor:after {
content: '\u2606';
display: block;
width: 100%;
height: 100%;
text-align: center;
}
.cm-bg-favor.cm-bg-active:after {
content: '\u2605';
color: orange;
}
:root {
--cm-pp-level-1: #37b24d;
--cm-pp-level-2: #95af14;
--cm-pp-level-3: #f4cc00;
--cm-pp-level-4: #fa9201;
--cm-pp-level-5: #e01111;
--cm-pp-level-6: #a016eb;
--cm-pp-filter-level-1: brightness(0) saturate(100%) invert(61%) sepia(11%) saturate(2432%) hue-rotate(79deg) brightness(91%) contrast(96%);
--cm-pp-filter-level-2: brightness(0) saturate(100%) invert(62%) sepia(80%) saturate(2102%) hue-rotate(32deg) brightness(99%) contrast(84%);
--cm-pp-filter-level-3: brightness(0) saturate(100%) invert(71%) sepia(53%) saturate(1820%) hue-rotate(9deg) brightness(107%) contrast(102%);
--cm-pp-filter-level-4: brightness(0) saturate(100%) invert(61%) sepia(62%) saturate(1582%) hue-rotate(356deg) brightness(94%) contrast(108%);
--cm-pp-filter-level-5: brightness(0) saturate(100%) invert(12%) sepia(72%) saturate(5597%) hue-rotate(354deg) brightness(105%) contrast(101%);
--cm-pp-filter-level-6: brightness(0) saturate(100%) invert(26%) sepia(84%) saturate(4389%) hue-rotate(271deg) brightness(86%) contrast(119%);
}
@keyframes cm-fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
visibility: hidden;
}
}
.cm-overlay {
position: relative;
}
.cm-overlay:after {
content: '';
position: absolute;
background: repeating-linear-gradient(135deg, #2223, #2223 70px, #0003 70px, #0003 80px);
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 900000;
}
.cm-overlay-fade:after {
animation-name: cm-fade-out;
animation-duration: 0.2s;
animation-timing-function: ease-in;
animation-fill-mode: forwards;
animation-delay: 0.4s
}
.cm-pp-level-1 {
color: var(--cm-pp-level-1);
}
.cm-pp-level-2 {
color: var(--cm-pp-level-2);
}
.cm-pp-level-3 {
color: var(--cm-pp-level-3);
}
.cm-pp-level-4 {
color: var(--cm-pp-level-4);
}
.cm-pp-level-5 {
color: var(--cm-pp-level-5);
}
.cm-pp-level-6 {
color: var(--cm-pp-level-6);
}
.cm-pp-level-1 [class*=timerCircle___] [class*=icon___] {
filter: var(--cm-pp-filter-level-1);
}
.cm-pp-level-2 [class*=timerCircle___] [class*=icon___] {
filter: var(--cm-pp-filter-level-2);
}
.cm-pp-level-3 [class*=timerCircle___] [class*=icon___] {
filter: var(--cm-pp-filter-level-3);
}
.cm-pp-level-4 [class*=timerCircle___] [class*=icon___] {
filter: var(--cm-pp-filter-level-4);
}
.cm-pp-level-5 [class*=timerCircle___] [class*=icon___] {
filter: var(--cm-pp-filter-level-5);
}
.cm-pp-level-6 [class*=timerCircle___] [class*=icon___] {
filter: var(--cm-pp-filter-level-6);
}
.cm-pp-level-1 [class*=timerCircle___] .CircularProgressbar-path {
stroke: var(--cm-pp-level-1) !important;
}
.cm-pp-level-2 [class*=timerCircle___] .CircularProgressbar-path {
stroke: var(--cm-pp-level-2) !important;
}
.cm-pp-level-3 [class*=timerCircle___] .CircularProgressbar-path {
stroke: var(--cm-pp-level-3) !important;
}
.cm-pp-level-4 [class*=timerCircle___] .CircularProgressbar-path {
stroke: var(--cm-pp-level-4) !important;
}
.cm-pp-level-5 [class*=timerCircle___] .CircularProgressbar-path {
stroke: var(--cm-pp-level-5) !important;
}
.cm-pp-level-6 [class*=timerCircle___] .CircularProgressbar-path {
stroke: var(--cm-pp-level-6) !important;
}
.cm-pp-level-1 [class*=commitButton___] {
border: 2px solid var(--cm-pp-level-1);
}
.cm-pp-level-2 [class*=commitButton___] {
border: 2px solid var(--cm-pp-level-2);
}
.cm-pp-level-3 [class*=commitButton___] {
border: 2px solid var(--cm-pp-level-3);
}
.cm-pp-level-4 [class*=commitButton___] {
border: 2px solid var(--cm-pp-level-4);
}
.cm-pp-level-5 [class*=commitButton___] {
border: 2px solid var(--cm-pp-level-5);
}
.cm-pp-best-build:not(.crime-option-locked) [class*=physicalProps___]:before {
content: '\u2713 ';
font-weight: bold;
color: var(--cm-pp-level-2);
}
.cm-sc-info {
transform: translateY(1px);
}
.cm-sc-notice,
.cm-sc-hint-button {
cursor: pointer;
}
.cm-sc-info-wrapper.cm-sc-hint-hidden > .cm-sc-hint,
.cm-sc-info-wrapper:not(.cm-sc-hint-hidden) > .cm-sc-orig-info {
display: none;
}
.cm-sc-hint-content {
display: flex;
justify-content: space-between;
flex-grow: 1;
gap: 5px;
white-space: nowrap;
overflow: hidden;
}
.cm-sc-notice,
.cm-sc-hint-action {
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.cm-sc-seen[data-cm-action=strong] .response-type-button:nth-child(1):after,
.cm-sc-seen[data-cm-action=soft] .response-type-button:nth-child(2):after,
.cm-sc-seen[data-cm-action=back] .response-type-button:nth-child(3):after,
.cm-sc-seen[data-cm-action=accelerate] .response-type-button:nth-child(4):after,
.cm-sc-seen[data-cm-action=capitalize] .response-type-button:nth-child(5):after {
content: '\u2713';
color: var(--crimes-green-color);
position: absolute;
top: 0;
right: 0;
font-size: 12px;
font-weight: bolder;
line-height: 1;
z-index: 999;
}
.cm-sc-seen.cm-sc-unsynced[data-cm-action=strong] .response-type-button:nth-child(1):after,
.cm-sc-seen.cm-sc-unsynced[data-cm-action=soft] .response-type-button:nth-child(2):after,
.cm-sc-seen.cm-sc-unsynced[data-cm-action=back] .response-type-button:nth-child(3):after,
.cm-sc-seen.cm-sc-unsynced[data-cm-action=accelerate] .response-type-button:nth-child(4):after,
.cm-sc-seen.cm-sc-unsynced[data-cm-action=capitalize] .response-type-button:nth-child(5):after {
content: '?';
}
.cm-sc-seen[data-cm-action=abandon] .response-type-button:after {
content: '\u2715';
color: var(--crimes-stats-criticalFails-color);
position: absolute;
top: 0;
right: 0;
font-size: 12px;
font-weight: bolder;
line-height: 1;
z-index: 999;
}
.cm-sc-scale {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: calc(100% + 10px);
line-height: 1;
font-size: 8px;
display: flex;
align-items: flex-end;
justify-content: center;
}
.cm-sc-multiplier {
position: absolute;
bottom: 0;
right: 0;
color: var(--crimes-baseText-color);
text-align: right;
font-size: 10px;
line-height: 1;
}
.cm-sc-farm-lifetime {
padding-top: 2px;
text-align: center;
}
.cm-sc-spam-option .levelLabel___LNbg8,
.cm-sc-spam-option .separator___C2skk {
display: none;
}
.cm-sc-spam-elapsed {
position: absolute;
right: -5px;
}
.cm-sc-settings {
height: 40px;
width: 100%;
background: var(--default-bg-panel-color););
border-bottom: 1px solid var(--crimes-crimeOption-borderBottomColor);
padding-left: 10px;
box-sizing: border-box;
display: flex;
align-items: center;
gap: 20px;
}
.cm-sc-algo-option {
cursor: pointer;
line-height: 1.5;
border-top: 2px solid #0000;
border-bottom: 2px solid #0000;
}
.cm-sc-algo-option.cm-sc-active {
border-bottom-color: var(--default-blue-color);
}
.cm-sc-algo.cm-sc-active {
cursor: pointer;
}
.cm-sc-algo.cm-sc-active:before {
content: '\u21bb ';
}
.cm-sc-low-cash:after {
content: 'Low Cash';
color: var(--default-red-color);
position: absolute;
width: 100%;
left: 0;
top: calc(100% - 4px);
line-height: 1;
font-size: 12px;
}
`);
}
interceptFetch();
renderMorale();
if (document.readyState === 'loading') {
document.addEventListener('readystatechange', () => {
if (document.readyState === 'interactive') {
renderStyle();
}
});
} else {
renderStyle();
}
}
// ================================
// 🐑 Sheepies Market Pricer original by Silmaril [2665762]
// ================================
async function initMarketFiller(){
'use strict';
if (window.SHEEPIE_Market) return;
window.SHEEPIE_Market = true;
const itemUrl = "https://api.torn.com/torn/{itemId}?selections=items&key={apiKey}&comment=MarketFiller";
const marketUrl = "https://api.torn.com/v2/market/{itemId}?selections=itemMarket&key={apiKey}&comment=MarketFiller";
const marketUrlV2 = "https://api.torn.com/v2/market?id={itemId}&selections=itemMarket&key={apiKey}&comment=MarketFiller";
let showPricesPopup = localStorage.getItem("silmaril-torn-market-filler-show-prices-popup") ?? '1';
showPricesPopup = Boolean(parseInt(showPricesPopup));
let priceDeltaRaw = localStorage.getItem("silmaril-torn-market-filler-price-delta") ?? localStorage.getItem("silmaril-torn-bazaar-filler-price-delta") ?? '-1[0]';
let apiKey = localStorage.getItem("sheepAPI") ?? '###PDA-APIKEY###';
let togglePricesPopupMenuId, setPriceDeltaMenuId, setApiKeyMenuId;
try {
togglePricesPopupMenuId = GM_registerMenuCommand(`Toggle Prices Popup (${showPricesPopup ? 'ON' : 'OFF'})`, togglePricesPopupVisibility);
setPriceDeltaMenuId = GM_registerMenuCommand(`Set Price Delta: ${priceDeltaRaw}`, setPriceDelta);
setApiKeyMenuId = GM_registerMenuCommand(`Set Api Key: ${apiKey}`, function() { checkApiKey(false); });
} catch (error) {
console.warn('[TornMarketFiller] Tampermonkey not detected!');
}
let GM_addStyle = function (s) {
let style = document.createElement("style");
style.type = "text/css";
style.innerHTML = s;
document.head.appendChild(style);
};
GM_addStyle(`#item-market-root [class^=addListingWrapper___] [class^=panels___] [class^=priceInputWrapper___]>.input-money-group>.input-money,#item-market-root [class^=viewListingWrapper___] [class^=priceInputWrapper___]>.input-money-group>.input-money{font-size:smaller!important;border-bottom-left-radius:0!important;border-top-left-radius:0!important}.silmaril-market-filler-popup{background:var(--tooltip-bg-color);padding:12px 18px;border-radius:8px;border:1px solid #888;box-shadow:0 4px 18px 0 #0009;color:var(--info-msg-font-color);z-index:99999;position:absolute;font-size:1em!important;line-height:1.5;pointer-events:auto}.silmaril-market-filler-popup-close{position:absolute;top:4px;right:7px;font-size:1em;color:#aaa;cursor:pointer}.silmaril-market-filler-popup-draggable{user-select:none;cursor:move}.silmaril-torn-market-filler-popup-price{cursor:pointer}`);
const pages = { "AddItems": 10, "ViewItems": 20, "Other": 0};
let recentFilledInput = null;
let popupOffsetX = localStorage.getItem("silmaril-torn-market-filler-popup-offset-x") ?? 0, popupOffsetY = 0, isDragging = false;
const marketTaxFactor = 1 - getCurrentMarketTax();
let currentPage = pages.Other;
let holdTimer;
const LOADING_THE_PRICES = 'Loading the prices...';
const isMobileView = window.innerWidth <= 784;
const observerTarget = document.querySelector("#item-market-root");
const observerConfig = { attributes: false, childList: true, characterData: false, subtree: true };
const observer = new MutationObserver(function(mutations) {
mutations.forEach(mutationRaw => {
let mutation = mutationRaw.target;
currentPage = getCurrentPage();
if (currentPage == pages.AddItems){
if (mutation.id && mutation.id.startsWith('headlessui-tabs-panel-')) {
mutation.querySelectorAll('[class*=itemRowWrapper___]:not(.silmaril-market-filler-processed) > [class*=itemRow___]:not([class*=grayedOut___]) [class^=priceInputWrapper___]').forEach(x => AddFillButton(x));
}
if (String(mutation.className).indexOf('priceInputWrapper___') > -1){
AddFillButton(mutation);
}
} else if (currentPage == pages.ViewItems){
if (mutation.className && mutation.className.startsWith('viewListingWrapper___')) {
mutation.querySelectorAll('[class*=itemRowWrapper___]:not(.silmaril-market-filler-processed) > [class*=itemRow___]:not([class*=grayedOut___]) [class^=priceInputWrapper___]').forEach(x => AddFillButton(x));
}
}
});
});
observer.observe(observerTarget, observerConfig);
addCustomFillPopup();
function AddFillButton(itemPriceElement){
if (itemPriceElement.querySelector('.silmaril-market-filler-button') != null){
return;
}
const wrapperParent = findParentByCondition(itemPriceElement, (el) => String(el.className).indexOf('itemRowWrapper___') > -1);
wrapperParent.classList.add('silmaril-market-filler-processed');
let itemIdString = wrapperParent.querySelector('[class^=itemRow___] [type=button][class^=viewInfoButton___]').getAttribute('aria-controls');
let itemImage = wrapperParent.querySelector('[class*=viewInfoButton] img');
let itemId = currentPage == pages.AddItems ? getItemIdFromString(itemIdString) : getItemIdFromImage(itemImage);
const span = document.createElement('span');
span.className = 'silmaril-market-filler-button input-money-symbol';
span.style.position = "relative";
span.setAttribute('data-action-flag', 'fill');
span.addEventListener('click', async function(e) { await handleFillClick(e, itemId) });
span.addEventListener('mousedown', startHold);
span.addEventListener('touchstart', startHold);
span.addEventListener('mouseup', cancelHold);
span.addEventListener('mouseleave', cancelHold);
span.addEventListener('touchend', cancelHold);
span.addEventListener('touchcancel', cancelHold);
const input = document.createElement('input');
input.type = 'button';
input.className = 'wai-btn';
span.appendChild(input);
itemPriceElement.querySelector('.input-money-group').prepend(span);
}
async function GetPrices(itemId){
let requestUrl = priceDeltaRaw.indexOf('[market]') != -1 ? itemUrl : marketUrlV2;
requestUrl = requestUrl
.replace("{itemId}", itemId)
.replace("{apiKey}", apiKey);
return fetch(requestUrl)
.then(response => response.json())
.then(data => {
if (data.error != null){
switch (data.error.code){
case 2:
apiKey = null;
localStorage.setItem("silmaril-torn-bazaar-filler-apikey", null);
console.error("[TornMarketFiller] Incorrect Api Key:", data);
return {"price": 'Wrong API key!', "amount": 0};
case 9:
console.warn("[TornMarketFiller] The API is temporarily disabled, please try again later");
return {"price": 'API is OFF!', "amount": 0};
default:
console.error("[TornMarketFiller] Error:", data.error.error);
return {"price": data.error.error, "amount": 0};
}
}
if (priceDeltaRaw.indexOf('[market]') != -1){
return {"price": data.items[itemId].market_value, "amount": 1};
} else {
if (data.itemmarket.listings[0].price == null){
console.warn("[TornMarketFiller] The API is temporarily disabled, please try again later");
return {"price": 'API is OFF!', "amount": 0};
}
// temporary hotfix to avoid wrong prices
if (data.itemmarket.item.id != itemId){
return {"price": 'API is BROKEN!', "amount": 0};
}
return data.itemmarket.listings;
}
})
.catch(error => {
console.error("[TornMarketFiller] Error fetching data:", error);
return 'Failed!';
});
}
function GetPrice(prices){
if (prices == null){
return 'No prices loaded';
}
if (prices.amount == 0){
return prices.price;
}
if (priceDeltaRaw.indexOf('[market]') != -1) {
prices = Array(prices);
let priceDelta = priceDeltaRaw.indexOf('[') == -1 ? priceDeltaRaw : priceDeltaRaw.substring(0, priceDeltaRaw.indexOf('['));
return Math.round(performOperation(prices[0].price, priceDelta));
} else if (priceDeltaRaw.indexOf('[median]') != -1) {
let priceDelta = priceDeltaRaw.indexOf('[') == -1 ? priceDeltaRaw : priceDeltaRaw.substring(0, priceDeltaRaw.indexOf('['));
return Math.round(performOperation(getMedianPrice(prices), priceDelta));
} else {
let marketSlotOffset = priceDeltaRaw.indexOf('[') == -1 ? 0 : parseInt(priceDeltaRaw.substring(priceDeltaRaw.indexOf('[') + 1, priceDeltaRaw.indexOf(']')));
let priceDeltaWithoutMarketOffset = priceDeltaRaw.indexOf('[') == -1 ? priceDeltaRaw : priceDeltaRaw.substring(0, priceDeltaRaw.indexOf('['));
return Math.round(performOperation(prices[Math.min(marketSlotOffset, prices.length - 1)].price, priceDeltaWithoutMarketOffset));
}
}
async function handleFillClick(event, itemId){
let target = event.currentTarget || event.target;
let priceInputs = target.parentNode.querySelectorAll('input.input-money');
recentFilledInput = priceInputs;
const popup = document.querySelector('.silmaril-market-filler-popup');
if (popup) {
const rect = target.getBoundingClientRect();
if (popupOffsetX == 0){
popupOffsetX = window.scrollX + rect.left - 300;
localStorage.setItem("silmaril-torn-market-filler-popup-offset-x", popupOffsetX);
}
popupOffsetY = window.scrollY + rect.top + 4;
let left = popupOffsetX;
let top = popupOffsetY;
popup.style.display = showPricesPopup ? 'block' : 'none';
popup.style.visibility = 'hidden';
popup.style.left = `${left}px`;
popup.style.top = `${top}px`;
popup.querySelector('.silmaril-market-filler-popup-body').innerHTML = LOADING_THE_PRICES;
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
if (popupRect.right > viewportWidth) {
left = Math.max(0, viewportWidth - popupRect.width - 10 + scrollX);
}
if (popupRect.left < 0) {
left = 10 + scrollX;
}
if (popupRect.bottom > viewportHeight) {
top = Math.max(0, viewportHeight - popupRect.height - 10 + scrollY);
}
if (popupRect.top < 0) {
top = 10 + scrollY;
}
popup.style.left = `${left}px`;
popup.style.top = `${top}px`;
popup.style.visibility = 'visible';
}
let action = target.getAttribute('data-action-flag');
let prices = await GetPrices(itemId);
const breakdown = GetPricesBreakdown(prices);
// Thanks to Rosti [2840742] for the help with the prices popup component
showCustomFillPopup(target, breakdown);
let price = action == 'fill' ? GetPrice(prices) : '';
switchActionFlag(target);
let parentRow = findParentByCondition(target, (el) => String(el.className).indexOf('info___') > -1);
let quantityInputs = parentRow.querySelectorAll('[class^=amountInputWrapper___] .input-money-group > .input-money');
if (quantityInputs.length > 0){
if (quantityInputs[0].value.length === 0 || parseInt(quantityInputs[0].value) < 1){
quantityInputs[0].value = action == 'fill' ? Number.MAX_SAFE_INTEGER : 0;
quantityInputs[1].value = action == 'fill' ? Number.MAX_SAFE_INTEGER : 0;
} else {
quantityInputs[0].value = action == 'clear' ? '' : quantityInputs[0].value;
quantityInputs[1].value = action == 'clear' ? '' : quantityInputs[1].value;
}
quantityInputs[0].dispatchEvent(new Event("input", {bubbles: true}));
} else {
let checkbox = parentRow.querySelector('[class^=checkboxWrapper___] > [class^=checkboxContainer___] [type=checkbox]');
if (checkbox && ((action == 'fill' && !checkbox.checked) || (action == 'clear' && checkbox.checked))){
checkbox.click();
}
}
priceInputs.forEach(x => {x.value = price});
priceInputs[0].dispatchEvent(new Event("input", {bubbles: true}));
}
function hideAllFillPopups() {
document.querySelector('.silmaril-market-filler-popup').style.display = 'none';
}
function showCustomFillPopup(targetElem, contentHTML) {
const popup = document.querySelector('.silmaril-market-filler-popup');
popup.querySelector('.silmaril-market-filler-popup-body').innerHTML = contentHTML;
popup.querySelectorAll('.silmaril-torn-market-filler-popup-price').forEach(row => {
row.addEventListener('click', (e) => {
recentFilledInput.forEach(x => {x.value = parseInt(e.target.getAttribute('data-price')) - 1});
recentFilledInput[0].dispatchEvent(new Event("input", {bubbles: true}));
});
});
}
function addCustomFillPopup() {
const popup = document.createElement('div');
popup.className = 'silmaril-market-filler-popup';
popup.style.display = 'none';
popup.style.left = popupOffsetX + 'px';
popup.style.top = '0px';
popup.innerHTML = '<div class="silmaril-market-filler-popup-close" title="Close">×</div><b class="silmaril-market-filler-popup-draggable">Drag from here</b><br><div class="silmaril-market-filler-popup-body"></div>';
popup.querySelector('.silmaril-market-filler-popup-close').onclick = function(){ popup.style.display = 'none'; };
document.body.appendChild(popup);
const dragHandle = popup.querySelector('.silmaril-market-filler-popup-draggable');
dragHandle.addEventListener("mousedown", (e) => {
isDragging = true;
popupOffsetX = e.clientX - popup.offsetLeft;
popupOffsetY = e.clientY - popup.offsetTop;
});
document.addEventListener("mousemove", (e) => {
if (isDragging) {
popup.style.left = (e.clientX - popupOffsetX) + "px";
popup.style.top = (e.clientY - popupOffsetY) + "px";
}
});
document.addEventListener("mouseup", (e) => {
if (isDragging) {
popupOffsetX = e.clientX - popupOffsetX;
popupOffsetY = e.clientY - popupOffsetY;
localStorage.setItem("silmaril-torn-market-filler-popup-offset-x", popupOffsetX);
}
isDragging = false;
});
// Touch events (mobile)
dragHandle.addEventListener("touchstart", (e) => {
isDragging = true;
const touch = e.touches[0];
popupOffsetX = touch.clientX - popup.offsetLeft;
popupOffsetY = touch.clientY - popup.offsetTop;
e.preventDefault();
}, { passive: false });
document.addEventListener("touchmove", (e) => {
if (isDragging) {
const touch = e.touches[0];
popup.style.left = (touch.clientX - popupOffsetX) + "px";
popup.style.top = (touch.clientY - popupOffsetY) + "px";
}
}, { passive: false });
document.addEventListener("touchend", () => {
if (isDragging) {
popupOffsetX = popup.offsetLeft;
popupOffsetY = popup.offsetTop;
localStorage.setItem("silmaril-torn-market-filler-popup-offset-x", popupOffsetX);
}
isDragging = false;
});
}
function getItemIdFromString(string){
const match = string.match(/-(\d+)-/);
if (match) {
const number = match[1];
return number;
} else {
console.error("[TornMarketFiller] ItemId not found!");
return -1;
}
}
function getItemIdFromImage(image){
let numberPattern = /\/(\d+)\//;
let match = image.src.match(numberPattern);
if (match) {
return parseInt(match[1], 10);
} else {
console.error("[TornMarketFiller] ItemId not found!");
return -1;
}
}
function switchActionFlag(target){
switch (target.getAttribute('data-action-flag')){
case 'fill':
target.setAttribute('data-action-flag', 'clear');
break;
case 'clear':
default:
target.setAttribute('data-action-flag', 'fill');
break;
}
}
function findParentByCondition(element, conditionFn){
let currentElement = element;
while (currentElement !== null) {
if (conditionFn(currentElement)) {
return currentElement;
}
currentElement = currentElement.parentElement;
}
return null;
}
function setPriceDelta() {
let userInput = prompt('Enter price delta formula (default: -1[0]):', priceDeltaRaw);
if (userInput !== null) {
priceDeltaRaw = userInput;
localStorage.setItem("silmaril-torn-market-filler-price-delta", userInput);
} else {
console.error("[TornMarketFiller] User cancelled the Price Delta input.");
}
}
function GetPricesBreakdown(prices){
if (prices == null) return "No prices loaded";
if (prices[0] === undefined){
prices = Array(prices);
}
const sb = new StringBuilder();
for (let i = 0; i < Math.min(prices.length, 5); i++){
if(typeof prices[i] !== "object" || prices[i].amount === undefined || prices[i].price === undefined) continue;
sb.append(`<span class="silmaril-torn-market-filler-popup-price" data-price=${prices[i].price}>${prices[i].amount} x ${formatNumberWithCommas(prices[i].price)} (${formatNumberWithCommas(Math.round(prices[i].price * marketTaxFactor))})</span>`);
if (i < Math.min(prices.length, 5)-1){
sb.append('<br>');
}
}
return sb.toString();
}
function performOperation(number, operation) {
const match = operation.match(/^([-+]?)(\d+(?:\.\d+)?)(%)?$/);
if (!match) {
throw new Error('Invalid operation string');
}
const [, operator, operand, isPercentage] = match;
const operandValue = parseFloat(operand);
const adjustedOperand = isPercentage ? (number * operandValue) / 100 : operandValue;
switch (operator) {
case '':
case '+':
return number + adjustedOperand;
case '-':
return number - adjustedOperand;
default:
throw new Error('Invalid operator');
}
}
function formatNumberWithCommas(number) {
return new Intl.NumberFormat('en-US').format(number);
}
function checkApiKey(checkExisting = true) {
if (!checkExisting || apiKey === null || apiKey.indexOf('PDA-APIKEY') > -1 || apiKey.length != 16){
let userInput = prompt("Please enter a PUBLIC Api Key, it will be used to get current bazaar prices:", apiKey ?? '');
if (userInput !== null && userInput.length == 16) {
apiKey = userInput;
localStorage.setItem("sheepAPI", userInput);
} else {
console.error("[TornMarketFiller] User cancelled the Api Key input.");
}
}
}
function askForPricesPopupFlag() {
let dsf = null;
let userInput = prompt("Please choose to show or hide the lowest 5 prices popup, enter 1 to SHOW or 0 to HIDE:", showPricesPopup ? '1' : '0');
if (userInput !== null && userInput.length == 1) {
if (userInput != '1' && userInput != '0'){
console.error("[TornMarketFiller] User entered invalid value for the Prices Popup input.");
return;
}
showPricesPopup = Boolean(parseInt(userInput));
localStorage.setItem('silmaril-torn-market-filler-show-prices-popup', showPricesPopup ? '1' : '0');
} else {
console.error("[TornMarketFiller] User cancelled the Prices Popup input.");
}
}
function togglePricesPopupVisibility() {
showPricesPopup = !showPricesPopup;
localStorage.setItem('silmaril-torn-market-filler-show-prices-popup', showPricesPopup ? '1' : '0');
}
function getMedianPrice(items) {
const prices = items.flatMap(item => Array(item.amount).fill(item.price));
prices.sort((a, b) => a - b);
const mid = Math.floor(prices.length / 2);
if (prices.length % 2 === 0) {
return (prices[mid - 1] + prices[mid]) / 2;
} else {
return prices[mid];
}
}
function getCurrentPage(){
if (window.location.href.indexOf('#/addListing') > -1){
return pages.AddItems;
} else if (window.location.href.indexOf('#/viewListing') > -1){
return pages.ViewItems;
} else {
return pages.Other;
}
}
function getCurrentMarketTax() {
return 0.05;
}
// function getTornToday() {
// const now = document.querySelector('span.server-date-time').textContent.split(' ');
// return now[now.length - 1];
// }
function parseDate(str) {
const [dd, mm, yy] = str.split('/').map(Number);
const fullYear = yy < 50 ? 2000 + yy : 1900 + yy;
return new Date(fullYear, mm - 1, dd);
}
const startHold = () => {
holdTimer = setTimeout(() => {
askForPricesPopupFlag();
setPriceDelta();
checkApiKey(false);
}, 2000);
};
const cancelHold = () => {
clearTimeout(holdTimer);
};
class StringBuilder {
constructor() {
this.parts = [];
}
append(str) {
this.parts.push(str);
return this;
}
toString() {
return this.parts.join('');
}
}
}
// ================================
// 🐑 Sheepies Xanax&BloodBag Reminders
// ================================
function initBloodBagReminder(){
'use strict';
if (window.SHEEPIE_BLOODBAG) return;
window.SHEEPIE_BLOODBAG = true;
const CONFIG = {
// Icon settings
fullLifeIconId: 'tm-full-life-bloodbag',
bloodBagPng: 'https://i.postimg.cc/mkZ1T68H/blood-bag-2.png',
// Destination URLs
destinations: {
factionArmoury: 'https://www.torn.com/factions.php?step=your&type=1#/tab=armoury&start=0&sub=medical',
personalInventory: 'https://www.torn.com/item.php#medical-items',
},
// Blood bag mechanics
lifePerBag: 30, // Each bag uses 30% life
cooldownPerBagMs: 60 * 60 * 1000, // Each bag adds 1 hour cooldown
// Status icons selector (for inserting our icon)
statusIconsSelector: 'ul[class*="status-icons"]',
// Poll interval
pollMs: 2000,
};
const BANNER_ID = "sheepieBloodBagBanner";
const LINK = "https://www.torn.com/factions.php?step=your&type=1#/tab=armoury&start=0&sub=medical";
function checkForBloodBag() {
const icon = document.querySelector('img[src*="blood-bag-2.png"]');
if (!icon) return;
if (document.getElementById(BANNER_ID)) return;
const banner = document.createElement("div");
banner.id = BANNER_ID;
banner.textContent = "🩸 Blood bags ready — click to fill";
banner.style.position = "fixed";
banner.style.top = "15px";
banner.style.left = "50%";
banner.style.transform = "translateX(-50%)";
banner.style.background = "#8b0000";
banner.style.color = "white";
banner.style.padding = "10px 20px";
banner.style.fontSize = "16px";
banner.style.fontWeight = "bold";
banner.style.borderRadius = "8px";
banner.style.boxShadow = "0 4px 12px rgba(0,0,0,0.4)";
banner.style.zIndex = "999999";
banner.style.cursor = "pointer";
banner.style.transition = "opacity 0.5s ease";
banner.addEventListener("click", function () {
window.location.href = LINK;
});
document.body.appendChild(banner);
setTimeout(() => {
banner.style.opacity = "0";
setTimeout(() => banner.remove(), 500);
}, 3000);
}
// Because Torn loads dynamically, poll briefly
if (!window.SHEEPIE_BLOODBAG_BANNER) {
window.SHEEPIE_BLOODBAG_BANNER = true;
const interval = setInterval(() => {
checkForBloodBag();
}, 2500);
setTimeout(() => clearInterval(interval), 10000);
}
// ===== GM_* COMPATIBILITY (TornPDA support) =====
const safeGM = {
getValue: (key, defaultVal) => {
try {
return typeof GM_getValue === 'function' ? GM_getValue(key, defaultVal) : defaultVal;
} catch { return defaultVal; }
},
setValue: (key, val) => {
try {
if (typeof GM_setValue === 'function') GM_setValue(key, val);
} catch { /* ignore */ }
},
registerMenuCommand: (name, fn) => {
try {
if (typeof GM_registerMenuCommand === 'function') GM_registerMenuCommand(name, fn);
} catch { /* ignore */ }
}
};
// ===== SESSIONSTORAGE DATA EXTRACTION =====
function getSidebarData() {
try {
const key = Object.keys(sessionStorage).find(k => /sidebarData\d+/.test(k));
if (!key) return null;
return JSON.parse(sessionStorage.getItem(key));
} catch {
return null;
}
}
function getLifeFromStorage() {
const data = getSidebarData();
if (!data) return null;
// Life data is at data.bars.life with amount/max properties
const life = data?.bars?.life;
if (life && typeof life.amount === 'number' && typeof life.max === 'number') {
const pct = life.max > 0 ? Math.round((life.amount / life.max) * 100) : 0;
return { current: life.amount, max: life.max, pct };
}
return null;
}
function hmsToMs(hms) {
if (!hms) return 0;
const parts = hms.split(':').map(Number);
if (parts.length === 3) {
const [h, m, s] = parts;
return ((h * 60 + m) * 60 + s) * 1000;
}
return 0;
}
function getMedicalCooldownInfo() {
const data = getSidebarData();
if (!data) return null;
const med = data?.statusIcons?.icons?.medical_cooldown;
if (!med) return null;
const nowSec = Date.now() / 1000;
const remainingMs = Math.max(0, (med.timerExpiresAt - nowSec) * 1000);
const maxMs = hmsToMs(med.factionUpgrade);
return {
remainingMs,
maxMs,
freeMs: Math.max(0, maxMs - remainingMs)
};
}
// ===== SETTINGS =====
function getDestinationURL() {
const destination = safeGM.getValue('bloodBagDestination', 'factionArmoury');
return CONFIG.destinations[destination] || CONFIG.destinations.factionArmoury;
}
function getBagsToFill() {
const bags = safeGM.getValue('bloodBagCount', 3);
return Math.max(1, Math.min(3, bags)); // Clamp to 1-3
}
function getThresholds() {
const bags = getBagsToFill();
return {
lifePercent: bags * CONFIG.lifePerBag, // 30%, 60%, or 90%
cooldownBufferMs: (bags - 1) * CONFIG.cooldownPerBagMs // 0, 1hr, or 2hr buffer
};
}
function getOpenInNewTab() {
return safeGM.getValue('bloodBagNewTab', true); // Default: open in new tab
}
function openSettingsModal() {
// Remove existing modal if present
const existingModal = document.getElementById('bloodbag-settings-modal');
if (existingModal) existingModal.remove();
const currentDestination = safeGM.getValue('bloodBagDestination', 'factionArmoury');
const currentBags = getBagsToFill();
const currentNewTab = getOpenInNewTab();
const thresholds = getThresholds();
const settingsModal = document.createElement('div');
settingsModal.id = 'bloodbag-settings-modal';
settingsModal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 100000;
display: flex;
align-items: center;
justify-content: center;
`;
const getCooldownText = (bags) => {
if (bags === 1) return 'any room available';
return `${bags - 1}hr buffer available`;
};
settingsModal.innerHTML = `
<div style="
background: #2e2e2e;
border-radius: 10px;
width: 450px;
max-width: 90%;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
">
<div style="
background: linear-gradient(to bottom, #1a1a1a, #2a2a2a);
padding: 15px 20px;
border-radius: 10px 10px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
">
<h2 style="margin: 0; color: #fff; font-size: 18px;">Blood Bag Settings</h2>
<button id="close-bloodbag-settings" style="
background: none;
border: none;
color: #aaa;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
">x</button>
</div>
<div style="padding: 20px; color: #ccc;">
<div style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 5px; color: #aaa; font-size: 14px;">
Bags to Fill:
</label>
<select id="bloodbag-count" style="
width: 100%;
padding: 10px;
background: #1a1a1a;
border: 1px solid #444;
border-radius: 5px;
color: #fff;
font-size: 14px;
box-sizing: border-box;
">
<option value="1" ${currentBags === 1 ? 'selected' : ''}>1 bag (requires >30% life)</option>
<option value="2" ${currentBags === 2 ? 'selected' : ''}>2 bags (requires >60% life)</option>
<option value="3" ${currentBags === 3 ? 'selected' : ''}>3 bags (requires >90% life)</option>
</select>
<p style="font-size: 12px; color: #888; margin-top: 5px;">
How many blood bags do you want to fill at once?<br>
Each bag uses 30% life and adds 1hr medical cooldown.
</p>
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 5px; color: #aaa; font-size: 14px;">
Destination Page:
</label>
<select id="bloodbag-destination" style="
width: 100%;
padding: 10px;
background: #1a1a1a;
border: 1px solid #444;
border-radius: 5px;
color: #fff;
font-size: 14px;
box-sizing: border-box;
">
<option value="factionArmoury" ${currentDestination === 'factionArmoury' ? 'selected' : ''}>Faction Armoury (Medical)</option>
<option value="personalInventory" ${currentDestination === 'personalInventory' ? 'selected' : ''}>Personal Inventory (Medical)</option>
</select>
<p style="font-size: 12px; color: #888; margin-top: 5px;">
Where clicking the blood bag icon takes you.<br>
<em>Tip: Long-press the icon to open this settings panel.</em>
</p>
</div>
<div style="margin-bottom: 20px;">
<label style="display: flex; align-items: center; cursor: pointer; color: #ccc; font-size: 14px;">
<input type="checkbox" id="bloodbag-newtab" ${currentNewTab ? 'checked' : ''} style="
width: 18px;
height: 18px;
margin-right: 10px;
cursor: pointer;
">
Open in new tab
</label>
<p style="font-size: 12px; color: #888; margin-top: 5px; margin-left: 28px;">
When disabled, clicking the icon navigates in the same tab.
</p>
</div>
<div id="trigger-conditions" style="
background: rgba(255,255,255,0.05);
padding: 12px;
border-radius: 5px;
margin-bottom: 20px;
font-size: 12px;
color: #aaa;
">
<strong style="color: #ccc;">Current Trigger Conditions:</strong><br>
- Life above ${thresholds.lifePercent}%<br>
- Medical cooldown: ${getCooldownText(currentBags)}
</div>
<div style="text-align: right;">
<button id="cancel-bloodbag-settings" style="
background: linear-gradient(to bottom, #555, #777);
border: none;
color: white;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin-right: 10px;
font-size: 14px;
">Cancel</button>
<button id="save-bloodbag-settings" style="
background: linear-gradient(to bottom, #799427, #a3c248);
border: none;
color: white;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
">Save</button>
</div>
</div>
</div>
`;
// Update trigger conditions when bags selection changes
const updateTriggerDisplay = () => {
const bags = parseInt(document.getElementById('bloodbag-count').value, 10);
const lifeReq = bags * CONFIG.lifePerBag;
const conditionsDiv = document.getElementById('trigger-conditions');
if (conditionsDiv) {
conditionsDiv.innerHTML = `
<strong style="color: #ccc;">Current Trigger Conditions:</strong><br>
- Life above ${lifeReq}%<br>
- Medical cooldown: ${getCooldownText(bags)}
`;
}
};
document.body.appendChild(settingsModal);
// Event listeners
document.getElementById('bloodbag-count').addEventListener('change', updateTriggerDisplay);
document.getElementById('close-bloodbag-settings').addEventListener('click', () => settingsModal.remove());
document.getElementById('cancel-bloodbag-settings').addEventListener('click', () => settingsModal.remove());
document.getElementById('save-bloodbag-settings').addEventListener('click', () => {
const bags = parseInt(document.getElementById('bloodbag-count').value, 10);
const destination = document.getElementById('bloodbag-destination').value;
const newTab = document.getElementById('bloodbag-newtab').checked;
safeGM.setValue('bloodBagCount', bags);
safeGM.setValue('bloodBagDestination', destination);
safeGM.setValue('bloodBagNewTab', newTab);
settingsModal.remove();
updateIcon();
});
settingsModal.addEventListener('click', (e) => {
if (e.target === settingsModal) settingsModal.remove();
});
}
// Register settings menu command
safeGM.registerMenuCommand('Blood Bag Settings', openSettingsModal);
// ===== XANAX REMINDER =====
function checkXanaxReminder(){
const existing = document.getElementById("xanaxBanner");
if(existing) return;
// 🐑 wait before checking (lets Torn render)
setTimeout(() => {
const drugcd = document.querySelector("[aria-label^='Drug Cooldown:']");
// if cooldown exists → do nothing
if (drugcd) return;
// 🐑 double-check shortly after (prevents flicker)
setTimeout(() => {
const recheck = document.querySelector("[aria-label^='Drug Cooldown:']");
if (recheck) return;
// ✅ safe to show banner
const banner = document.createElement("div");
banner.id = "xanaxBanner";
banner.textContent = "💊 Take your fwickin Xanax";
banner.style.position = "fixed";
banner.style.top = "20px";
banner.style.left = "50%";
banner.style.transform = "translateX(-50%)";
banner.style.background = "#8b0000";
banner.style.color = "white";
banner.style.padding = "10px 20px";
banner.style.fontSize = "16px";
banner.style.fontWeight = "bold";
banner.style.borderRadius = "8px";
banner.style.boxShadow = "0 4px 12px rgba(0,0,0,0.4)";
banner.style.zIndex = "999999";
banner.style.cursor = "pointer";
banner.style.transition = "opacity 0.5s ease";
banner.onclick = () => {
window.location.href = "https://www.torn.com/item.php#drug-items";
};
document.body.appendChild(banner);
setTimeout(() => {
banner.style.opacity = "0";
setTimeout(() => banner.remove(), 500);
}, 30000);
}, 500); // second check
}, 1500); // initial delay (adjust if needed)
}
// ===== ICON MANAGEMENT =====
function updateIcon() {
const statusUl = document.querySelector(CONFIG.statusIconsSelector);
if (!statusUl) return;
const existing = document.getElementById(CONFIG.fullLifeIconId);
const life = getLifeFromStorage();
const med = getMedicalCooldownInfo();
const thresholds = getThresholds();
// Check conditions
// Life must be above threshold (30%, 60%, or 90% based on bags setting)
const lifeOk = life && life.pct > thresholds.lifePercent;
// Cooldown check: current < max - buffer
// For 1 bag: just need current < max (any room)
// For 2 bags: need current < max - 1hr
// For 3 bags: need current < max - 2hr
let cooldownOk = true;
if (med && med.maxMs > 0) {
// We have medical cooldown info
// Check if: remainingMs < maxMs - bufferMs
// Which means: we have enough room for all bags
cooldownOk = med.remainingMs < (med.maxMs - thresholds.cooldownBufferMs);
}
// If no medical cooldown icon exists, cooldownOk stays true (no cooldown = can use)
const shouldShow = lifeOk && cooldownOk;
if (shouldShow) {
// Build tooltip text
let label = `Life: ${formatNum(life.current)} / ${formatNum(life.max)} (${life.pct}%)`;
if (med && med.maxMs > 0) {
const remainHrs = Math.floor(med.remainingMs / 3600000);
const remainMin = Math.floor((med.remainingMs % 3600000) / 60000);
const maxHrs = Math.floor(med.maxMs / 3600000);
label += ` - CD: ${remainHrs}h${remainMin}m / ${maxHrs}h`;
} else {
label += ` - No medical cooldown`;
}
if (existing) {
updateIconTooltip(existing, label);
return;
}
const li = buildBloodBagIcon(label);
statusUl.appendChild(li);
} else if (existing) {
existing.remove();
}
}
function buildBloodBagIcon(tooltipText) {
const li = document.createElement('li');
li.id = CONFIG.fullLifeIconId;
li.style.background = 'none';
li.style.animation = 'tmPulse 900ms ease-out 1';
const a = document.createElement('a');
a.href = getDestinationURL();
if (getOpenInNewTab()) {
a.target = '_blank';
a.rel = 'noopener noreferrer';
}
a.setAttribute('aria-label', tooltipText);
a.tabIndex = 0;
a.setAttribute('data-is-tooltip-opened', 'false');
const img = document.createElement('img');
img.src = CONFIG.bloodBagPng;
img.alt = 'Blood Bag';
img.width = 17;
img.height = 17;
img.style.display = 'block';
a.appendChild(img);
li.appendChild(a);
// Long-press to open settings
setupLongPress(a, 500, openSettingsModal);
// Native-style tooltip
enableNativeLikeTooltip(a);
// Add pulse animation style if not present
if (!document.getElementById('tm-pulse-style')) {
const style = document.createElement('style');
style.id = 'tm-pulse-style';
style.textContent = `
@keyframes tmPulse {
0% { transform: scale(0.9); }
60% { transform: scale(1.1); }
100% { transform: scale(1.0); }
}
`;
document.head.appendChild(style);
}
return li;
}
function setupLongPress(element, duration, callback) {
let timer = null;
let didLongPress = false;
function startPress() {
didLongPress = false;
timer = setTimeout(() => {
didLongPress = true;
callback();
}, duration);
}
function cancelPress() {
clearTimeout(timer);
timer = null;
}
function endPress(e) {
clearTimeout(timer);
if (didLongPress) {
e.preventDefault();
e.stopPropagation();
didLongPress = false;
}
}
// Touch events (mobile/TornPDA)
element.addEventListener('touchstart', startPress, { passive: true });
element.addEventListener('touchend', endPress);
element.addEventListener('touchmove', cancelPress, { passive: true });
element.addEventListener('touchcancel', cancelPress);
// Mouse events (desktop)
element.addEventListener('mousedown', startPress);
element.addEventListener('mouseup', endPress);
element.addEventListener('mouseleave', cancelPress);
// Prevent click if long-press occurred
element.addEventListener('click', (e) => {
if (didLongPress) {
e.preventDefault();
e.stopPropagation();
didLongPress = false;
}
});
}
function updateIconTooltip(li, text) {
const a = li.querySelector('a');
if (!a) return;
a.href = getDestinationURL();
if (getOpenInNewTab()) {
a.target = '_blank';
a.rel = 'noopener noreferrer';
} else {
a.removeAttribute('target');
a.removeAttribute('rel');
}
a.setAttribute('aria-label', text);
if (typeof a.__tmUpdateTipText === 'function') a.__tmUpdateTipText(text);
}
// ===== TOOLTIP =====
function enableNativeLikeTooltip(anchor) {
let tipEl = null;
let hideTimer = null;
const CLS = {
tip: 'tooltip___aWICR tooltipCustomClass___gbI4V',
arrowWrap: 'arrow___yUDKb top___klE_Y',
arrowIcon: 'arrowIcon___KHyjw',
};
function buildTooltip(text) {
const el = document.createElement('div');
el.className = CLS.tip;
el.setAttribute('role', 'tooltip');
el.setAttribute('tabindex', '-1');
el.style.position = 'absolute';
el.style.transitionProperty = 'opacity';
el.style.transitionDuration = '200ms';
el.style.opacity = '0';
const [title, subtitle] = parseTwoLines(text);
const b = document.createElement('b');
b.textContent = title;
el.appendChild(b);
if (subtitle) {
const div = document.createElement('div');
div.textContent = subtitle;
el.appendChild(div);
}
const arrowWrap = document.createElement('div');
arrowWrap.className = CLS.arrowWrap;
const arrowIcon = document.createElement('div');
arrowIcon.className = CLS.arrowIcon;
arrowWrap.appendChild(arrowIcon);
el.appendChild(arrowWrap);
return el;
}
function setText(text) {
if (!tipEl) return;
const [title, subtitle] = parseTwoLines(text);
const b = tipEl.querySelector('b');
if (b) b.textContent = title;
let sub = b?.nextElementSibling;
if (subtitle) {
if (!sub || sub.tagName !== 'DIV') {
sub = document.createElement('div');
b.after(sub);
}
sub.textContent = subtitle;
} else if (sub) {
sub.remove();
}
}
function parseTwoLines(text) {
const parts = text.split(' - ');
if (parts.length >= 2) {
return [parts[0].trim(), parts[1].trim()];
}
return [text.trim(), ''];
}
function positionTooltip() {
if (!tipEl) return;
const r = anchor.getBoundingClientRect();
const ew = tipEl.offsetWidth;
const eh = tipEl.offsetHeight;
let left = Math.round(r.left + (r.width - ew) / 2);
let top = Math.round(r.top - eh - 14);
left = Math.max(8, Math.min(left, window.innerWidth - ew - 8));
if (top < 8) {
top = Math.round(r.bottom + 10);
}
tipEl.style.left = `${left}px`;
tipEl.style.top = `${top}px`;
const arrow = tipEl.querySelector(`.${CLS.arrowWrap.split(' ')[0]}`);
if (arrow) {
const iconCenter = r.left + r.width / 2;
const arrowLeft = Math.round(iconCenter - left - 6 + 14);
arrow.style.left = `${arrowLeft}px`;
}
}
function showTip() {
clearTimeout(hideTimer);
const text = anchor.getAttribute('aria-label');
if (!text) return;
if (!tipEl) {
tipEl = buildTooltip(text);
document.body.appendChild(tipEl);
anchor.__tmTipEl = tipEl;
} else {
setText(text);
}
anchor.setAttribute('data-is-tooltip-opened', 'true');
tipEl.style.opacity = '0';
tipEl.style.left = '-9999px';
tipEl.style.top = '-9999px';
requestAnimationFrame(() => {
positionTooltip();
requestAnimationFrame(() => {
if (tipEl) tipEl.style.opacity = '1';
});
});
}
function hideTip(immediate = false) {
if (!tipEl) return;
anchor.setAttribute('data-is-tooltip-opened', 'false');
if (immediate) {
tipEl.remove();
anchor.__tmTipEl = null;
tipEl = null;
return;
}
tipEl.style.opacity = '0';
hideTimer = setTimeout(() => {
tipEl?.remove();
anchor.__tmTipEl = null;
tipEl = null;
}, 210);
}
anchor.__tmUpdateTipText = (text) => setText(text);
anchor.addEventListener('mouseenter', showTip);
anchor.addEventListener('mouseleave', () => hideTip(false));
anchor.addEventListener('focus', showTip);
anchor.addEventListener('blur', () => hideTip(true));
window.addEventListener('scroll', () => hideTip(true), { passive: true });
}
// ===== CSS GUARDS =====
function ensureStyles() {
if (document.getElementById('tm-bloodbag-styles')) return;
const s = document.createElement('style');
s.id = 'tm-bloodbag-styles';
s.textContent = `
#${CONFIG.fullLifeIconId},
#${CONFIG.fullLifeIconId} a,
#${CONFIG.fullLifeIconId} img {
background: none !important;
background-image: none !important;
-webkit-mask: none !important;
mask: none !important;
box-shadow: none !important;
border: none !important;
}
#${CONFIG.fullLifeIconId}::before,
#${CONFIG.fullLifeIconId}::after,
#${CONFIG.fullLifeIconId} a::before,
#${CONFIG.fullLifeIconId} a::after {
content: none !important;
}
ul[class*="status-icons"] {
height: auto !important;
overflow: visible !important;
}
`;
document.head.appendChild(s);
}
// ===== UTILITIES =====
function formatNum(n) {
try {
return n.toLocaleString();
} catch {
return String(n);
}
}
// ===== INITIALIZATION =====
// Declare before anything can call scheduleCheck (fixes TDZ crash)
let checkScheduled = false;
function scheduleCheck() {
if (checkScheduled) return;
checkScheduled = true;
requestAnimationFrame(() => {
checkScheduled = false;
updateIcon();
checkXanaxReminder();
});
}
// One-time CSS injection (run immediately like v3.4)
ensureStyles();
// Observe DOM changes (SPA) and poll (like v3.4)
const mo = new MutationObserver(() => {
scheduleCheck();
});
mo.observe(document.documentElement, { childList: true, subtree: true });
setInterval(scheduleCheck, CONFIG.pollMs);
// Initial check
scheduleCheck();
}
// ================================
// 🐑 Torn Territory War Time Left by Ramin Quluzade, Silmaril [2665762]
// ================================
function initTerritoryWarTime(){
'use strict';
if (window.SHEEPIE_TERRITORY_TIMER) return;
window.SHEEPIE_TERRITORY_TIMER = true;
const targetElementSelector = '.f-war-list.war-new';
const observerOptions = { childList: true, subtree: true };
const observerCallback = async function(mutationsList, observer) {
for (const mutation of mutationsList) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
const targetElement = document.querySelector(targetElementSelector);
if (targetElement) {
let territoryWars = mutation.target.querySelectorAll(".f-war-list.war-new div[class^='status-wrap territoryBox']");
if (territoryWars.length > 0) {
territoryWars.forEach(war => {
war.querySelector('.info .faction-progress-wrap').style.paddingTop = '0px';
let timeLeftElement = document.createElement('div');
timeLeftElement.classList.add('time-left', 'timer');
let timeLeftBestElement = document.createElement('div');
timeLeftBestElement.classList.add('time-left-best', 'timer');
war.querySelector('.info .faction-progress-wrap').append(timeLeftElement, timeLeftBestElement);
});
territoryWars.forEach(war => {
let enemyCountDiv = war.querySelector('.info .member-count.enemy-count .count');
let allyCountDiv = war.querySelector('.info .member-count.your-count .count');
renderTimeLeft(war);
// Set up a MutationObserver on the added child element
const childObserver = new MutationObserver(function(childMutations) {
childMutations.forEach(function(childMutation) {
if (childMutation.type === 'characterData') {
let territoryWar = childMutation.target.parentNode.parentNode.parentNode.parentNode;
renderTimeLeft(territoryWar);
}
});
});
setInterval(renderTimeLeft, 1000 + Math.floor(Math.random() * 10) + 1, war);
childObserver.observe(enemyCountDiv, { characterData: true, subtree: true });
childObserver.observe(allyCountDiv, { characterData: true, subtree: true });
});
observer.disconnect();
}
}
}
}
};
const observer = new MutationObserver(observerCallback);
observer.observe(document.documentElement, observerOptions);
function renderTimeLeft(war) {
let enemyCountDiv = war.querySelector('.info .member-count.enemy-count .count');
let allyCountDiv = war.querySelector('.info .member-count.your-count .count');
let enemyCount = Number(enemyCountDiv.innerText);
let allyCount = Number(allyCountDiv.innerText);
let isAllyAttack = war.querySelector('.info .member-count.your-count .count i').classList.contains('swords-icon');
let remainder = isAllyAttack ? allyCount - enemyCount : enemyCount - allyCount;
let timeLeft = '??:??:??:??';
let timeLeftBest = '??:??:??:??';
let scoreText = war.querySelector('.info .faction-progress-wrap .score').innerText;
let score = scoreText.replaceAll(',', '').split('/');
let pointsLeft = Number(score[1]) - Number(score[0]);
let maximumSlots = Number(score[1]) / 50000;
if (remainder > 0) {
let secondsUntilGoal = pointsLeft / remainder;
timeLeft = convertSecondsToDHMS(secondsUntilGoal);
}
timeLeftBest = convertSecondsToDHMS(pointsLeft / maximumSlots);
let timeLeftDiv = war.querySelector('.info .faction-progress-wrap .time-left');
let timeLeftBestDiv = war.querySelector('.info .faction-progress-wrap .time-left-best');
const timeLeftCharacters = timeLeft.split('');
const timeLeftBestCharacters = timeLeftBest.split('');
const timeLeftSpanArray = ['CURRENT '];
timeLeftCharacters.forEach(char => {
const span = document.createElement('span');
span.textContent = char;
timeLeftSpanArray.push(span);
});
timeLeftDiv.replaceChildren(...timeLeftSpanArray);
const timeLeftBestSpanArray = ['BESTCASE '];
timeLeftBestCharacters.forEach(char => {
const span = document.createElement('span');
span.textContent = char;
timeLeftBestSpanArray.push(span);
});
timeLeftBestDiv.replaceChildren(...timeLeftBestSpanArray);
}
function convertSecondsToDHMS(seconds) {
if (seconds === Infinity){
return '??:??:??:??';
}
const oneDay = 86400; // number of seconds in a day
const oneHour = 3600; // number of seconds in an hour
const oneMinute = 60; // number of seconds in a minute
// Calculate the number of days, hours, minutes, and seconds
const days = Math.floor(seconds / oneDay);
const hours = Math.floor((seconds % oneDay) / oneHour);
const minutes = Math.floor((seconds % oneHour) / oneMinute);
const remainingSeconds = Math.round(seconds % oneMinute);
// Construct a formatted string with the results
let output = '';
output += `${days.toString().padStart(2, '0')}:`;
output += `${hours.toString().padStart(2, '0')}:`;
output += `${minutes.toString().padStart(2, '0')}:`;
output += `${remainingSeconds.toString().padStart(2, '0')}`;
return output;
}
}
// ================================
// 🐑 Sheepies FastPacks
// ================================
function initFastPacks(){
'use strict';
if (window.SHEEPIE_FastPacks) return;
window.SHEEPIE_FastPacks = true;
//Removes all images, leaving only the result text and buttons.
//This should prevent the "Use Another" button moving most of the time
let removeVisuals = true;
// cc Manuito
let GM_addStyle = function(s) {
let style = document.createElement("style");
style.type = "text/css";
style.innerHTML = s;
document.head.appendChild(style);
};
GM_addStyle(`
.d .pack-open-content.disabled-link .pack-open-msg a.open-another-cache {
pointer-events: auto !important;
cursor: default !important;
color: var(--default-blue-color) !important;
}
.d .pack-open-msg {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
.d .animated-fadeIn {
opacity: 1 !important;
}
.d .animated {
animation-duration: 0s !important;
}
.d .pack-open-result .cache-item:nth-child(2) {
animation-delay: 0s !important;
}
.d .pack-open-result .item-amount {
animation: none !important;
animation-delay: 0s !important;
opacity: 1 !important;
}
.r .pack-open-result {
animation-name: none !important;
animation-duration: 0s !important;
}
.d .pack-open-result-divider.visible {
transition: none !important;
}
`);
if(removeVisuals) {
GM_addStyle(`
.pack-open-result {
visibility: hidden !important;
height: 0px !important;
}
.d .cache_wrapper,
.d .pack-open-result-divider {
display: none !important
}
`);
}
}
// ================================
// 🐑 Sheepies Fast Slots
// ================================
function initFastSlots(){
'use strict';
if (window.SHEEPIE_Slots) return;
window.SHEEPIE_Slots = true;
const originalAjax = $.ajax;
$.ajax = function (options) {
if (options.data != null && options.data.sid == 'slotsData' && options.data.step == 'play') {
const originalSuccess = options.success;
options.success = function (data, textStatus, jqXHR) {
if (data.error) delete data.error;
if (data.errorMsg) delete data.errorMsg;
data.barrelsAnimationSpeed = 0;
if (originalSuccess) {
originalSuccess(data, textStatus, jqXHR);
}
};
}
return originalAjax(options);
}
function enableBetButtons() {
document.querySelectorAll(".slots-btn-list .betbtn").forEach(btn => {
btn.classList.remove("disabled");
});
}
function disableBetButtons() {
document.querySelectorAll(".slots-btn-list .betbtn").forEach(btn => {
btn.classList.add("disabled");
});
}
function watchBarrelsSpinAndStop(delay = 60) {
const barrels = document.querySelectorAll("#barrel0, #barrel1, #barrel2");
let timers = new Map();
let stopped = new Map();
barrels.forEach(barrel => stopped.set(barrel, true));
barrels.forEach(barrel => {
const observer = new MutationObserver(() => {
disableBetButtons();
stopped.set(barrel, false);
clearTimeout(timers.get(barrel));
timers.set(barrel, setTimeout(() => {
stopped.set(barrel, true);
if ([...stopped.values()].every(Boolean)) {
enableBetButtons();
}
}, delay));
});
observer.observe(barrel, {
attributes: true,
attributeFilter: ["style"]
});
});
}
var o = setInterval(() => {
if($('#barrels').length == 1){
clearInterval(o)
watchBarrelsSpinAndStop();
}
}, 100);
}
// ================================
// 🐑 Sheepies Propaganda Machine
// ================================
function initRecruitmentPanel(){
"use strict";
if (window.SHEEPIE_RECRUITMENT) return;
window.SHEEPIE_RECRUITMENT = true;
let apiKey = localStorage.getItem("sheepAPI") ?? '###PDA-APIKEY###';
/* -----------------------
AUTH + TOKEN
----------------------- */
function getToken(){
return new Promise((resolve) => {
let existing = localStorage.getItem("sheepie_token");
if(existing){
return resolve(existing);
}
GM_xmlhttpRequest({
method: "POST",
url: "https://www.sheepie.ca/intel-dashboard-public/auth.php",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
data: "key=" + encodeURIComponent(apiKey),
onload: function(res){
try {
const data = JSON.parse(res.responseText);
if(!data.success){
alert(data.error || "Auth failed");
return resolve(null);
}
localStorage.setItem("sheepie_token", data.token);
resolve(data.token);
} catch {
resolve(null);
}
},
onerror: function(){
resolve(null);
}
});
});
}
/* -----------------------------
SETTINGS
----------------------------- */
const sedition = {
bg: "#151520",
border: "#2a2a40",
accent: "#7c3aed",
glow: "#a855f7",
button: "#1c1c28",
hover: "#7c3aed",
text: "#e5e5f0"
};
/* -----------------------------
CREATE RECRUITMENT BUTTON
----------------------------- */
function createRecruitButton() {
const btn = document.createElement("div");
btn.id = "sheepieRecruitButton";
btn.innerHTML = '<img src="https://sheepie.ca/img/cult256.png" width="40">';
btn.title = "Spread Propaganda";
document.body.appendChild(btn);
btn.onclick = pasteRecruitMessage;
}
/* -----------------------------
PASTE RECRUIT MESSAGE
----------------------------- */
async function pasteRecruitMessage(){
console.log("Recruit paste triggered");
/* -----------------------------
GET PLAYER NAME
----------------------------- */
let name = "there";
try{
const nameElement = document.querySelector(
'span.root___xn40j.headingH1___RwTDm'
);
if(nameElement){
name = nameElement.textContent.trim();
}
}catch(e){
console.log("Could not detect name");
}
/* -----------------------------
GET CURRENT DAY
----------------------------- */
const days = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
];
const day = days[new Date().getDay()];
/* -----------------------------
FETCH MESSAGE LIST
----------------------------- */
let html;
html = await new Promise(async (resolve,reject)=>{
const token = await getToken();
if(!token){
console.log("Auth failed");
return reject();
}
GM_xmlhttpRequest({
method: "GET",
url: "https://www.sheepie.ca/intel-dashboard-public/getRecruitmentMessages.php?token=" + token,
onload: function(response){
resolve(response.responseText);
},
onerror: function(){
reject();
}
});
});
/* -----------------------------
PARSE <li> ITEMS
----------------------------- */
const parser = new DOMParser();
const doc = parser.parseFromString(html,"text/html");
const items = Array.from(doc.querySelectorAll("li"));
if(items.length === 0){
console.log("No recruitment messages found");
return;
}
/* -----------------------------
PICK RANDOM MESSAGE
----------------------------- */
const randomItem =
items[Math.floor(Math.random()*items.length)];
let message = randomItem.textContent;
/* -----------------------------
REPLACE VARIABLES
----------------------------- */
message = message.replaceAll("{name}",name);
message = message.replaceAll("{day}",day);
/* -----------------------------
FIND CHAT TEXTAREA
----------------------------- */
const textarea = document.querySelector(
'div.content___vtupF textarea'
);
if(!textarea){
console.log("Chat textarea not found");
return;
}
/* -----------------------------
INSERT MESSAGE
----------------------------- */
textarea.value = message + "!";
textarea.focus();
/* trigger Torn input detection */
textarea.dispatchEvent(
new Event("input",{bubbles:true})
);
console.log("Recruit message inserted");
}
/* -----------------------------
INDEXED DB
----------------------------- */
function openRecruitDB(){
return new Promise((resolve,reject)=>{
const req = indexedDB.open("sheepieRecruitDB",1);
req.onupgradeneeded = function(e){
const db = e.target.result;
if(!db.objectStoreNames.contains("messages")){
db.createObjectStore("messages",{keyPath:"id",autoIncrement:true});
}
};
req.onsuccess = function(e){
resolve(e.target.result);
};
req.onerror = function(){
reject();
};
});
}
// open dashboard
function openDashboard(){
const dash = window.open("", "_blank");
dash.document.write(`
<html>
<head>
<title>Sheepie Recruit Dashboard</title>
<style>
body{
background:#151520;
color:#e5e5f0;
font-family:Arial;
margin:0;
}
header{
padding:15px;
background:#1c1c28;
border-bottom:1px solid #2a2a40;
font-size:20px;
}
nav{
padding:10px;
border-bottom:1px solid #2a2a40;
}
button{
background:#1c1c28;
color:#e5e5f0;
border:1px solid #2a2a40;
padding:6px 12px;
margin-right:6px;
cursor:pointer;
}
button:hover{
background:#7c3aed;
}
.page{
padding:20px;
}
textarea{
width:100%;
height:200px;
background:#1c1c28;
color:#e5e5f0;
border:1px solid #2a2a40;
padding:10px;
}
.entry{
padding:8px;
border-bottom:1px solid #2a2a40;
display:flex;
justify-content:space-between;
}
</style>
</head>
<body>
<header>🐑 Sheepie Recruit Dashboard</header>
<nav>
<button onclick="showPage('add')">Add Message</button>
<button onclick="showPage('local')">IndexedDB Messages</button>
<button onclick="showPage('web')">Website Messages</button>
</nav>
<div id="page-add" class="page">
<h3>Add Recruitment Message</h3>
<textarea id="msgInput"></textarea>
<br><br>
<button id="addMsgBtn">Add To Database</button>
<div id="saveStatus" style="
margin-top:10px;
color:#7c3aed;
font-weight:bold;
display:none;
">
Saved ✓
</div>
</div>
<div id="page-local" class="page" style="display:none">
<h3>IndexedDB Messages</h3>
<div id="localList"></div>
</div>
<div id="page-web" class="page" style="display:none">
<h3>Website Messages</h3>
<div id="webList"></div>
</div>
<script>
function openRecruitDB(){
return new Promise((resolve,reject)=>{
const req = indexedDB.open("sheepieRecruitDB",1);
req.onupgradeneeded = function(e){
const db = e.target.result;
if(!db.objectStoreNames.contains("messages")){
db.createObjectStore("messages",{keyPath:"id",autoIncrement:true});
}
};
req.onsuccess = function(e){
resolve(e.target.result);
};
req.onerror = function(){
reject();
};
});
}
async function addMessage(){
console.log("Add button clicked");
const txt = document.getElementById("msgInput").value.trim();
if(!txt){
alert("Enter a message first");
return;
}
const entry = "<li>"+txt+"</li>";
const db = await openRecruitDB();
const tx = db.transaction("messages","readwrite");
const store = tx.objectStore("messages");
const request = store.add({text:entry});
request.onsuccess = function(){
/* clear textarea */
document.getElementById("msgInput").value="";
/* show confirmation */
const status = document.getElementById("saveStatus");
status.style.display="block";
setTimeout(()=>{
status.style.display="none";
},2000);
};
request.onerror = function(e){
console.error("Failed to save", e);
};
}
function showPage(p){
document.getElementById("page-add").style.display="none";
document.getElementById("page-local").style.display="none";
document.getElementById("page-web").style.display="none";
document.getElementById("page-"+p).style.display="block";
}
window.addEventListener("load", () => {
document.getElementById("addMsgBtn").onclick = addMessage;
});
</script>
</body>
</html>
`);
}
/* -----------------------------
CSS
----------------------------- */
function injectCSS() {
const style = document.createElement("style");
style.textContent = `
#sheepieRecruitToggle{
position:absolute;
top:40px;
right:100px;
background:#151520;
border:1px solid #2a2a40;
padding:4px 8px;
border-radius:6px;
color:#e5e5f0;
font-size:12px;
z-index:999999;
box-shadow:0 0 6px rgba(124,58,237,0.4);
}
/* Recruitment Button */
#sheepieRecruitButton{
position:fixed;
bottom:0px;
right:0px;
width:40px;
height:40px;
background:#1c1c28;
border:1px solid #2a2a40;
border-radius:6px;
display:flex;
align-items:center;
justify-content:center;
cursor:pointer;
z-index:999999;
box-shadow:0 0 6px rgba(124,58,237,0.35);
}
#sheepieRecruitButton:hover{
background:#7c3aed;
box-shadow:0 0 10px rgba(168,85,247,0.7);
}
`;
document.head.appendChild(style);
}
/* -----------------------------
INIT
----------------------------- */
function init(){
injectCSS();
createRecruitButton();
openRecruitDB();
}
if (document.readyState === "loading") {
window.addEventListener("load", init);
} else {
init();
}
}
// ================================
// 🐑 Sheepies Newsletter Template Injector
// ================================
function initTXTInjector(){
'use strict';
if (window.SHEEPIE_TXT_INJECTOR) return;
window.SHEEPIE_TXT_INJECTOR = true;
/* -----------------------
AUTH + TOKEN (SAFE)
----------------------- */
function getToken(){
return new Promise((resolve) => {
let existing = localStorage.getItem("sheepie_token");
if(existing){
return resolve(existing);
}
GM_xmlhttpRequest({
method: "POST",
url: "https://www.sheepie.ca/intel-dashboard-public/auth.php",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
data: "key=" + encodeURIComponent(apiKey),
onload: function(res){
try {
const data = JSON.parse(res.responseText);
if(!data.success){
alert(data.error || "Auth failed");
return resolve(null);
}
localStorage.setItem("sheepie_token", data.token);
resolve(data.token);
} catch {
resolve(null);
}
},
onerror: function(){
resolve(null);
}
});
});
}
const allowedUrls = [
"https://www.torn.com/messages.php#/p=compose",
"https://www.torn.com/factions.php?step=your#/tab=controls&option=newsletter",
"https://www.torn.com/messages.php",
"https://www.torn.com/factions.php?step=your&type=1#/tab=controls&option=newsletter"
];
if (!allowedUrls.includes(window.location.href)) return;
GM_addStyle(`
#sheepieInjectorBtn {
position: fixed;
bottom: 25px;
right: 25px;
z-index: 9999;
background: linear-gradient(145deg, #1a1a1a, #2b2b2b);
color: #f5f5f5;
border: 1px solid #666;
padding: 12px 18px;
border-radius: 12px;
font-weight: bold;
font-size: 14px;
cursor: pointer;
box-shadow: 0 0 12px rgba(255,255,255,0.08);
transition: all 0.2s ease;
}
#sheepieInjectorBtn:hover {
background: #333;
border-color: #aaa;
box-shadow: 0 0 18px rgba(255,255,255,0.18);
}
`);
const button = document.createElement("button");
button.id = "sheepieInjectorBtn";
button.textContent = "🐑 Inject TXT";
document.body.appendChild(button);
function getComposeTextarea() {
return document.querySelector("textarea.sourceArea___fQWHn");
}
function getNewsletterEditor() {
return document.querySelector("#mce_0.editor-content.mce-content-body.editorContent___uEg52");
}
function getComposeTitleInput() {
return document.querySelector("input.subject");
}
function getNewsletterTitleInput() {
return document.querySelector("input.text-input.titleField___Rtn72");
}
function getCurrentDay() {
const days = [
"Sunday","Monday","Tuesday","Wednesday",
"Thursday","Friday","Saturday"
];
return days[new Date().getDay()];
}
function extractTitle(text) {
const match = text.match(/<title>(.*?)<\/title>/i);
if (!match) return { title: null, cleaned: text };
let title = match[1].trim();
// replace ${day} with actual day
title = title.replace(/\$\{day\}/gi, getCurrentDay());
// remove the title tag from content
const cleaned = text.replace(/<title>.*?<\/title>/i, "").trim();
return { title, cleaned };
}
function injectTitle(title) {
if (!title) return;
const composeTitle = getComposeTitleInput();
const newsletterTitle = getNewsletterTitleInput();
if (composeTitle) {
composeTitle.value = title;
composeTitle.dispatchEvent(new Event("input", { bubbles: true }));
}
if (newsletterTitle) {
if (newsletterTitle.value.includes("Faction Newsletter")) {
newsletterTitle.value = "";
}
newsletterTitle.value = title;
newsletterTitle.dispatchEvent(new Event("input", { bubbles: true }));
}
}
function fetchAndInject() {
return new Promise((resolve) => {
getToken().then((token) => {
if(!token){
alert("Auth failed.");
resolve(false);
return;
}
GM_xmlhttpRequest({
method: "GET",
url: "https://www.sheepie.ca/intel-dashboard-public/getPlayerFile.php?token=" + token,
onload: function (response) {
if (response.status !== 200) {
console.warn("Fetch failed:", response.status);
alert("Failed to load your data.");
resolve(false);
return;
}
let raw = response.responseText;
if (!raw || raw.trim().length < 5 || raw.toLowerCase().includes("error")) {
raw = "🐑 Contact SheepPrincess[2679129] to purchase this upgrade for your faction";
}
const { title, cleaned } = extractTitle(raw);
const finalContent = cleaned || "🐑 Contact SheepPrincess[2679129] to purchase this upgrade for your faction";
const composeBox = getComposeTextarea();
const newsletterBox = getNewsletterEditor();
if (composeBox) {
composeBox.value = finalContent;
composeBox.dispatchEvent(new Event("input", { bubbles: true }));
injectTitle(title);
resolve(true);
return;
}
if (newsletterBox) {
newsletterBox.innerHTML = finalContent;
newsletterBox.dispatchEvent(new Event("input", { bubbles: true }));
injectTitle(title);
resolve(true);
return;
}
localStorage.removeItem("sheepie_token");
alert("Editor not found.");
resolve(false);
},
onerror: function () {
localStorage.removeItem("sheepie_token");
alert("Failed to load your data.");
resolve(false);
}
});
});
});
}
button.addEventListener("click", async () => {
const success = await fetchAndInject();
if (!success) {
alert("Injection failed.");
}
});
}
// ================================
// 🐑 Sheepies Disappointment Calculator
// ================================
function initDisappointmentCalc(){
if (window.SHEEPIE_DISAPPOINTMENT) return;
window.SHEEPIE_DISAPPOINTMENT = true;
if (!window.location.href.includes("preferences.php")) return;
/* -----------------------
PAYWALL CHECK
----------------------- */
function validateKey(apiKey){
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "POST",
url: "https://www.sheepie.ca/intel-dashboard-public/auth.php",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
data: "key=" + encodeURIComponent(apiKey),
onload: function(res){
try{
const data = JSON.parse(res.responseText);
if(!data.success){
alert(data.error || "Access denied");
return resolve(false);
}
resolve(true);
}catch{
alert("Auth failed");
resolve(false);
}
},
onerror: function(){
alert("Auth request failed");
resolve(false);
}
});
});
}
/* --------------------------
CREATE BUTTON
-------------------------- */
const btn = document.createElement("button")
btn.innerText = "Get Disappointed In Your Life"
btn.style.position = "fixed"
btn.style.bottom = "40px"
btn.style.right = "40px"
btn.style.padding = "10px 16px"
btn.style.background = "#734180"
btn.style.color = "white"
btn.style.border = "1px solid #666"
btn.style.borderRadius = "6px"
btn.style.cursor = "pointer"
btn.style.zIndex = 999
document.body.appendChild(btn)
/* --------------------------
BUTTON CLICK
-------------------------- */
btn.onclick = async function(){
const apiKey = prompt("Enter your FULL ACCESS API key, we dont save any data, only check your active cult subscription and add up your logs")?.trim()
if(!apiKey) return
// 🔒 PAYWALL CHECK
const valid = await validateKey(apiKey)
if(!valid) return
const overlay = createLoader()
try{
const total = await start(apiKey, overlay)
overlay.remove()
alert(`Total Money Wasted: $${total.toFixed(2)} USD`)
}catch(e){
overlay.remove()
alert("Error: " + e.message)
}
}
/* --------------------------
LOADER UI
-------------------------- */
function createLoader(){
const overlay = document.createElement("div")
overlay.style.position = "fixed"
overlay.style.top = "0"
overlay.style.left = "0"
overlay.style.width = "100%"
overlay.style.height = "100%"
overlay.style.background = "rgba(0,0,0,0.85)"
overlay.style.zIndex = 99999999
overlay.style.display = "flex"
overlay.style.flexDirection = "column"
overlay.style.alignItems = "center"
overlay.style.justifyContent = "center"
overlay.style.color = "white"
overlay.style.fontSize = "18px"
overlay.innerHTML = `
<div>Loading payment logs...</div>
<div style="width:400px;height:20px;background:#333;margin-top:20px;border:1px solid #666">
<div id="sheepieBar" style="height:100%;width:0%;background:#6cc644"></div>
</div>
<div id="sheepieText" style="margin-top:10px">0 payments processed</div>
`
document.body.appendChild(overlay)
return overlay
}
const wait = ms => new Promise(r=>setTimeout(r,ms))
/* --------------------------
MAIN LOGIC
-------------------------- */
async function start(apiKey, overlay){
let url = `https://api.torn.com/v2/user/log?cat=72&limit=100&key=${apiKey}`
let total = 0
let seen = new Set()
let processed = 0
const bar = overlay.querySelector("#sheepieBar")
const text = overlay.querySelector("#sheepieText")
while(url){
console.log("Pulling:", url)
const res = await fetch(url)
const json = await res.json()
if(!json.log) throw new Error("API returned no log data")
for(const entry of json.log){
if(seen.has(entry.id)) continue
seen.add(entry.id)
const title = entry.details?.title
if(
title !== "Donation success" &&
title !== "Subscription success"
) continue
if(entry.data?.value){
const match = entry.data.value.match(/\(\$(.*?)\)/)
if(match){
total += parseFloat(match[1])
}
}
processed++
}
bar.style.width = Math.min(100, processed/50) + "%"
text.innerText = `${processed} payments processed`
let next = json?._metadata?.links?.prev
if(next){
url = next + `&key=${apiKey}`
await wait(800) // respect rate limits
}else{
url = null
}
}
return total
}
}
// ================================
// 🐑 Sheepies ChainWatch Timer
// ================================
function initChainTimer(){
if (window.SHEEPIE_CHAIN_TIMER) return;
window.SHEEPIE_CHAIN_TIMER = true;
const MAX_MS = 5 * 60 * 1000;
let timerMs = null;
let lastUpdate = Date.now();
let isFocused = document.hasFocus();
/* -----------------------
CREATE UI
----------------------- */
const timer = document.createElement("div");
timer.style.position = "fixed";
timer.style.top = "120px";
timer.style.right = "20px";
timer.style.padding = "8px 14px";
timer.style.background = "black";
timer.style.border = "2px solid green";
timer.style.borderRadius = "8px";
timer.style.color = "white";
timer.style.fontWeight = "bold";
timer.style.fontSize = "120px";
timer.style.zIndex = "999999";
timer.style.userSelect = "none";
timer.style.cursor = "move";
timer.style.display = "none";
document.body.appendChild(timer);
/* -----------------------
DRAG
----------------------- */
let drag = false, ox = 0, oy = 0;
timer.onmousedown = e => {
drag = true;
ox = e.clientX - timer.offsetright;
oy = e.clientY - timer.offsetTop;
};
document.onmouseup = () => drag = false;
document.onmousemove = e => {
if(drag){
timer.style.right = (e.clientX - ox) + "px";
timer.style.top = (e.clientY - oy) + "px";
}
};
/* -----------------------
HELPERS
----------------------- */
function format(ms){
const s = Math.floor(ms/1000);
const m = Math.floor(s/60);
const sec = s % 60;
return `${m.toString().padStart(2,"0")}:${sec.toString().padStart(2,"0")}`;
}
function getColor(ms){
const p = ms / MAX_MS;
if(p > 0.66) return "green";
if(p > 0.33) return "yellow";
if(p > 0.15) return "orange";
return "red";
}
function applyPanicEffects(ms){
if(!timer) return;
const seconds = ms / 1000;
// reset defaults
timer.style.transform = "scale(1)";
timer.style.boxShadow = "none";
// --- < 120s (2 min) → subtle pulse
if(seconds < 120){
const scale = 1 + (120 - seconds) / 600; // up to +0.2
timer.style.transform = `scale(${scale})`;
}
// --- < 60s → glow
if(seconds < 60){
const intensity = Math.min(1, (60 - seconds) / 60);
timer.style.boxShadow = `0 0 ${10 + intensity*20}px rgba(255,165,0,0.8)`;
}
// --- < 30s → shake
if(seconds < 30){
const shake = Math.sin(Date.now() / 50) * (30 - seconds) * 0.3;
timer.style.transform += ` translateX(${shake}px)`;
}
// --- < 10s → aggressive flash
if(seconds < 10){
const flash = Math.floor(Date.now() / 200) % 2;
timer.style.background = flash ? "darkred" : "black";
timer.style.color = "red";
timer.style.boxShadow = "0 0 30px red";
} else {
timer.style.background = "black";
}
}
function readDOMTimer(){
const el = document.querySelector('.bar-timeleft___B9RGV');
if(!el) return null;
const text = el.textContent.trim();
if(!text.includes(":")) return null;
const [m, s] = text.split(":").map(Number);
if(isNaN(m) || isNaN(s)) return null;
return (m*60 + s) * 1000;
}
/* -----------------------
VISIBILITY CONTROL
----------------------- */
document.addEventListener("visibilitychange", () => {
isFocused = document.visibilityState === "visible";
});
/* -----------------------
MAIN LOOP
----------------------- */
setInterval(() => {
const now = Date.now();
const delta = now - lastUpdate;
lastUpdate = now;
// 🟢 ONLY sync when focused
if(isFocused){
const domTime = readDOMTimer();
if(domTime !== null){
timerMs = domTime;
}
}
if(timerMs !== null){
if(!isFocused){
timerMs -= delta; // ONLY decay when unfocused
}
if(timerMs <= 0){
timerMs = null;
timer.style.display = "none";
return;
}
timer.style.display = "block";
timer.innerText = format(timerMs);
const color = getColor(timerMs);
timer.style.borderColor = color;
timer.style.color = color;
applyPanicEffects(timerMs);
}
}, 100);
}
// ================================
// 🐑 Sheepies SheepieClicker
// ================================
function initSheepieClicker(){
if (window.SHEEPIE_CLICKER) return;
window.SHEEPIE_CLICKER = true;
if(document.getElementById("sheepiePanel")) return
/* --------------------------
TAB LOCK
-------------------------- */
const sheepieTabId = Date.now()+"-"+Math.random()
const lock = GM_getValue("sheepieActiveTab")
const lockTime = GM_getValue("sheepieActiveTabTime",0)
/* lock expires after 10 seconds */
const lockExpired = Date.now() - lockTime > 10000
const sheepieIsOwner = !lock || lockExpired || lock === sheepieTabId
if(sheepieIsOwner){
GM_setValue("sheepieActiveTab", sheepieTabId)
GM_setValue("sheepieActiveTabTime", Date.now())
}
/* --------------------------
SECOND TAB PAGE
-------------------------- */
const sheepieAlreadyOpenPage = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body{
margin:0;
background:#111;
color:#fff;
font-family:sans-serif;
display:flex;
align-items:center;
justify-content:center;
height:100vh;
text-align:center;
}
.box{
background:#1b1b1b;
border:1px solid #444;
border-radius:10px;
padding:30px;
box-shadow:0 0 20px rgba(0,0,0,0.6);
}
img{
width:120px;
margin-bottom:15px;
}
</style>
</head>
<body>
<div class="box">
<img src="https://sheepie.ca/sheepieclicker/golden_sheep.png">
<h2>🐑 SheepieClicker is already running</h2>
<p>Return to the original tab.</p>
</div>
</body>
</html>
`
/* --------------------------
PANEL STATE
-------------------------- */
const saved = JSON.parse(localStorage.getItem("sheepiePanelState") || "{}")
const startWidth = saved.width || 420
const startHeight = saved.height || 640
const startRight = saved.right || 20
const startBottom = saved.bottom || 20
const startMinimized = saved.minimized || false
/* --------------------------
CREATE PANEL
-------------------------- */
const panel = GM_addElement(document.body,"div",{id:"sheepiePanel"})
panel.innerHTML = `
<div id="sheepieHeader">
🐑 SheepieClicker
<span>
<span id="sheepieMin">—</span>
<span id="sheepieClose">✕</span>
</span>
</div>
`
const iframe = GM_addElement(panel,"iframe",{
src:"https://sheepie.ca/sheepieclicker/sheepieclicker.html"
})
/* --------------------------
IFRAME LOAD
-------------------------- */
iframe.addEventListener("load",()=>{
try{
if(!sheepieIsOwner){
iframe.src="about:blank"
iframe.addEventListener("load",()=>{
const doc=iframe.contentDocument
doc.open()
doc.write(sheepieAlreadyOpenPage)
doc.close()
},{once:true})
return
}
const save = GM_getValue("sheepieSave")
if(save){
iframe.contentWindow.localStorage.setItem("sheepieSave",save)
}
}catch(e){}
})
/* --------------------------
SAVE BACKUP
-------------------------- */
setInterval(()=>{
try{
const save = iframe.contentWindow?.localStorage?.getItem("sheepieSave")
if(save){
GM_setValue("sheepieSave",save)
}
}catch(e){}
},5000)
/* keep tab lock alive */
setInterval(()=>{
if(sheepieIsOwner){
GM_setValue("sheepieActiveTabTime", Date.now())
}
},5000)
/* --------------------------
RESIZER
-------------------------- */
const resizer = GM_addElement(panel,"div",{id:"sheepieResize"})
/* --------------------------
HEADER BUTTONS
-------------------------- */
document.getElementById("sheepieClose").onclick=()=>panel.remove()
const minBtn=document.getElementById("sheepieMin")
if(startMinimized){
panel.classList.add("minimized")
}
/* --------------------------
PANEL CLAMP
-------------------------- */
function clampPanel(){
const rect=panel.getBoundingClientRect()
let right=parseInt(getComputedStyle(panel).right)||0
let bottom=parseInt(getComputedStyle(panel).bottom)||0
const overflowX=rect.right-window.innerWidth
const overflowY=rect.bottom-window.innerHeight
const overflowLeft=rect.left
const overflowTop=rect.top
if(overflowX>0) right+=overflowX
if(overflowY>0) bottom+=overflowY
if(overflowLeft<0) right+=overflowLeft
if(overflowTop<0) bottom+=overflowTop
panel.style.right=right+"px"
panel.style.bottom=bottom+"px"
}
/* --------------------------
SAVE PANEL STATE
-------------------------- */
function saveState(){
const minimized=panel.classList.contains("minimized")
const state=JSON.parse(localStorage.getItem("sheepiePanelState")||"{}")
if(!minimized){
state.width=panel.offsetWidth
state.height=panel.offsetHeight
}
state.right=parseInt(getComputedStyle(panel).right)||20
state.bottom=parseInt(getComputedStyle(panel).bottom)||20
state.minimized=minimized
localStorage.setItem("sheepiePanelState",JSON.stringify(state))
}
/* --------------------------
MINIMIZE
-------------------------- */
minBtn.onclick=()=>{
const minimizing=!panel.classList.contains("minimized")
const currentHeight=panel.offsetHeight
const headerHeight=34
let bottom=parseInt(getComputedStyle(panel).bottom)||20
if(minimizing){
bottom+=(currentHeight-headerHeight)
}else{
bottom-=(panel.dataset.preMinHeight-headerHeight)
}
panel.style.bottom=bottom+"px"
if(minimizing){
panel.dataset.preMinHeight=currentHeight
}
panel.classList.toggle("minimized")
clampPanel()
saveState()
}
/* --------------------------
DRAGGING
-------------------------- */
const header=document.getElementById("sheepieHeader")
let dragging=false
let dragStartX,dragStartY
let dragStartRight,dragStartBottom
header.addEventListener("mousedown",e=>{
if(e.target.id==="sheepieClose"||e.target.id==="sheepieMin") return
dragging=true
dragStartX=e.clientX
dragStartY=e.clientY
dragStartRight=parseInt(getComputedStyle(panel).right)
dragStartBottom=parseInt(getComputedStyle(panel).bottom)
document.body.style.userSelect="none"
})
window.addEventListener("mousemove",e=>{
if(!dragging) return
let newRight=dragStartRight-(e.clientX-dragStartX)
let newBottom=dragStartBottom-(e.clientY-dragStartY)
const panelWidth=panel.offsetWidth
const panelHeight=panel.offsetHeight
const maxRight=window.innerWidth-panelWidth
const maxBottom=window.innerHeight-panelHeight
newRight=Math.max(0,Math.min(newRight,maxRight))
newBottom=Math.max(0,Math.min(newBottom,maxBottom))
panel.style.right=newRight+"px"
panel.style.bottom=newBottom+"px"
clampPanel()
})
window.addEventListener("mouseup",()=>{
if(!dragging) return
dragging=false
document.body.style.userSelect=""
saveState()
})
/* --------------------------
RESIZE
-------------------------- */
let resizing=false
let startX,startY,startW,startH
resizer.addEventListener("mousedown",e=>{
resizing=true
startX=e.clientX
startY=e.clientY
startW=panel.offsetWidth
startH=panel.offsetHeight
dragStartRight=parseInt(getComputedStyle(panel).right)
dragStartBottom=parseInt(getComputedStyle(panel).bottom)
document.body.style.userSelect="none"
})
window.addEventListener("mousemove",e=>{
if(!resizing) return
const dx=e.clientX-startX
const dy=e.clientY-startY
const newW=startW+dx
const newH=startH+dy
panel.style.width=newW+"px"
panel.style.height=newH+"px"
panel.style.right=(dragStartRight-dx)+"px"
panel.style.bottom=(dragStartBottom-dy)+"px"
clampPanel()
})
window.addEventListener("mouseup",()=>{
if(!resizing) return
resizing=false
document.body.style.userSelect=""
saveState()
})
/* --------------------------
STYLE
-------------------------- */
GM_addStyle(`
#sheepiePanel{
position:fixed;
right:${startRight}px;
bottom:${startBottom}px;
width:${startWidth}px;
height:${startHeight}px;
background:#111;
border:1px solid #444;
border-radius:10px;
z-index:999999;
box-shadow:0 0 20px rgba(0,0,0,0.7);
overflow:hidden;
}
#sheepieHeader{
background:#222;
color:#fff;
padding:8px 10px;
font-weight:bold;
font-size:14px;
display:flex;
justify-content:space-between;
align-items:center;
cursor:move;
}
#sheepieClose{
cursor:pointer;
opacity:0.7;
}
#sheepieClose:hover{
opacity:1;
}
#sheepiePanel iframe{
width:149.25%;
height:149.25%;
border:none;
transform:scale(0.67);
transform-origin:top left;
}
#sheepieResize{
position:absolute;
right:0;
bottom:0;
width:16px;
height:16px;
cursor:nwse-resize;
background:linear-gradient(135deg,transparent 50%,#666 50%);
}
#sheepieMin{
cursor:pointer;
opacity:0.7;
margin-right:6px;
}
#sheepiePanel.minimized iframe{
display:none;
}
#sheepiePanel.minimized{
height:34px !important;
overflow:hidden;
}
`)
/* --------------------------
RELEASE LOCK
-------------------------- */
window.addEventListener("beforeunload",()=>{
try{
const lock=GM_getValue("sheepieActiveTab")
if(lock===sheepieTabId){
GM_deleteValue("sheepieActiveTab")
}
}catch(e){}
})
setTimeout(clampPanel,0)
}
// ================================
// 🐑 Sheepies Custom Buttons
// ================================
function createSheepieButton({ id, label, bottom, left, storageKey }) {
if (document.getElementById(id)) return;
const btn = document.createElement("button");
btn.id = id;
btn.innerText = label;
btn.style.position = "fixed";
btn.style.bottom = bottom + "px";
btn.style.left = left + "px";
btn.style.background = sheepColour;
btn.style.color = "white";
btn.style.border = "1px solid purple";
btn.style.cursor = "pointer";
btn.style.zIndex = "999999";
// ✅ TEXT-BASED SCALING
btn.style.fontSize = "clamp(10px, 1.2vw, 14px)";
btn.style.padding = "2px 6px";
btn.onclick = () => {
// 🔴 RESET BUTTON
if (storageKey === "__resetall__") {
if (!confirm("Reset Script?")) return;
localStorage.removeItem("sheeplink1");
localStorage.removeItem("sheeplink2");
localStorage.removeItem("sheepAPI");
localStorage.removeItem("sheepie_modules")
localStorage.removeItem("sheepie_token")
localStorage.removeItem("sheepiePanelState");
alert("Script reset");
window.location.reload();
return;
}
// 🔴 RESET BUTTON
if (storageKey === "__reset__") {
if (!confirm("Reset saved links?")) return;
localStorage.removeItem("sheeplink1");
localStorage.removeItem("sheeplink2");
alert("Links reset");
return;
}
// 🟣 STATIC BUTTON (FF Scout)
if (!storageKey) {
window.open(
'https://ffscouter.com/target-finder?preset=custom&liveUpdates=0&minLevel=1&maxLevel=100&minFF=2&maxFF=2.3&limit=20&inactiveOnly=1&factionless=1',
'FFSCouter'
).focus();
return;
}
// 🔵 NORMAL CUSTOM BUTTON
let link = localStorage.getItem(storageKey);
if (!link) {
link = prompt("Enter link (include https://)");
if (!link) return;
localStorage.setItem(storageKey, link);
}
window.open(link, "_blank").focus();
};
document.body.appendChild(btn);
}
function initCustomButtons(){
if (window.SHEEPIE_CUSTOM_BTNS) return;
window.SHEEPIE_CUSTOM_BTNS = true;
const base = 40;
const gap = 20;
createSheepieButton({
id: "sheepie-btn-1",
label: "Link 1",
bottom: base,
left: 40,
storageKey: "sheeplink1"
});
createSheepieButton({
id: "sheepie-btn-2",
label: "Link 2",
bottom: base + gap,
left: 40,
storageKey: "sheeplink2"
});
createSheepieButton({
id: "sheepie-btn-ffscouter",
label: "FFScouter",
bottom: base + gap + gap,
left: 40, // adjust position if needed
storageKey: null
});
createSheepieButton({
id: "sheepie-btn-reset",
label: "Reset Links",
bottom: base + gap + gap + gap,
left: 40, // adjust position as needed
storageKey: "__reset__"
});
createSheepieButton({
id: "sheepie-btn-resetall",
label: "Reset Script",
bottom: base + gap + gap + gap + gap,
left: 40, // adjust position as needed
storageKey: "__resetall__"
});
}
// ================================
// 🐑 Sheepies AIO Dashboard&Ticker
// ================================
function formatRemaining(ts){
const diff = ts - Math.floor(Date.now() / 1000);
if(diff <= 0) return "Expired";
const d = Math.floor(diff / 86400);
const h = Math.floor((diff % 86400) / 3600);
const m = Math.floor((diff % 3600) / 60);
return `${d}d ${h}h ${m}m`;
}
(function(){
'use strict';
/* -----------------------
STORAGE
----------------------- */
const STORAGE_KEY = "sheepie_modules";
function getModules(){
return JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");
}
function setModules(mods){
localStorage.setItem(STORAGE_KEY, JSON.stringify(mods));
}
/* -----------------------
MODULE REGISTRY
----------------------- */
const modules = {};
registerModule("OC Helper", initOCTracker);
registerModule("Chain Timer(only on pc)", initChainTimer);
registerModule("GhettoYT(only on pc)", initYouTube);
registerModule("Timezone Helper", initTimezoneEngine);
registerModule("Scamming/Pickpocket Helper", initCrimeMorale);
registerModule("Market Pricer", initMarketFiller);
registerModule("Reminders", initBloodBagReminder);
registerModule("Remaining Wall Estimates", initTerritoryWarTime);
registerModule("Fast Packs", initFastPacks);
registerModule("Fast Slots", initFastSlots);
registerModule("Propaganda Machine(Requires Subscription)", initRecruitmentPanel);
registerModule("Newsletter Templates(Requires Subscription)", initTXTInjector);
registerModule("disappointmentCalc(Requires Subscription)", initDisappointmentCalc);
registerModule("SheepieClicker(only on pc)", initSheepieClicker);
registerModule("Custom Buttons", initCustomButtons);
/* register modules later */
function registerModule(name, fn){
modules[name] = fn;
}
/* -----------------------
RUN ENABLED MODULES
----------------------- */
function runModules(){
const enabled = getModules();
for(const name in modules){
if(enabled[name]){
try{
modules[name]();
}catch(e){
console.error("Module failed:", name, e);
}
}
}
}
/* -----------------------
DASHBOARD UI
----------------------- */
function createDashboard(){
const btn = document.createElement("button");
btn.textContent = "🐑";
btn.style.position = "absolute";
btn.style.top = "20px";
btn.style.left = "20px";
btn.style.zIndex = "999999";
btn.style.fontSize = "clamp(10px, 1.2vw, 14px)";
btn.style.background = sheepColour;
btn.style.color = "white";
btn.style.border = "1px solid #555";
btn.style.borderRadius = "8px";
document.body.appendChild(btn);
const panel = document.createElement("div");
panel.style.position = "absolute";
panel.style.top = "60px";
panel.style.left = "20px";
panel.style.background = "#1a1a22";
panel.style.border = "1px solid #444";
panel.style.padding = "10px";
panel.style.display = "none";
panel.style.zIndex = "999999";
panel.style.width = "220px";
panel.style.color = "white";
document.body.appendChild(panel);
btn.onclick = () => {
panel.style.display = panel.style.display === "none" ? "block" : "none";
renderPanel();
};
function renderPanel(){
panel.innerHTML = "<b>Modules</b><br><br>";
const enabled = getModules();
// 🐑 SAVE BUTTON (define BEFORE use)
const saveBtn = document.createElement("button");
saveBtn.textContent = "💾 Save & Apply";
saveBtn.style.marginTop = "10px";
saveBtn.style.width = "100%";
saveBtn.style.padding = "6px";
saveBtn.style.background = sheepColour;
saveBtn.style.color = "white";
saveBtn.style.border = "1px solid #555";
saveBtn.style.borderRadius = "6px";
saveBtn.style.cursor = "pointer";
saveBtn.onclick = () => {
location.reload();
};
for(const name in modules){
const row = document.createElement("div");
const toggle = document.createElement("input");
toggle.type = "checkbox";
toggle.checked = !!enabled[name];
toggle.onchange = () => {
enabled[name] = toggle.checked;
setModules(enabled);
if(saveBtn){
saveBtn.textContent = "⚠ Apply Changes";
}
};
const label = document.createElement("span");
label.textContent = " " + name;
row.appendChild(toggle);
row.appendChild(label);
panel.appendChild(row);
}
panel.appendChild(saveBtn);
}
}
/* -----------------------
ALWAYS-ON: NEWS TICKER
----------------------- */
function initTicker(){
/* -----------------------
AUTH (SAME AS OTHER SCRIPTS)
----------------------- */
function authenticate(){
return new Promise((resolve) => {
let apiKey = localStorage.getItem("sheepAPI") ?? '###PDA-APIKEY###';
if(!apiKey){
return resolve(null);
}
GM_xmlhttpRequest({
method: "POST",
url: "https://www.sheepie.ca/intel-dashboard-public/auth.php",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
data: "key=" + encodeURIComponent(apiKey),
onload: function(res){
try{
const data = JSON.parse(res.responseText);
const expires = data.expires || 0;
if(!data.success){
return resolve(null);
}
localStorage.setItem("sheepie_token", data.token);
resolve(data.token);
}catch(e){
console.error("Auth parse error", e);
resolve(null);
}
},
onerror: function(){
resolve(null);
}
});
});
}
function getToken(){
return new Promise((resolve) => {
let token = localStorage.getItem("sheepie_token");
if(token){
return resolve(token);
}
authenticate().then(resolve);
});
}
if(document.getElementById("sheepie-ticker")) return;
const ticker = document.createElement("div");
ticker.id = "sheepie-ticker";
ticker.style.background = sheepColour;
ticker.style.color = "white";
ticker.style.padding = "5px";
ticker.style.display = "flex";
ticker.style.justifyContent = "center";
ticker.style.gap = "10px";
ticker.style.fontWeight = "bold";
ticker.style.alignItems = "center";
ticker.style.fontSize = "clamp(8px, 1.2vw, 14px)";
ticker.style.position = "relative";
ticker.style.width = "100%";
ticker.style.overflow = "hidden";
document.body.prepend(ticker);
const content = document.createElement("div");
content.style.display = "flex";
content.style.maxWidth = "100%";
content.style.overflow = "hidden";
content.style.gap = "10px";
content.style.alignItems = "center";
ticker.appendChild(content);
const expiryEl = document.createElement("div");
expiryEl.style.position = "absolute";
expiryEl.style.top = "0";
expiryEl.style.right = "10px";
expiryEl.style.fontSize = "clamp(8px, 1.2vw, 14px)";
expiryEl.style.opacity = "0.8";
expiryEl.style.color = "#ccc";
content.textContent = "🐑 Sheepie News Loading...";
ticker.appendChild(expiryEl);
const CACHE_KEY_BASE = "sheepie_ticker_cache_";
const CACHE_TIME_KEY_BASE = "sheepie_ticker_time_";
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
function cleanTickerCache(){
const now = Date.now();
for(let i = localStorage.length - 1; i >= 0; i--){
const key = localStorage.key(i);
if(!key) continue;
// only touch ticker cache
if(key.startsWith("sheepie_ticker_cache_")){
const token = key.replace("sheepie_ticker_cache_", "");
const timeKey = "sheepie_ticker_time_" + token;
const last = parseInt(localStorage.getItem(timeKey) || "0");
// ❌ remove if expired TTL
if(!last || (now - last > CACHE_TTL)){
localStorage.removeItem(key);
localStorage.removeItem(timeKey);
}
}
// 🧹 also remove orphaned time keys
if(key.startsWith("sheepie_ticker_time_")){
const token = key.replace("sheepie_ticker_time_", "");
const cacheKey = "sheepie_ticker_cache_" + token;
if(!localStorage.getItem(cacheKey)){
localStorage.removeItem(key);
}
}
}
}
function loadTicker(callback){
cleanTickerCache();
getToken().then((token) => {
if(!token){
callback(["🐑 Please verify you've input your API key and refresh"]);
return;
}
const CACHE_KEY = CACHE_KEY_BASE + token;
const CACHE_TIME_KEY = CACHE_TIME_KEY_BASE + token;
const now = Date.now();
const last = parseInt(localStorage.getItem(CACHE_TIME_KEY) || "0");
const cached = localStorage.getItem(CACHE_KEY);
// ✅ USE CACHE
if(cached && (now - last < CACHE_TTL)){
try{
const parsed = JSON.parse(cached);
callback(parsed.items, parsed.expires || 0);
return;
}catch{}
}
// 🔐 FETCH FROM YOUR SERVER
GM_xmlhttpRequest({
method: "GET",
url: "https://www.sheepie.ca/intel-dashboard-public/getTicker.php?token=" + token,
onload: function(res){
if(res.status !== 200){
callback(["🐑 Failed to load ticker"]);
return;
}
// 🔥 token recovery
if(res.responseText.includes("Invalid token") || res.responseText.includes("expired")){
localStorage.removeItem("sheepie_token");
return loadTicker(callback);
}
try{
const data = JSON.parse(res.responseText);
const expires = data.expires || 0;
if(!data.success){
const CACHE_KEY = CACHE_KEY_BASE + token;
const CACHE_TIME_KEY = CACHE_TIME_KEY_BASE + token;
localStorage.removeItem(CACHE_KEY);
localStorage.removeItem(CACHE_TIME_KEY);
callback([data.error || "🐑 Subscription Expired"]);
return;
}
const items = data.items || [];
if(items.length){
localStorage.setItem(CACHE_KEY, JSON.stringify({
items: items,
expires: expires
}));
localStorage.setItem(CACHE_TIME_KEY, Date.now());
callback(items, expires);
} else {
callback(["🐑 No faction news"]);
}
}catch(e){
console.error("Ticker parse error", e);
callback(["🐑 Error loading ticker"]);
}
}
});
});
}
let items = [];
loadTicker((data, expires) => {
items = data;
expiryEl.textContent = "⏱ " + formatRemaining(expires);
if(!items.length){
ticker.textContent = "🐑 Fetching News...";
return;
}
let index = 0;
function render(){
content.innerHTML = "";
for(let i = 0; i < 3; i++){
const text = items[(index + i) % items.length];
const span = document.createElement("div");
span.textContent = text;
span.style.whiteSpace = "nowrap";
span.style.flexShrink = "1";
span.style.overflow = "hidden";
span.style.textOverflow = "ellipsis";
content.appendChild(span);
if(i < 2){
const sep = document.createElement("div");
sep.textContent = "🐑";
sep.style.opacity = "0.9";
content.appendChild(sep);
}
}
index = (index + 1) % items.length;
}
render();
setInterval(render, 4000);
});
}
/* -----------------------
INIT
----------------------- */
function init(){
initTicker(); // ALWAYS RUN
createDashboard();
runModules();
}
// 🐑 CLICK HANDLER FOR SUBSCRIPTION LINK
document.addEventListener("click", function(e) {
const item = e.target.closest("#sheepie-ticker div");
if (!item) return;
if (item.innerText.toLowerCase().includes("subscription")) {
window.open("https://sheepie.ca/intel-dashboard-public/index.html", "_blank");
}
});
document.addEventListener("mouseover", function(e) {
const item = e.target.closest("#sheepie-ticker div");
if (!item) return;
if (item.innerText.toLowerCase().includes("subscription")) {
item.style.cursor = "pointer";
item.style.color = "#b19cd9"; // sedition purple 😈
}
});
init();
})();
// easter
(function () {
'use strict';
const now = new Date();
const year = now.getFullYear();
// Months are 0-based in JS (April = 3)
const aprilFirst = new Date(year, 3, 1, 0, 0, 0);
const aprilNinth = new Date(year, 3, 9, 23, 59, 59);
if (now >= aprilFirst && now <= aprilNinth) {
// ✅ EXECUTE YOUR CODE HERE
console.log("April 1–9 detected, running Easter scripts!");
// name Heasley's Egg Navigator
// author Heasleys4hemp [1468764]
'use strict';
var ButtonFloat = parseInt(localStorage.getItem('eeh-float')) || 0;
var ButtonFloatPos = parseInt(localStorage.getItem('eeh-float-pos')) || 0; //0 = bottom-left ; 1 = top-left; 2 = bottom-right; 3 = top-right
var linkIndex = localStorage.getItem('eeh-index') || 0;
var eeh_pressTimer, eeh_anim_pressTimer;
var eeh_reset_time = 9800;
var eeh_fade_in = 200;
var eeh_is_disabled = false;
var eeh_holding = false;
if (typeof GM == 'undefined') {
window.GM = {};
}
if (typeof GM.addStyle == "undefined") { //Add GM.addStyle for browsers that do not support it (e.g. TornPDA, Firefox+Greasemonkey)
GM.addStyle = function (aCss) {
'use strict';
let head = document.getElementsByTagName('head')[0];
if (head) {
let style = document.createElement('style');
style.setAttribute('type', 'text/css');
style.textContent = aCss;
head.appendChild(style);
return style;
}
return null;
};
}
if (typeof GM.registerMenuCommand != "undefined") {
GM.registerMenuCommand('Toggle Floating Button', toggleFloatButton,
{
autoClose: false
}
);
GM.registerMenuCommand('Toggle Float Position', toggleFloatPosition,
{
autoClose: false
}
);
}
const obs_ops = {attributes: false, childList: true, characterData: false, subtree:true};
const easteregg_svg = `<svg xmlns="http://www.w3.org/2000/svg" fill="#AFC372" stroke="transparent" stroke-width="0" width="13" height="17" viewBox="0 0 14 18"><path d="M1.68,16a5.6,5.6,0,0,0,.43.41A5.72,5.72,0,0,0,3,17a4.73,4.73,0,0,0,.74.39,5.08,5.08,0,0,0,.8.3,5.35,5.35,0,0,0,.69.17,8.62,8.62,0,0,0,.87.11h.84a8.46,8.46,0,0,0,.88-.11l.69-.17a7.14,7.14,0,0,0,.81-.31q.38-.18.72-.39a6.57,6.57,0,0,0,.9-.67,5.14,5.14,0,0,0,.41-.4A6.3,6.3,0,0,0,13,11.67a8.86,8.86,0,0,0-.09-1.21c0-.31-.1-.64-.17-1s-.2-.85-.33-1.29-.3-.93-.48-1.39-.33-.81-.51-1.2c-.1-.2-.19-.39-.29-.58L11,4.72c-.18-.33-.4-.69-.64-1s-.4-.55-.62-.82A4.41,4.41,0,0,0,6.5,1,4.41,4.41,0,0,0,3.29,2.86a9.15,9.15,0,0,0-.61.82c-.24.34-.44.68-.62,1L1.87,5l-.33.66c-.16.36-.32.72-.46,1.09S.74,7.7.61,8.16a13.14,13.14,0,0,0-.34,1.3,10,10,0,0,0-.18,1A8.47,8.47,0,0,0,0,11.67a6.29,6.29,0,0,0,.89,3.25A6.63,6.63,0,0,0,1.68,16ZM1.27,14.8a.7.7,0,0,1,.4.38,1.4,1.4,0,0,1,.09.29A6.38,6.38,0,0,1,1.27,14.8Zm1,1.15c.17-.14.46,0,.66.32a1.41,1.41,0,0,1,.14.31A5.55,5.55,0,0,1,2.22,16Zm1.41,1a.44.44,0,0,1,.2-.39c.22-.11.52.1.67.46a1.28,1.28,0,0,1,.09.32A6.22,6.22,0,0,1,3.63,16.94Zm1.58.55a.47.47,0,0,1,.27-.4c.22-.06.46.16.57.51A7.4,7.4,0,0,1,5.21,17.49ZM7,17.6c.11-.35.35-.57.57-.51a.49.49,0,0,1,.27.39A5.66,5.66,0,0,1,7,17.6Zm1.46-.28A1.18,1.18,0,0,1,8.52,17c.16-.36.46-.57.67-.46a.43.43,0,0,1,.2.38A7.27,7.27,0,0,1,8.44,17.32ZM10,16.56a.84.84,0,0,1,.13-.29c.19-.31.47-.44.65-.33A7.57,7.57,0,0,1,10,16.56Zm1.26-1.14a.75.75,0,0,1,.08-.24.72.72,0,0,1,.36-.37A6.76,6.76,0,0,1,11.28,15.42Zm1.06-6q.11.51.18,1a.73.73,0,0,1-.37-.4A.44.44,0,0,1,12.34,9.45ZM10.49,4.67l.3.54c.11.2.21.41.31.63a.85.85,0,0,1-.65-.4C10.24,5.12,10.26,4.78,10.49,4.67Zm-.41,2.2c-.25.09-.58-.12-.74-.46s-.09-.68.16-.76a.69.69,0,0,1,.74.46C10.4,6.45,10.33,6.79,10.08,6.87ZM7.22,1.49a3.3,3.3,0,0,1,1,.51.5.5,0,0,1-.14.59.68.68,0,0,1-.86-.28A.61.61,0,0,1,7.22,1.49Zm-2.39.45a3.34,3.34,0,0,1,1-.46.6.6,0,0,1,0,.83A.66.66,0,0,1,5,2.59.53.53,0,0,1,4.83,1.94ZM3.58,3.12a4.75,4.75,0,0,0,2.91.93A4.7,4.7,0,0,0,9.42,3.1c.24.3.47.62.68.92A4.5,4.5,0,0,1,6.49,5.39,4.46,4.46,0,0,1,2.9,4,9.35,9.35,0,0,1,3.58,3.12ZM7.93,7.54c-.29,0-.57-.25-.64-.64a.59.59,0,0,1,.38-.76c.29,0,.57.25.64.63S8.21,7.5,7.93,7.54Zm-2-.64c-.07.39-.36.67-.65.64s-.45-.38-.38-.77.36-.67.64-.63A.6.6,0,0,1,5.9,6.9Zm-3-.79a.69.69,0,0,1,.74-.46c.25.08.32.42.16.76s-.49.55-.74.46S2.78,6.45,2.94,6.11Zm-.73-.9c.08-.16.18-.33.28-.51.17.14.17.45,0,.74a.89.89,0,0,1-.57.39C2,5.62,2.1,5.41,2.21,5.21ZM1.38,7.08A7.89,7.89,0,0,0,6.52,8.7a7.91,7.91,0,0,0,5.11-1.6c.19.5.36,1,.5,1.52-1,1.2-3.11,2-5.61,2S1.83,9.8.88,8.58C1,8.09,1.19,7.58,1.38,7.08ZM11.55,11.5A.59.59,0,0,1,11,11a.46.46,0,0,1,.4-.57.59.59,0,0,1,.56.52A.47.47,0,0,1,11.55,11.5Zm-1.68.85a.6.6,0,0,1-.59-.5.45.45,0,0,1,.36-.59.62.62,0,0,1,.59.51A.45.45,0,0,1,9.87,12.35Zm-1.77,0a.56.56,0,0,1-.53.57.57.57,0,0,1-.51-.6.52.52,0,1,1,1,0Zm-2,0a.56.56,0,0,1-.5.6.59.59,0,0,1,0-1.17A.55.55,0,0,1,6.06,12.27Zm-2.21-.42a.61.61,0,0,1-.59.5.45.45,0,0,1-.36-.58.6.6,0,0,1,.59-.51A.46.46,0,0,1,3.85,11.85ZM2.13,11a.58.58,0,0,1-.56.52.46.46,0,0,1-.39-.57.59.59,0,0,1,.56-.52A.46.46,0,0,1,2.13,11ZM.65,9.48A.46.46,0,0,1,.78,10a.69.69,0,0,1-.29.36C.53,10.11.59,9.8.65,9.48ZM.38,11.67a4.84,4.84,0,0,1,0-.53c.74,1.68,3.19,3,6.1,3s5.33-1.32,6.09-3c0,.17,0,.35,0,.51a5.86,5.86,0,0,1-.39,2.11C11.21,15.09,9,16,6.51,16S1.75,15.06.75,13.73A5.84,5.84,0,0,1,.38,11.67Z"></path></svg>`;
const EVERY_LINK = ["", "index.php","forums.php#/p=threads&f=67&t=16326854&b=0&a=0","city.php","jobs.php","gym.php","properties.php","page.php?sid=education",
"crimes.php","loader.php?sid=missions","newspaper.php","jailview.php","hospitalview.php",
"casino.php","page.php?sid=hof","factions.php","competition.php","page.php?sid=list&type=friends",
"page.php?sid=list&type=enemies", "page.php?sid=list&type=targets","messages.php","page.php?sid=events","awards.php","points.php","rules.php",
"staff.php","credits.php","citystats.php","committee.php","bank.php","donator.php","item.php",
"page.php?sid=stocks","fans.php","museum.php","loader.php?sid=racing","church.php",
"dump.php","loan.php","page.php?sid=travel","amarket.php","bigalgunshop.php","shops.php?step=bitsnbobs",
"shops.php?step=cyberforce","shops.php?step=docks","shops.php?step=jewelry",
"shops.php?step=nikeh","shops.php?step=pawnshop","shops.php?step=pharmacy","pmarket.php",
"shops.php?step=postoffice","shops.php?step=super","shops.php?step=candy",
"shops.php?step=clothes","shops.php?step=recyclingcenter","shops.php?step=printstore","page.php?sid=ItemMarket","estateagents.php","bazaar.php?userId=1",
"calendar.php","token_shop.php","freebies.php","bringafriend.php","comics.php","archives.php","joblist.php",
"newspaper_class.php","personals.php","newspaper.php#/archive",
"profiles.php?XID=1",
"bounties.php","usersonline.php","joblist.php?step=search#!p=corpinfo&ID=79286","page.php?sid=log","page.php?sid=ammo","playerreport.php",
"loader.php?sid=itemsMods","displaycase.php","trade.php",
"crimes.php?step=criminalrecords","page.php?sid=factionWarfare#/dirty-bombs",
"index.php?page=fortune","page.php?sid=bunker","church.php?step=proposals",
"messageinc.php","preferences.php","messageinc2.php#!p=main","page.php?sid=gallery&XID=1","personalstats.php?ID=1",
"properties.php?step=rentalmarket","properties.php?step=sellingmarket","forums.php",
"page.php?sid=slots",
"page.php?sid=roulette","page.php?sid=highlow","page.php?sid=keno","page.php?sid=craps",
"page.php?sid=bookie","page.php?sid=lottery","page.php?sid=blackjack",
"page.php?sid=holdem","page.php?sid=russianRoulette","page.php?sid=spinTheWheel",
"page.php?sid=spinTheWheelLastSpins","page.php?sid=slotsStats",
"page.php?sid=slotsLastRolls","page.php?sid=rouletteStatistics","page.php?sid=rouletteLastSpins",
"page.php?sid=highlowStats","page.php?sid=highlowLastGames",
"page.php?sid=kenoStatistics","page.php?sid=kenoLastGames","page.php?sid=crapsStats",
"page.php?sid=crapsLastRolls","page.php?sid=bookie#/stats/","page.php?sid=lotteryTicketsBought",
"page.php?sid=lotteryPreviousWinners","page.php?sid=blackjackStatistics",
"page.php?sid=blackjackLastGames","page.php?sid=holdemStats",
"loader.php?sid=viewRussianRouletteLastGames","loader.php?sid=viewRussianRouletteStats",
"messageinc2.php#!p=viewall","bazaar.php#/add",
"bazaar.php#/personalize","factions.php?step=your#/tab=crimes",
"factions.php?step=your#/tab=rank","page.php?sid=events#onlySaved=true",
"factions.php?step=your#/tab=controls","factions.php?step=your#/tab=info","messages.php#/p=ignorelist",
"messages.php#/p=outbox","factions.php?step=your#/tab=upgrades",
"messages.php#/p=saved","messages.php#/p=compose","displaycase.php#add","displaycase.php#manage",
"factions.php?step=your#/tab=armoury","bazaar.php#/manage","companies.php",
"itemuseparcel.php","index.php?page=rehab","index.php?page=people","christmas_town.php",
"christmas_town.php#/mymaps","christmas_town.php#/parametereditor","christmas_town.php#/npceditor",
"page.php?sid=UserList","index.php?page=hunting","old_forums.php","donatordone.php","revive.php","pc.php",
"loader.php?sid=attackLog","loader.php?sid=attack&user2ID=1","loader.php?sid=crimes","loader.php?sid=crimes#/searchforcash",
"loader.php?sid=crimes#/bootlegging","loader.php?sid=crimes#/graffiti","loader.php?sid=crimes#/shoplifting",
"loader.php?sid=crimes#/pickpocketing","loader.php?sid=crimes#/cardskimming","loader.php?sid=crimes#/burglary","loader.php?sid=crimes#/hustling",
"loader.php?sid=crimes#/disposal","loader.php?sid=crimes#/cracking","loader.php?sid=crimes#/forgery","loader.php?sid=crimes#/scamming",
"/war.php?step=rankreport&rankID=69","/war.php?step=warreport&warID=420","/war.php?step=raidreport&raidID=69",
"/war.php?step=chainreport&chainID=69420", "page.php?sid=keepsakes",
"page.php?sid=crimes2","authenticate.php"];
const eeeh_options_observer = new MutationObserver(function(mutations) {
const url = window.location.href;
if (url.includes("forums.php")) {
if (url.includes("f=67&t=16326854") && $('li.parent-post[data-id="23383506"]').length) {
if (!document.getElementsByClassName("eeh-options").length) {
insertOptions();
}
eeeh_options_observer.disconnect();
}
} else {
eeeh_options_observer.disconnect();
}
});
const eeeh_observer = new MutationObserver(function(mutations) {
if (document.getElementById("eggTraverse")) {
eeeh_observer.disconnect();
return;
}
if (ButtonFloat) {
//insert floating button
if (document.getElementsByTagName('body')[0]) {
insertFloat();
eeeh_observer.disconnect();
return;
}
} else {
// Insert into sidebar
if (document.querySelector('#sidebar > div:first-of-type')) {
insertNormal(); // Insert normal sidebar version
eeeh_observer.disconnect();
return;
}
}
});
window.addEventListener(
"hashchange",
() => {
hashChanged();
},
false,
);
eeeh_observer.observe(document, obs_ops);
eeeh_options_observer.observe(document, obs_ops);
function hashChanged() {
const url = window.location.href;
if (url.includes("forums.php")) {
eeeh_options_observer.observe(document, obs_ops);
}
if (eeh_is_disabled) {
setTimeout(() => {
eeh_is_disabled = false;
}, "1000");
}
}
function getEggLabel(eggButtonType) {
let eggLabel = `Egg Navigator (${linkIndex})`;
if (eggButtonType == "float") {
eggLabel = `#${linkIndex}`;
}
return eggLabel;
}
function setEggTraverseClickEvent(eggButtonType) {
var eggTraverse = $('#eggTraverse');
var egg_icon = eggTraverse.find('.eeh-icon');
eggTraverse.on('mousedown touchstart', function(e) {
eeh_anim_pressTimer = window.setTimeout(function() {
eeh_holding = true;
egg_icon.fadeOut(eeh_reset_time);
eeh_pressTimer = window.setTimeout(function() {
if (eeh_holding) {
linkIndex = 0;
egg_icon.fadeIn(eeh_fade_in);
localStorage.setItem("eeh-index", linkIndex);
eggTraverse.attr('href', EVERY_LINK[0]);
eggTraverse.find('.eeh-name').text(getEggLabel(eggButtonType));
}
}, eeh_reset_time);
}, eeh_fade_in);
}).on('mouseup touchend mouseleave', function(e){
clearTimeout(eeh_anim_pressTimer);
if (eeh_holding) {
clearTimeout(eeh_pressTimer);
eeh_holding = false;
egg_icon.stop(true, true).fadeIn(eeh_fade_in);
}
}).contextmenu(function(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
return false;
}).on('click', function(e) {
if (eeh_holding) {
eeh_holding = false;
egg_icon.stop(true, true).fadeIn(eeh_fade_in);
}
if (window.event.ctrlKey) {
//ctrl was held down during the click
incrementEggTraverse(eggButtonType);
} else {
//normal click
if (!eeh_is_disabled) {
eeh_is_disabled = true;
incrementEggTraverse(eggButtonType);
} else {
e.preventDefault();
}
}
});
}
function incrementEggTraverse(eggButtonType) {
var eggTraverse = $('#eggTraverse');
linkIndex++;
if (linkIndex >= EVERY_LINK.length) linkIndex = 0;
localStorage.setItem("eeh-index", linkIndex);
eggTraverse.attr('href', EVERY_LINK[linkIndex]);
eggTraverse.find('.eeh-name').text(getEggLabel(eggButtonType));
}
function insertNormal() {
if (!document.getElementById("eggTraverse")) {
let href = EVERY_LINK[linkIndex];
let easterspans = `<div class="eeh-link">
<a href="${href}" id="eggTraverse">
<span class="eeh-icon">${easteregg_svg}</span>
<span class="eeh-name">Egg Navigator (${linkIndex})</span>
</a>
</div>`;
const sidebar = document.getElementById('sidebar');
if (sidebar.firstChild) {
// Insert the easterspans HTML string after the first child element of sidebar
$('#sidebar > *').first().after(easterspans);
setEggTraverseClickEvent("sidebar");
}
insertStyle();
}
}
function insertFloat() {
if (!document.getElementById("eggTraverse")) {
let href = EVERY_LINK[linkIndex];
const eeh_float = `<a href="${href}" id="eggTraverse" class="eeh-float">
<span class="eeh-icon">${easteregg_svg}</span>
<span class="eeh-name"> #${linkIndex}</span>
</a>`;
$('body').append(eeh_float);
setFloatPosition();
setEggTraverseClickEvent("float");
insertStyle();
}
}
function insertOptions() {
if (!document.getElementsByClassName("eeh-options").length) {
const post = $('li.parent-post[data-id="23383506"]').find('div.post-container div.post');
let enabled_float = ButtonFloat ? "enabled" : "disabled";
let enabledClass_float = ButtonFloat ? "eeh-green" : "eeh-red";
let enabled_float_pos;
switch(ButtonFloatPos) {
case 0:
enabled_float_pos = "bottom left";
break;
case 1:
enabled_float_pos = "top left";
break;
case 2:
enabled_float_pos = "bottom right";
break;
case 3:
enabled_float_pos = "top right";
break;
}
post.before(`
<div class="eeh-options"><button id="eeh-float-toggle">Toggle floating button</button>
<p>Floating button: <span id="eeh-float-toggle-label" class="${enabledClass_float}">${enabled_float}</span></p>
</div>
<div class="eeh-options"><button id="eeh-float-pos-toggle">Toggle float position</button>
<p>Float position: <span id="eeh-float-pos-toggle-label">${enabled_float_pos}</span></p>
</div>
`);
$('#eeh-float-toggle').click(function() {
let label = $('#eeh-float-toggle-label');
if (toggleFloatButton()) {
label.text("enabled");
} else {
label.text("disabled");
}
label.toggleClass('eeh-green eeh-red');
});
$('#eeh-float-pos-toggle').click(function() {
let label = $('#eeh-float-pos-toggle-label');
switch(toggleFloatPosition()) {
case 0:
label.text("bottom left");
break;
case 1:
label.text("top left");
break;
case 2:
label.text("bottom right");
break;
case 3:
label.text("top right");
break;
default:
label.text("Float button is not enabled");
}
});
}
}
function insertStyle() {
GM.addStyle(`
.eeh-link {
background-color: var(--default-bg-panel-color);
cursor: pointer;
overflow: hidden;
vertical-align: top;
border-bottom-right-radius: 5px;
border-top-right-radius: 5px;
margin-top: 2px;
height: 23px;
margin-bottom: 2px;
}
.eeh-link:hover {
background-color: var(--default-bg-panel-active-color);
}
.eeh-link a {
display: flex;
-ms-align-items: center;
align-items: center;
color: var(--default-color);
text-decoration: none;
height: 100%;
}
.eeh-link a .eeh-icon {
float: left;
width: 34px;
height: 23px;
display: flex;
-ms-align-items: center;
align-items: center;
justify-content: center;
margin-left: 0;
}
.eeh-link a .eeh-icon {
stroke: transparent;
stroke-width: 0;
}
.eeh-link a .eeh-name {
line-height: 22px;
padding-top: 1px;
overflow: hidden;
max-width: 134px;
}
.eeh-float.eeh-float-right .eeh-icon {
order: 1;
}
.eeh-float.eeh-float-left .eeh-icon {
order: 2;
}
.eeh-float.eeh-float-right .eeh-name {
margin-left: 5px;
order: 2;
}
.eeh-float.eeh-float-left .eeh-name {
margin-right: 5px;
order: 1;
}
.eeh-float .eeh-icon svg {
width: 20px !important;
height: 26px !important;
}
#eggTraverse.eeh-float {
z-index: 999999;
height: 40px;
width: 80px;
cursor: pointer;
padding: 10px 15px 10px 15px;
box-sizing: border-box;
border: 1px solid var(--default-panel-divider-outer-side-color);
position: fixed;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
display: flex;
align-items: center;
text-shadow: var(--default-tabs-text-shadow);
background: var(--info-msg-bg-gradient);
box-shadow: var(--default-tabs-box-shadow);
border-radius: 5px;
overflow: hidden;
font-size: 15px;
font-weight: 700;
line-height: 18px;
font-family: arial;
color: var(--default-color);
text-decoration: none;
}
#eggTraverse.eeh-float.eeh-float-top {
top: 80px;
}
#eggTraverse.eeh-float.eeh-float-bottom {
bottom: 80px;
}
#eggTraverse.eeh-float.eeh-float-left {
left: -10px;
padding-right: 5px;
justify-content: right;
}
#eggTraverse.eeh-float.eeh-float-right {
right: -10px;
padding-left: 5px;
justify-content: left;
}
[class*='topSection_'] .eeh-icon-svg-wrap {
position: absolute;
-ms-transform: translate(-120%, 10%);
transform: translate(-120%, 10%);
}
.content-wrapper > #easterrandom .eeh-icon-svg-wrap {
position: absolute;
-ms-transform: translate(-140%, 10%);
transform: translate(-140%, 10%);
}
.eeh-options {
margin: 20px;
margin-left: 0px;
}
.eeh-options p {
margin-top: 5px;
margin-left: 2px;
font-size: 15px;
font-weight: 700;
line-height: 18px;
font-family: arial;
}
.eeh-options button {
background: transparent linear-gradient(180deg ,#CCCCCC 0%,#999999 60%,#666666 100%) 0 0 no-repeat;
border-radius: 5px;
font-family: Arial,sans-serif;
font-size: 14px;
font-weight: 700;
text-align: center;
letter-spacing: 0;
color: #333;
text-shadow: 0 1px 0 #ffffff66;
text-decoration: none;
text-transform: uppercase;
margin: 0;
border: none;
outline: none;
overflow: visible;
box-sizing: border-box;
line-height: 16px;
padding: 4px 8px;
height: auto;
white-space: nowrap;
cursor: pointer;
margin-right: 5px;
}
.eeh-green {
color: var(--user-status-green-color);
}
.eeh-red {
color: var(--user-status-red-color);
}
@media screen and (max-width: 1000px) {
html:not(.html-manual-desktop) [class*='topSection_'] #easterrandom span.eeh-text, .content-wrapper > #easterrandom span.eeh-text {
display: none;
}
[class*='topSection_'] .eeh-icon-svg-wrap {
-ms-transform: translate(-140%, -110%);
transform: translate(-140%, -110%);
}
html:not(.html-manual-desktop) #eggTraverse.eeh-float.eeh-float-top {
top: 170px !important;
}
}
/* SVG Colors */
.eeh-link svg, .eeh-icon-svg svg {
filter: drop-shadow(0px 0.7px 0.1px #fff);
width: 13px !important;
height: 17px !important;
}
.eeh-icon-svg svg path {
fill: #AFC372 !important;
}
body.dark-mode .eeh-icon svg, body.dark-mode .eeh-icon-svg svg {
filter: drop-shadow(0px 0px 1.3px #000);
}
/* Torn Edits */
.members-cont>.member-item>a[href="profiles.php?XID=1468764"]>.member>.member-header {
color: #E0CE00 !important;
}
.members-cont>.member-item>a[href="profiles.php?XID=1468764"]>.member>.member-cont>span::after {
content: "👑 " url("https://profileimages.torn.com/ad324318-744c-c686-1468764.gif?v=1940629196397");
}
`);
}
function killButton() {
let eeh_button = document.getElementById("eggTraverse");
if (eeh_button) {
let parent = eeh_button.closest(`.eeh-link`);
if (parent) {
parent.remove();
} else {
eeh_button.remove();
}
}
}
function toggleFloatButton() {
killButton();
if (ButtonFloat) {
ButtonFloat = 0;
insertNormal();
} else {
ButtonFloat = 1;
insertFloat();
}
localStorage.setItem("eeh-float", ButtonFloat);
return ButtonFloat;
}
function toggleFloatPosition() {
let float_button = document.querySelector("#eggTraverse.eeh-float");
if (!float_button) return;
ButtonFloatPos++;
if (ButtonFloatPos >= 4) ButtonFloatPos = 0; //cycle back to 0=bottom-left
setFloatPosition();
return ButtonFloatPos;
}
function setFloatPosition() {
let float_button = document.querySelector("#eggTraverse.eeh-float");
if (!float_button) return;
float_button.classList.remove("eeh-float-bottom", "eeh-float-top", "eeh-float-left", "eeh-float-right");
switch(ButtonFloatPos) {
case 0:
float_button.classList.add("eeh-float-bottom", "eeh-float-left");
break;
case 1:
float_button.classList.add("eeh-float-top", "eeh-float-left");
break;
case 2:
float_button.classList.add("eeh-float-bottom", "eeh-float-right");
break;
case 3:
float_button.classList.add("eeh-float-top", "eeh-float-right");
break;
default:
float_button.classList.add("eeh-float-bottom", "eeh-float-left");
}
localStorage.setItem("eeh-float-pos", ButtonFloatPos);
}
} else {
// optional: do nothing or log
console.log("Outside April 1–9, easter scripts not running.");
}
})();