您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.
当前为
// ==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 // ==/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: [email protected] // 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) => ` <h4>${title}</h4> <p>${bodyHtml}</p> `; /** * 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) => `<b>${name}</b>: <i>${description}</i>`; /** * buildTechStackParagraph - Builds a paragraph listing all tech‐stack items. * @param {Array<{name:string,desc:string}>} items * @returns {string} — `<p>…</p>` with `<br>`-separated items */ function buildTechStackParagraph(items) { return ` <p> This script was developed using the following technologies:<br> ${items.map(({ name, desc }) => techItemTemplate(name, desc)).join('<br>')} </p> `; } /*** INFO MODAL CONTENT GENERATION ***/ function createTitle(titleText) { return `<h4>${titleText}</h4>`; } function createParagraph(paragraphText) { return `<p>${paragraphText}</p>`; } function createSection({ titleText, paragraphText }) { return `${createTitle(titleText)}${createParagraph(paragraphText)}<hr>`; } function generateInfoModalContent() { return INFO_MODAL_CONTENT.map(({ title, paragraph }) => createSection(title, paragraph)).join(''); } /** * ────────────────────────────────────────────────────────────────────────────── * SECTION: TEXT DEFINITIONS * • SECTION_MOTIVATION, SECTION_FEATURES, etc. * • techItems array * ────────────────────────────────────────────────────────────────────────────── */ const SECTION_MOTIVATION = ` <h4>Personal Motivation</h4> <p> 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. </p> `; const SECTION_FEATURES = ` <h4>What the Script Does</h4> <p> 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. </p> `; const techItems = [ { name: "Tampermonkey", desc: "A browser extension that safely runs custom scripts to improve your experience, without tracking or ads, respecting eRepublik’s gameplay rules." }, { name: "JavaScript (ES5)", desc: "Ensures legacy browser support and eRepublik’s frontend compatibility." }, { name: "HTML & CSS", desc: "All UI is built natively into the DOM." }, { name: "ChatGPT Plus", desc: "Assisted during rapid prototyping & testing." } ]; // ────────────────────────────────────────────────────────────────────────── // SECTION: COLOR PALETTE – raw color swatches // ────────────────────────────────────────────────────────────────────────── const COLOR_PALETTE = { GREEN_500 : '#0f0', ORANGE_500: '#ffa500', CYAN_400 : '#6cf', GRAY_400 : '#ccc', GRAY_200 : '#eee', GRAY_100 : '#333', BLACK_900 : '#000' }; /** * ────────────────────────────────────────────────────────────────────────────── * SECTION: CONFIGURATION * • CONFIG constants * • CONFIG.textLabels assignments * ────────────────────────────────────────────────────────────────────────────── */ const CONFIG = { VERSION: '3.13', DEBUG: false, EVENT_LENGTH: 14, // days TOTAL_MISSIONS: 10, ICONS: { reset:'⟲', progress:'📌' }, REWARD_ICON: 'https://www.erepublik.net/images/modules/popups/dyeHard/currency.png', BLUEPRINT_ICON: 'https://www.erepublik.net/images/modules/battle/garage/blueprints/vehicle_blueprint_large.png', RESET_POSITION: { top: '60px', left: '20px' }, STORAGE_KEY: 'missionTrackerPosition', STATE_KEY: 'missionTrackerState', DONATE_URL: 'https://www.erepublik.com/en/economy/donate-money/2103304', FONTS: { title: '13px', base: '12px', small: '11px', tiny: '10px' }, /* ────────────────────────────────────────────────────────────────────────────── * SECTION: SEMANTIC COLOR TOKENS (what the colour *means* in UI) * ────────────────────────────────────────────────────────────────────────────── */ COLORS: { OK: COLOR_PALETTE.GREEN_500, // ✔︎ bright lime WARN: COLOR_PALETTE.ORANGE_500, // ⚠︎ warm orange TEXT: COLOR_PALETTE.CYAN_400, // ℹ︎ light cyan MUTED: COLOR_PALETTE.GRAY_400, // mid-light grey SURFACE: COLOR_PALETTE.GRAY_200, // bright grey DIVIDER : COLOR_PALETTE.GRAY_100 // dark gray }, BORDERS: { DIVIDER: `0.25px solid ${COLOR_PALETTE.GRAY_100}` }, HTML : { hr() { return `<hr class="mt-divider">`; } }, pollIntervalMs: 200, maxWaitTimeMs: 10000, fetchDelayMs: 150, textLabels: {}, }; CONFIG.textLabels.techStack = buildTechStackParagraph(techItems); CONFIG.textLabels.infoText = ` ${SECTION_MOTIVATION} <hr> ${SECTION_FEATURES} <hr> <h4>Who Will Benefit</h4> <p> 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. </p> <hr> <h4>⚠️ Important Note</h4> <p> This script does <em>not</em> 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. </p> <hr> <h4>Free, Transparent, Player-Driven</h4> <p> 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. </p> <hr> <h4>Tech Stack</h4> ${CONFIG.textLabels.techStack} <hr> <h4>License</h4> <p> For personal, non-commercial use only. Redistribution or commercial use is not permitted without the author's written consent. </p> <hr> <h4>Support Future Development</h4> <p> 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. </p> <ul> <li><img src="https://www.erepublik.net/images/modules/sidebar/currency.png?1698060179" style="height: 10px; vertical-align: text-bottom;"> <strong><a href="https://www.erepublik.com/en/economy/donate-money/2103304" target="_blank">Donate via eRepublik</a></strong></li> <li><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAMAAABF0y+mAAAArlBMVEUIIvUIIvYIIOcII/kAEa0AHfxFTXwAGvMAAP+vuTcMH80AIPkAAOrt+wB5g4IAEP9kbo37/xjf7AAAHvoAANvk8gDw/yf1/ySdp1AEH/bO2hqTnVkAAPIAFv0AHusRI83U4Q1ueI96g3uSnWoqNrW6xTM6RKi/yywAANKdp1tFc8I2XdKapGRKVK0lRuCP4jiJ2kkePOY/SaQQK+9+yWGb9AB3vnAAFcRwtIFkopY0Lz+5AAAAtklEQVR4AbzRQwLEQBAAwO7YtpO1rf8/LLjFx51rTRv+8nDOiBklKXIaaYbFSeR4QZwylGRFnUJV0w0Jx820bMf1uBFFX7UC13H0MIrJviepYtdWa5anCXanp4pMb8xx5cWytwskY1WTG1tZqjgsi+padtxNNDEpV+jydsxoEwAje8eNWLw/xADq8UQPzTxfrjcTzLuAA/Mfz9frDUh+xg7qc98fh4DJkBq9PfxquJOJJFBuMAAAWPsMsDR/OjAAAAAASUVORK5CYII=" style="height: 10px; vertical-align: text-bottom;"> <strong><a href="mailto:[email protected]">Donate via Satoshi (ZBD)</a></strong></li> </ul> <p> For feedback, bug reports, suggestions, or custom script requests, feel free to <strong><a href="https://www.erepublik.com/en/main/messages-compose/2103304" target="_blank">send me a message</a></strong>. </p> <p> Sincerely Yours,<br> <strong><a href="https://www.erepublik.com/en/citizen/profile/2103304" target="_blank">Janko Fran</a></strong> </p> <hr> <p class="footnote" style="text-align:right; margin-top:8px; color:${COLOR_PALETTE.BLACK_900}"> Script version: v${CONFIG.VERSION} </p> `; 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 <em>not</em> 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 <div> 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('<span>Hello</span>'); * 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 = ` <div class="mt-title-container"> <img src="https://www.erepublik.net/images/icons_svg/sidebar/events_icon.svg" class="mt-title-icon"> <span class="mt-title-text">#SPRINGBREAK Mission Tracker</span> </div> <div class="mt-button-container"> <button id="mt-reset" class="mt-button" title="Reset"> <img src="https://www.erepublik.net/images/modules/battle/garage/reset_icon.png" class="mt-button-icon"> <button id="mt-info" class="mt-button" title="Info"> <img src="https://www.erepublik.net/images/modules/sidebar/info_icon.png" class="mt-button-icon"> </button> <button id="mt-donate" class="mt-button" title="Donate"> <img src="https://www.erepublik.net/images/modules/_icons/gold_icon.png" class="mt-button-icon"> </button> <button id="mt-toggle" class="mt-toggle-button" title="Collapse">–</button> </div> `; panel.appendChild(header); // CONTENT const content = document.createElement('div'); content.id = 'mt-content'; content.style.maxHeight = 'calc(90vh - 46px)'; content.style.overflowY = 'auto'; panel.appendChild(content); document.body.appendChild(panel); // Load saved state from localStorage const savedState = localStorage.getItem(CONFIG.STATE_KEY); const isCollapsed = savedState === 'collapsed'; content.style.display = isCollapsed ? 'none' : 'block'; panel.style.height = isCollapsed ? '46px' : 'auto'; const toggleButton = header.querySelector('#mt-toggle'); toggleButton.textContent = isCollapsed ? '+' : '–'; toggleButton.title = isCollapsed ? 'Expand' : 'Collapse'; // DRAG & CONTROLS makeDraggable(panel, header); header.querySelector('#mt-toggle').addEventListener('click', () => { const toggleButton = header.querySelector('#mt-toggle'); if (content.style.display === 'none') { content.style.display = 'block'; panel.style.height = 'auto'; header.querySelector('#mt-toggle').textContent = '–'; toggleButton.title = 'Collapse'; localStorage.setItem(CONFIG.STATE_KEY, 'open'); } else { content.style.display = 'none'; panel.style.height = '46px'; header.querySelector('#mt-toggle').textContent = '+'; toggleButton.title = 'Expand'; localStorage.setItem(CONFIG.STATE_KEY, 'collapsed'); } }); header.querySelector('#mt-reset').addEventListener('click', () => { panel.style.top = CONFIG.RESET_POSITION.top; panel.style.left = CONFIG.RESET_POSITION.left; content.style.display = 'block'; panel.style.height = 'auto'; header.querySelector('#mt-toggle').textContent = '–'; localStorage.removeItem(CONFIG.STORAGE_KEY); // Clear saved position localStorage.setItem(CONFIG.STATE_KEY, 'open'); }); header.querySelector('#mt-info').addEventListener('click', showInfoModal); header.querySelector('#mt-donate').addEventListener('click', () => { window.open(CONFIG.DONATE_URL, '_blank'); }); } panel.querySelector('#mt-content').innerHTML = ''; return panel; } /*** INFO MODAL ***/ function showInfoModal() { let modal = document.getElementById('mt-info-modal'); if (!modal) { modal = document.createElement('div'); modal.id = 'mt-info-modal'; modal.innerHTML = ` <div> <h3>About This Script – #SPRINGBREAK Mission Tracker</h3> ${CONFIG.textLabels.infoText} <button id="mt-close-info">Close</button> </div> `; document.body.appendChild(modal); modal.querySelector('#mt-close-info').onclick = () => modal.remove(); } } function parseStepsFromTitle(title) { const stepMatch = (title || '').match(/(\d+)\s*\/\s*(\d+)/); return stepMatch ? { currentStep: parseInt(stepMatch[1], 10), totalSteps: parseInt(stepMatch[2], 10) } : { currentStep: 1, totalSteps: 1 }; } /*─────────── Mission sub-templates ───────────*/ /** icon + title + percentage */ function missionHeaderHTML({ icon, title, percent }) { return ` <div class="mission-header"> <img src="${icon}" style="width:20px;height:20px;margin-right:6px;vertical-align:middle"> <strong>${title}</strong> <span style="color:${CONFIG.COLORS.TEXT};font-size:${CONFIG.FONTS.small};margin-left:4px"> (${percent.toFixed(1)}%) </span> </div>`; } function missionDescriptionHTML(description) { return ` <div class="mission-desc"> ${description} </div>`; } function missionRequirementHTML({ done, requirement, total }) { const colour = done >= total ? CONFIG.COLORS.OK : CONFIG.COLORS.WARN; const percent = Math.floor(done / total * 100); return ` <div class="mission-req"> ${CONFIG.ICONS.progress} ${requirement} <span style="color:${colour};font-weight:bold"> ${done}/${total} (${percent}%) </span> </div>`; } function missionRewardHTML(rewards) { const rewardList = rewards.map(r => { const rewardText = `+${r.displayValue} ${mapReward(r.category)}`; if (r.category === 'vehicle_blueprint') { return `<img src="${CONFIG.BLUEPRINT_ICON}" style="width:16px;height:16px;vertical-align:middle;margin-right:4px;" title="Blueprint" onerror="this.style.display='none';"> ${rewardText}`; } return rewardText; }).join(', '); return ` <div class="mission-reward"> <img src="${CONFIG.REWARD_ICON}" style="width:16px;height:16px;vertical-align:middle;margin-right:4px"> ${rewardList} </div>`; } function renderMissionBox(mission) { /* ---- crunch numbers --------------------------------------------------- */ const { currentStep, totalSteps } = parseStepsFromTitle(mission.title); const liveConditionData = (mission.liveConditions || [])[0] || {}; const [currentStepCount, totalStepsFromCondition] = Array.isArray(liveConditionData.partial) ? liveConditionData.partial : [0, 1]; const stepFraction = totalStepsFromCondition > 0 ? currentStepCount / totalStepsFromCondition : 0; const stepFractionPercent = ((currentStep - 1) + stepFraction) / totalSteps * 100; const missionIcon = mission.img.startsWith('//') ? 'https:' + mission.img : mission.img; // Log rewards for debugging (only in debug mode) if (CONFIG.DEBUG) { console.log(`Mission ${mission.title} (ID: ${mission.id}) rewards:`, mission.rewards.map(r => ({ category: r.category, displayValue: r.displayValue }))); } /* ---- assemble HTML ---------------------------------------------------- */ const missionDisplayElement = document.createElement('div'); missionDisplayElement.style.marginBottom = '14px'; missionDisplayElement.innerHTML = missionHeaderHTML({ icon: missionIcon, title: mission.title, percent: stepFractionPercent }) + missionDescriptionHTML(mission.description) + missionRequirementHTML({ done: currentStepCount, requirement: liveConditionData.requirement, total: totalStepsFromCondition }) + missionRewardHTML(mission.rewards) + CONFIG.HTML.hr(); return { missionDisplayElement, stepFractionPercent }; } /*** RENDER ***/ function renderMissionPanel(missions) { const panel = getPanel(); const content = panel.querySelector('#mt-content'); // time & summary const staticConds = window.missionsJSON.flatMap(m => m.conditions); const days = staticConds.map(txt => { const m = txt.match(/Day\s*([\d,]+)/); return m ? parseInt(m[1].replace(/,/g,''),10) : NaN; }).filter(d => !isNaN(d)); const endDay = Math.max(...days); const startDay = endDay - CONFIG.EVENT_LENGTH + 1; const eDay = window.erepublik.settings.eDay; const serverTime = getServerTimeFromScriptTag(); const daysPassed = eDay - startDay + 1; const dayIndex = Math.min(CONFIG.EVENT_LENGTH, Math.max(1, daysPassed)); let dayPct = "N/A", timeText = "", color = CONFIG.COLORS.MUTED; if (serverTime) { const { fractionalProgress, hours, minutes } = getEventProgressFromServer(startDay, eDay, serverTime, CONFIG.EVENT_LENGTH); dayPct = (fractionalProgress * 100).toFixed(2); timeText = `, Time ${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; color = fractionalProgress > 0.5 ? CONFIG.COLORS.OK : fractionalProgress > 0.25 ? CONFIG.COLORS.WARN : CONFIG.COLORS.MUTED; } const summary = document.createElement('div'); summary.style.cssText = ` padding:8px;margin-bottom:12px; background:rgba(43,43,43,0.9); border-radius:4px;font-size:${CONFIG.FONTS.base}; `; summary.innerHTML = ` <div id="mt-overall"><strong>Overall Progress:</strong> calculating…</div> <div style="font-size:${CONFIG.FONTS.small}; color: ${color}"> <strong>Time progress:</strong> Day ${dayIndex}/${CONFIG.EVENT_LENGTH}${timeText} (${dayPct}%) </div> `; content.appendChild(summary); // missions let sumPct = 0; // Render real missions missions.forEach(m => { const { missionDisplayElement, stepFractionPercent } = renderMissionBox(m); content.appendChild(missionDisplayElement); sumPct += stepFractionPercent; }); // Mock mission for testing blueprint rewards (only in debug mode) let mockElement, mockPct; if (CONFIG.DEBUG) { const mockMission = { id: 'mock_1', title: 'Test Mission 1/2', description: 'This is a test mission for blueprint rewards.', img: 'https://www.erepublik.net/images/icons_svg/sidebar/events_icon.svg', liveConditions: [{ requirement: 'Complete test tasks', partial: [1, 2] }], rewards: [ { category: 'spring_break_tokens', displayValue: '110' }, { category: 'vehicle_blueprint', displayValue: '1' } ] }; // Render mock mission first for testing const { missionDisplayElement: mockElement, stepFractionPercent: mockPct } = renderMissionBox(mockMission); content.appendChild(mockElement); sumPct += mockPct; } // compute overall const completedCount = CONFIG.TOTAL_MISSIONS - missions.length; const totalPct = sumPct + completedCount * 100; summary.querySelector('#mt-overall').innerHTML = `<strong>Overall Progress:</strong> ${(totalPct / CONFIG.TOTAL_MISSIONS).toFixed(1)}%`; } /** * ────────────────────────────────────────────────────────────────────────────── * SECTION: STYLE INJECTION * • injectInfoModalStyles * ────────────────────────────────────────────────────────────────────────────── */ /** * injectStyles - inject one or more named CSS blocks into the page * @param {...string} blocks – CSS strings */ function injectStyles(...blocks) { const style = document.createElement('style'); style.textContent = blocks.join('\n'); document.head.appendChild(style); } /* Header Styles */ const CSS_HEADER = ` .mt-title-container { display: flex; align-items: center; font-size: ${CONFIG.FONTS.title}; } .mt-title-icon { width: 20px; height: 20px; margin-right: 6px; } .mt-title-text { font-weight: bold; } .mt-button-container { display: flex; align-items: center; } .mt-button { background: none; border: none; margin-right: 6px; vertical-align: middle; cursor: pointer; padding: 0; } .mt-button-icon { width: 16px; height: 16px; vertical-align: middle; } .mt-toggle-button { background: none; border: none; color: #eee; font-size: 16px; cursor: pointer; padding: 0; vertical-align: middle; } `; const CSS_INFO_MODAL = ` #mt-info-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(255, 255, 255, 0.95); border: 1px solid #ccc; border-radius: 8px; padding: 15px; max-width: 500px; z-index: 11000; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); font-family: Arial, sans-serif; font-size: 12px; color: #333; line-height: 1.25em; overflow-y: auto; max-height: 80vh; } #mt-info-modal h3 { margin-top: 0; margin-bottom: 0.1em; font-size: 1em; } #mt-info-modal h4 { margin-top: 0em; margin-bottom: 0em; font-size: 11.25px; } #mt-info-modal hr { margin: 3px 0; border: none; border-top: 1px solid #ccc; } #mt-info-modal p { margin: 0; } #mt-info-modal p.footnote { font-size: 11px; color: ${COLOR_PALETTE.GRAY_400}; } #mt-info-modal ul { padding-left: 20px; margin: 0.5em 0; } #mt-info-modal li { margin-bottom: 0.25em; } #mt-info-modal button { margin-top: 10px; padding: 5px 10px; border: none; cursor: pointer; background: #222; color: #fff; border-radius: 4px; display: block; font-size: 12px; } #mt-info-modal p[style*="font-size: 11px"] { font-size: 11px; color: gray; } `; const CSS_MISC = ` /* thin line between mission cards */ .mt-divider { border: none; border-top: ${CONFIG.BORDERS.DIVIDER}; margin: 2px 0; } `; const CSS_MISSIONS = ` /* Mission Panel Styles */ .mission-header { display: flex; align-items: center; padding-left: 8px; font-size: ${CONFIG.FONTS.base}; } .mission-header img { width: 20px; height: 20px; margin-right: 6px; vertical-align: middle; } .mission-header span { color: ${CONFIG.COLORS.TEXT}; font-size: ${CONFIG.FONTS.small}; margin-left: 4px; } .mission-desc { padding-left: 16px; font-size: ${CONFIG.FONTS.small}; font-style: italic; color: ${CONFIG.COLORS.MUTED}; margin: 4px 0; } .mission-req { padding-left: 16px; font-size: ${CONFIG.FONTS.base}; } .mission-reward { padding-left: 16px; font-size: ${CONFIG.FONTS.base}; margin-top: 6px; } .mission-reward img { width: 16px; height: 16px; vertical-align: middle; margin-right: 4px; } `; const StyleManager = { injected: new Set(), /** * Inject a single named CSS block. * @param {string} name – Unique identifier for this style block * @param {string} cssText – The actual CSS string */ inject(name, cssText) { if (this.injected.has(name)) return; // prevent duplicates const style = document.createElement('style'); style.textContent = cssText; style.setAttribute('data-style-name', name); document.head.appendChild(style); this.injected.add(name); }, /** * Inject multiple named CSS blocks from an object. * @param {Object<string, string>} styles – { name: cssText } */ injectAll(styles) { for (const [name, cssText] of Object.entries(styles)) { this.inject(name, cssText); } }, remove(name) { document.querySelectorAll(`style[data-style-name="${name}"]`).forEach(style => style.remove()); this.injected.delete(name); }, list() { return Array.from(this.injected); } }; StyleManager.injectAll({ header: CSS_HEADER, infoModal: CSS_INFO_MODAL, misc: CSS_MISC, missions: CSS_MISSIONS }); /** * ────────────────────────────────────────────────────────────────────────────── * SECTION: MAIN ENTRYPOINT * ────────────────────────────────────────────────────────────────────────────── */ /*** MAIN ***/ async function main() { if (!window.erepublik?.settings || !window.csrfToken) { console.warn('[MissionTracker] Missing erepublik settings or CSRF token.'); return; } try { const culture = window.erepublik.settings.culture; const token = window.csrfToken; const host = window.erepublik.settings.hostname; const detailedMissions = await fetchDetailedMissions(culture, token, host); renderMissionPanel(detailedMissions); console.log('%c[MissionTracker] Ready', 'color:#6cf;font-weight:bold'); } catch (err) { console.error('[MissionTracker] Error', err); } } main(); })();