// ==UserScript==
// @name eRepublik #SPRINGBREAK Mission Tracker v3.13 (v43)
// @namespace http://tampermonkey.net/
// @version 3.13
// @description See all your eRepublik #SPRINGBREAK mission progress in real time — at a glance, in a clean, draggable, collapsible panel. Fully local, privacy-first, player-friendly, and bot-free. Tracks missions, not players. Made by a gamer, for gamers.
// @author Janko Fran
// @match https://www.erepublik.com/*
// @grant none
// @license Custom License - Personal, Non-Commercial Use Only
// @run-at document-idle
// @homepageURL https://greasyfork.org/en/users/1461808-janko-fran
// @supportURL https://www.erepublik.com/en/main/messages-compose/2103304
// @downloadURL none
// ==/UserScript==
/* jshint esversion: 11 */
/* License: This script is provided free of charge for personal, non-commercial use.
// You are granted a perpetual, royalty-free license to use this script on your own eRepublik account.
// No part of this script may be modified, redistributed, or used for commercial purposes without the express written permission of the author, Janko Fran.
// Donations are welcome to support future improvements. For details, see the Info modal or documentation.
//
// Donation Links:
// • eRepublik Donations: https://www.erepublik.com/en/economy/donate-money/2103304
// • Satoshi Donations: janko7ea63e4e@zbd.gg
// For custom scripts or financial donations, please contact:
// https://www.erepublik.com/en/main/messages-compose/2103304
*/
(async function() {
'use strict';
/**
* ──────────────────────────────────────────────────────────────────────────────
* SECTION: HTML CONTENT BUILDERS
* • sectionTemplate
* • renderSection
* • techItemTemplate
* • buildTechStackParagraph
* ──────────────────────────────────────────────────────────────────────────────
*/
/**
* sectionTemplate - A small template for any “Section” with a heading and a paragraph.
* @param {string} title
* @param {string} bodyHtml
*/
const sectionTemplate = (title, bodyHtml) => `
${title}
${bodyHtml}
`;
/**
* renderSection - Wraps sectionTemplate for clarity and future customization.
* @param {string} title
* @param {string} bodyHtml
*/
function renderSection(title, bodyHtml) {
return sectionTemplate(title, bodyHtml);
}
/**
* techItemTemplate - Renders a single tech‐stack item.
* @param {string} name —technology name
* @param {string} description—its description
* @returns {string} —bold name + italic desc
*/
const techItemTemplate = (name, description) =>
`${name}: ${description}`;
/**
* buildTechStackParagraph - Builds a paragraph listing all tech‐stack items.
* @param {Array<{name:string,desc:string}>} items
* @returns {string} — `
…
` with ` `-separated items
*/
function buildTechStackParagraph(items) {
return `
This script was developed using the following technologies:
${items.map(({ name, desc }) => techItemTemplate(name, desc)).join(' ')}
`;
}
/*** INFO MODAL CONTENT GENERATION ***/
function createTitle(titleText) {
return `
${titleText}
`;
}
function createParagraph(paragraphText) {
return `
Since official development of eRepublik has slowed significantly in recent years,
I decided to improve the player experience myself. This project began as a personal
tool, and I’m sharing it for the benefit of other active players who still enjoy the game.
In many ways, this is how the company workflow should have worked from the beginning.
This project is a small contribution toward keeping eRepublik fun, efficient, and rewarding.
`;
const SECTION_FEATURES = `
What the Script Does
Shows live mission progress, time-elapsed and overall average for the #SPRINGBREAK event.
It runs entirely in your browser—no data is sent or stored externally.
Anyone juggling all 10 daily missions will save time and clicks by seeing everything
at a glance, with real-time updates and a draggable, collapsible panel.
⚠️ Important Note
This script does not automate any part of gameplay—you still need to manually
click “Work,” “Fight,” or travel to yourself. It simply provides a clear,
live overview of your mission progress.
Free, Transparent, Player-Driven
This script is free, transparent, and built entirely with players in mind. There are no trackers, no ads, and no hidden behavior. It was created with genuine passion for the game and a commitment to fair, efficient, and enjoyable gameplay.
Tech Stack
${CONFIG.textLabels.techStack}
License
For personal, non-commercial use only. Redistribution or commercial use is not permitted without the author's written consent.
Support Future Development
If this script has saved you time or made company management easier, please consider supporting future improvements of this and other scripts. Donations help cover development time, testing, and enhancements, and are a much-appreciated motivation to keep going.
`;
const INFO_MODAL_CONTENT = [
{
title: 'Who Will Benefit',
paragraph: 'Anyone juggling all 10 daily missions will save time and clicks by seeing everything at a glance, with real-time updates and a draggable, collapsible panel.'
},
{
title: '⚠️ Important Note',
paragraph: 'This script does not automate any part of gameplay—you still need to manually click “Work,” “Fight,” or travel to yourself. It simply provides a clear, live overview of your mission progress.'
},
{
title: 'Free, Transparent, Player-Driven',
paragraph: 'This script is free, transparent, and built entirely with players in mind. There are no trackers, no ads, and no hidden behavior. It was created with genuine passion for the game and a commitment to fair, efficient, and enjoyable gameplay.'
},
{
title: 'Tech Stack',
paragraph: CONFIG.textLabels.techStack
},
{
title: 'License',
paragraph: 'For personal, non-commercial use only. Redistribution or commercial use is not permitted without the author\'s written consent.'
},
{
title: 'Support Future Development',
paragraph: 'If this script has saved you time or made company management easier, please consider supporting future improvements of this and other scripts. Donations help cover development time, testing, and enhancements, and are a much-appreciated motivation to keep going.'
}
];
/*** DRAGGABLE ***/
function makeDraggable(panel, handle) {
handle.style.cursor = 'move';
let sx, sy, px, py;
// Load saved position from localStorage
const savedPosition = localStorage.getItem(CONFIG.STORAGE_KEY);
if (savedPosition) {
const { top, left } = JSON.parse(savedPosition);
panel.style.top = top;
panel.style.left = left;
}
handle.addEventListener('mousedown', e => {
e.preventDefault();
sx = e.clientX; sy = e.clientY;
px = panel.offsetLeft; py = panel.offsetTop;
function onMove(e) {
panel.style.left = px + (e.clientX - sx) + 'px';
panel.style.top = py + (e.clientY - sy) + 'px';
}
function onUp() {
document.removeEventListener('mousemove', onMove);
// Save position to localStorage
localStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify({
top: panel.style.top,
left: panel.style.left
}));
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp, { once: true });
});
}
function waitFor(conditionFn, interval = CONFIG.pollIntervalMs, timeout = CONFIG.maxWaitTimeMs) {
return new Promise((resolve, reject) => {
const intervalId = setInterval(() => {
try {
const result = conditionFn();
if (result) {
clearInterval(intervalId);
clearTimeout(timeoutHandle);
resolve(result);
}
} catch (e) {
clearInterval(intervalId);
clearTimeout(timeoutHandle);
reject(e);
}
}, interval);
const timeoutHandle = setTimeout(() => {
clearInterval(intervalId);
reject(new Error('Timeout waiting for condition'));
}, timeout);
});
}
/*** WAIT FOR missionsJSON ***/
function waitForMissionsData() {
return waitFor(() => {
return Array.isArray(window.missionsJSON) && window.missionsJSON.length? window.missionsJSON.slice(): null;
});
}
async function fetchDetailedMissions(culture, token, host, delay = CONFIG.fetchDelayMs) {
const missions = await waitForMissionsData();
const detailedMissions = [];
for (let mission of missions) {
detailedMissions.push(await fetchMission(mission, culture, token, host));
await new Promise(resolve => setTimeout(resolve, delay));
}
return detailedMissions;
}
/*** FETCH LIVE DATA ***/
async function fetchMission(m, culture, token, host) {
try {
const url = `${location.protocol}//${host}/${culture}/main/mission-check?missionId=${m.id}&_token=${token}`;
const res = await fetch(url, { credentials: 'same-origin' });
const j = await res.json();
if (Array.isArray(j.conditions)) m.liveConditions = j.conditions;
if (Array.isArray(j.rewards)) m.rewards = j.rewards;
} catch (e) {
console.error('fetchMission', m.id, e);
}
return m;
}
/*** MAP REWARDS ***/
const mapReward = cat => ({
springBreakTokens: 'Springcoins',
spring_break_tokens: 'Springcoins',
gold: 'Gold',
currency: 'Currency',
vehicle_blueprint: 'Blueprint'
}[cat] || cat);
/*** HELPER FUNCTIONS ***/
function getServerTimeFromScriptTag() {
const scripts = document.querySelectorAll('script');
for (let script of scripts) {
const text = script.textContent;
if (text.includes('SERVER_DATA') && text.includes('serverTime')) {
const match = text.match(/SERVER_DATA\s*=\s*({.*?})\s*[,;]\s*ErpkShop/s);
if (match && match[1]) {
try {
const serverData = JSON.parse(match[1]);
return serverData.serverTime || null;
} catch (e) {
console.error("SERVER_DATA JSON parsing failed", e);
}
}
}
}
return null;
}
/**
* ──────────────────────────────────────────────────────────────────────────────
* SECTION: UTILITY FUNCTIONS
*
* • htmlToDiv
* • getEventProgress
* • getEventProgressFromServer
* ──────────────────────────────────────────────────────────────────────────────
*/
/**
* htmlToDiv
* ---------
* Turn a raw HTML string into a detached
element.
*
* @param {string} html – The markup you want wrapped (should produce valid DOM).
* @returns {HTMLDivElement} – A DIV whose innerHTML is set to the trimmed string.
*
* Usage:
* const node = htmlToDiv('Hello');
* document.body.appendChild(node);
*/
function htmlToDiv(html) {
const div = document.createElement('div');
div.innerHTML = html.trim();
return div;
}
/**
* Calculate precise event progress including time of day.
*
* @param {number} startDay - The eRepublik day the event starts (e.g., 6363)
* @param {number} currentDay - The current eRepublik day (e.g., 6365)
* @param {string} currentTimeStr - Time of day in "HH:MM" format (e.g., "01:12")
* @param {number} totalDays - Total duration of the event in days (e.g., 14)
* @returns {number} - Fractional progress from 0 to 1
*/
function getEventProgress(startDay, currentDay, currentTimeStr, totalDays) {
const [hours, minutes] = currentTimeStr.split(':').map(Number);
const minutesInDay = 24 * 60;
const timeFraction = (hours * 60 + minutes) / minutesInDay;
const daysPassed = currentDay - startDay;
const totalProgress = daysPassed + timeFraction;
return Math.min(1, totalProgress / totalDays); // Clamp to max 1.0
}
function getEventProgressFromServer(startDay, currentDay, serverTimeObj, totalDays) {
const minutesInDay = 24 * 60;
const hours = serverTimeObj.hour;
const minutes = new Date(serverTimeObj.dateTime).getMinutes(); // safer than relying on offset
const timeFraction = (hours * 60 + minutes) / minutesInDay;
const daysPassed = currentDay - startDay;
const totalProgress = daysPassed + timeFraction;
const fractionalProgress = Math.min(1, totalProgress / totalDays); // Clamp to max 1.0
return {
fractionalProgress,
hours,
minutes
};
}
/**
* ──────────────────────────────────────────────────────────────────────────────
* SECTION: GUI RENDERERS
* • getPanel / showInfoModal
* • renderMissionBox / renderMissionPanel
* ──────────────────────────────────────────────────────────────────────────────
*/
/*** PANEL CREATION ***/
function getPanel() {
let panel = document.getElementById('mission-tracker-panel');
if (!panel) {
panel = document.createElement('div');
panel.id = 'mission-tracker-panel';
Object.assign(panel.style, {
position: 'fixed',
top: CONFIG.RESET_POSITION.top,
left: CONFIG.RESET_POSITION.left,
width: '400px',
maxHeight: '90vh',
background: 'rgba(30,30,30,0.85)',
color: CONFIG.COLORS.SURFACE,
fontSize: CONFIG.FONTS.base,
zIndex: 99999,
border: '1px solid #444',
borderRadius: '8px',
boxShadow: '0 0 10px rgba(0,0,0,0.8)',
overflow: 'hidden'
});
// HEADER
const header = document.createElement('div');
header.id = 'mt-header';
Object.assign(header.style, {
background: 'rgba(51,51,51,0.9)',
padding: '6px 10px',
display: 'flex',
alignItems: 'center',
justifyContent:'space-between',
borderTopLeftRadius: '8px',
borderTopRightRadius: '8px',
userSelect: 'none'
});
header.innerHTML = `