Skip to content

Instantly share code, notes, and snippets.

@c7x43t
Created June 29, 2025 13:26
Show Gist options
  • Save c7x43t/3fcb281a4e536bce6fc231edfa7a1cc4 to your computer and use it in GitHub Desktop.
Save c7x43t/3fcb281a4e536bce6fc231edfa7a1cc4 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name New Userscript
// @namespace http://tampermonkey.net/
// @version 2025-06-13
// @description try to take over the world!
// @author You
// @match *://*.ogame.gameforge.com/*
// @grant GM_xmlhttpRequest
// @connect example.com
// @icon https://www.google.com/s2/favicons?sz=64&domain=gameforge.com
// ==/UserScript==
(function() {
'use strict';
// Domain and path validation
const expectedHost = 'lobby.ogame.gameforge.com';
const expectedPath = '/accounts';
if (location.hostname !== expectedHost || !location.pathname.startsWith(expectedPath)) {
//console.warn(`🚫 Not on expected page: ${expectedHost}${expectedPath}`);
return;
}
/**
* Async helper to wait for a selector to appear
* @param {string} selector - CSS selector
* @param {number} timeout - Max wait time in ms (default: 5000)
* @param {number} interval - Check interval in ms (default: 200)
* @returns {Promise<Element|null>} - Resolves with element or null
*/
async function waitForElement(selector, timeout = 5000, interval = 200) {
const endTime = Date.now() + timeout;
return new Promise(resolve => {
const check = () => {
const element = document.querySelector(selector);
if (element) {
resolve(element);
} else if (Date.now() < endTime) {
setTimeout(check, interval);
} else {
resolve(null);
}
};
check();
});
}
const selector = '#myAccounts [role="rowgroup"] button';
waitForElement(selector).then(button => {
if (button) {
console.log('🎯 Found and clicking account button.');
button.click();
} else {
console.warn('❌ No matching button found after timeout.');
}
});
})();
(function() {
'use strict';
const interceptor = document.createElement('script');
interceptor.textContent = `
(function () {
const originalFetch = window.fetch;
window.fetch = async function(input, init = {}) {
const url = typeof input === 'string' ? input : input.url || '';
const method = (init.method || 'GET').toUpperCase();
const body = init.body || '';
// Check if this is the galaxy fetch request
if (
url.includes('component=galaxy') &&
url.includes('action=fetchGalaxyContent') &&
method === 'POST'
) {
const bodyParams = new URLSearchParams(body);
const galaxy = bodyParams.get('galaxy');
const system = bodyParams.get('system');
// Dispatch a hookable event
window.dispatchEvent(new CustomEvent('intercept:galaxyFetch', {
detail: { galaxy, system }
}));
}
return originalFetch.call(this, input, init);
};
})();
`;
document.documentElement.appendChild(interceptor);
// Optional cleanup
interceptor.remove();
// ✅ Handle the intercepted event
window.addEventListener('intercept:galaxyFetch', (e) => {
const { galaxy, system } = e.detail;
console.log('✅ Intercepted galaxy fetch:', galaxy, system);
// 👉 Trigger side-effect here
// For example: window.externalFetch(...), highlight system, etc.
});
//
async function fetchGalaxyContent(galaxy, system) {
const url = '/game/index.php?page=ingame&component=galaxy&action=fetchGalaxyContent&ajax=1&asJson=1';
const formData = new URLSearchParams();
formData.append('galaxy', galaxy);
formData.append('system', system);
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
},
body: formData.toString(),
credentials: 'same-origin',
});
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
const data = await response.json();
if (!data.success || !data.system || !Array.isArray(data.system.galaxyContent)) {
throw new Error('Unexpected response format.');
}
return data.system.galaxyContent;
} catch (err) {
console.error('Failed to fetch galaxy content:', err);
return [];
}
}
unsafeWindow.externalFetch = function (url, options = {}) {
return new Promise((resolve, reject) => {
const method = options.method || 'GET';
const headers = options.headers || {};
const data = options.body || null;
GM_xmlhttpRequest({
method,
url,
headers,
data,
responseType: options.responseType || 'text',
onload: function (res) {
const fakeResponse = {
ok: res.status >= 200 && res.status < 300,
status: res.status,
statusText: res.statusText,
url: res.finalUrl || url,
text: () => Promise.resolve(res.responseText),
json: () => Promise.resolve(JSON.parse(res.responseText)),
headers: {
get: (key) => res.responseHeaders
?.split('\r\n')
?.find(line => line.toLowerCase().startsWith(key.toLowerCase() + ':'))
?.split(':')
?.slice(1)
?.join(':')
?.trim() || null
}
};
resolve(fakeResponse);
},
onerror: reject,
ontimeout: reject,
onabort: reject
});
});
};
// Example usage of externalFetch to wrap GM request
unsafeWindow.makeExampleComRequest = function () {
return externalFetch('https://example.com')
.then(res => res.text())
};
// Keep existing function exposed
unsafeWindow.fetchGalaxyContent = async function (galaxy, system) {
const url = '/game/index.php?page=ingame&component=galaxy&action=fetchGalaxyContent&ajax=1&asJson=1';
const formData = new URLSearchParams();
formData.append('galaxy', galaxy);
formData.append('system', system);
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
},
body: formData.toString(),
credentials: 'same-origin',
});
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
const data = await response.json();
if (!data.success || !data.system || !Array.isArray(data.system.galaxyContent)) {
throw new Error('Unexpected response format.');
}
return data.system.galaxyContent;
} catch (err) {
console.error('Failed to fetch galaxy content:', err);
return [];
}
};
async function fetchComponentHTML(component) {
const url = `/game/index.php?page=componentOnly&component=${component}&ajax=1`;
const response = await fetch(url, {
method: 'GET',
credentials: 'same-origin',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (!response.ok) throw new Error(`Failed to fetch ${component}: ${response.status}`);
const htmlText = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(htmlText, 'text/html');
return doc;
}
const parseCoords = (text) => {
const match = text?.match(/\[(\d+):(\d+):(\d+)\]/);
if (!match) return { raw: text || '', x: null, y: null, z: null };
return {
raw: text,
x: Number(match[1]),
y: Number(match[2]),
z: Number(match[3]),
};
};
unsafeWindow.parseCoords = parseCoords;
function mergeFleetEvents(events) {
// Separate returns
const returns = events.filter(e => e.returnFlight);
const result = [];
// Process each outbound
for (const o of events) {
if (!o.returnFlight) {
// find matching return
const idx = returns.findIndex(r =>
r.missionType === o.missionType &&
r.shipCount === o.shipCount &&
r.origin.raw === o.origin.raw &&
r.destination.raw === o.destination.raw
);
if (idx !== -1) {
// attach returnTime & remove that return
o.returnTime = returns[idx].arrivalTime;
returns.splice(idx, 1);
}
result.push(o);
}
}
// append any leftover returns
for (const r of returns) {
result.push(r);
}
return result;
}
unsafeWindow.mergeFleetEvents = mergeFleetEvents;
async function getFleetEvents() {
const doc = await fetchComponentHTML('eventList');
const rows = [...doc.querySelectorAll('tr.eventFleet')];
const parseCoords = (text) => {
const match = text?.match(/\[(\d+):(\d+):(\d+)\]/);
if (!match) return { raw: text || '', x: null, y: null, z: null };
return {
raw: text,
x: Number(match[1]),
y: Number(match[2]),
z: Number(match[3]),
};
};
const rawEvents = rows.map(row => {
const id = row.id; // like "eventRow-2832624"
const missionType = Number(row.dataset.missionType);
const returnFlight = row.dataset.returnFlight === 'true';
const arrivalTime = Number(row.dataset.arrivalTime);
const originText = row.querySelector('.coordsOrigin a')?.textContent?.trim() || '';
const destinationText = row.querySelector('.destCoords a')?.textContent?.trim() || '';
const shipCount = row.querySelector('.detailsFleet span')?.textContent?.trim();
const eventIdMatch = id.match(/eventRow-(\d+)/);
const eventId = eventIdMatch ? Number(eventIdMatch[1]) : null;
return {
id,
eventId,
missionType,
returnFlight,
arrivalTime,
origin: parseCoords(originText),
destination: parseCoords(destinationText),
shipCount: Number(shipCount) || 0
};
});
// Separate outbound and return flights
const outboundFlights = new Map();
const finalEvents = [];
for (const event of rawEvents) {
if (!event.returnFlight) {
// Outbound flight — store by mission type and destination
outboundFlights.set(event.eventId, event);
finalEvents.push(event);
} else {
// Return flight — try to match and merge
const match = outboundFlights.get(event.eventId);
if (match) {
match.returnTime = event.arrivalTime;
} else {
// No match, treat as standalone return
finalEvents.push(event);
}
}
}
return mergeFleetEvents(finalEvents);
}
unsafeWindow.getFleetEvents = getFleetEvents;
function getTechnologies(containerSelector = '#technologies ul.icons') {
const container = document.querySelector(containerSelector);
if (!container) return [];
const result = [];
container.querySelectorAll('li.technology').forEach(li => {
const technologyId = li.getAttribute('data-technology');
const upgradeBtn = li.querySelector('button.upgrade');
const tech = {
technology_id: technologyId,
status: li.getAttribute('data-status'),
is_spaceprovider: li.getAttribute('data-is-spaceprovider'),
label: li.getAttribute('aria-label')?.trim(),
tooltip: li.getAttribute('data-tooltip-title'),
current_level: null,
target_level: null,
upgrade: null // default to null
};
// Set current level
const levelEl = li.querySelector('.level, .amount'); // for storage vs units
if (levelEl?.dataset?.value) {
tech.current_level = parseInt(levelEl.dataset.value, 10);
}
// Set target level if tooltip contains "Stufe X ausbauen"
if (upgradeBtn) {
const tooltip = upgradeBtn.getAttribute('data-tooltip-title');
const match = tooltip?.match(/Stufe\s+(\d+)/);
if (match) {
tech.target_level = parseInt(match[1], 10);
}
// Provide upgrade function
tech.upgrade = () => {
upgradeBtn.click();
};
}
result.push(tech);
});
return result;
}
unsafeWindow.getTechnologies = getTechnologies;
function waitFor(selector, timeout = 5000) {
return new Promise((resolve, reject) => {
const interval = 100;
let elapsed = 0;
const check = () => {
const el = document.querySelector(selector);
if (el) return resolve(el);
if ((elapsed += interval) >= timeout) return reject('Timeout waiting for: ' + selector);
setTimeout(check, interval);
};
check();
});
}
unsafeWindow.waitFor = waitFor;
// fetchGalaxyContent(1, 117).then(galaxyPositions => {
// console.log(galaxyPositions);
// });
function injectBotControlPanel() {
async function scanGalaxy(x, y) {
const url = '/game/index.php?page=ingame&component=galaxy&action=fetchGalaxyContent&ajax=1&asJson=1';
console.log(`Scanning: Galaxy ${x} System ${y}`)
const body = new URLSearchParams({ galaxy: x, system: y });
const resp = await fetch(url, {
method: 'POST',
credentials: 'same-origin', // send your session cookies
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
'Accept': '*/*', // or 'application/json, text/javascript, */*; q=0.01'
},
body: body.toString()
});
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}`);
}
// now this will really be JSON
return resp.json();
}
function sleep(ms){
return new Promise(resolve=>setTimeout(resolve,ms))
}
function augmentScanResult(scanResult){
return scanResult.system.galaxyContent.filter(position=>position.planets.find(planet=>planet.planetType===2))
.map(position=>{
const tfPlanet = position.planets.find(planet=>planet.planetType===2)
const resources = {
crystal: {...tfPlanet.resources.crystal},
deuterium: {...tfPlanet.resources.deuterium},
metal: {...tfPlanet.resources.metal}
}
resources.total = Number(resources.crystal.amount) + Number(resources.deuterium.amount) + Number(resources.metal.amount);
position.tf = resources;
return position;
});
}
// read the raw array of entries from localStorage
function getStoredTargets() {
try {
return JSON.parse(localStorage.getItem('bot_control_debree_targets') || '[]');
} catch {
return [];
}
}
// remove entries older than 1h and dedupe by coords (keep newest)
function pruneTargets(arr) {
const cutoff = Date.now() - 60*60*1000; // 1h
const map = new Map();
arr.forEach(e => {
const ts = new Date(e.timestamp).getTime();
if (isNaN(ts) || ts < cutoff) return;
const prev = map.get(e.coords);
if (!prev || ts > new Date(prev.timestamp).getTime()) {
map.set(e.coords, e);
}
});
return Array.from(map.values());
}
// write back to localStorage
function saveTargets(arr) {
localStorage.setItem('bot_control_debree_targets', JSON.stringify(arr));
}
/**
* Scan a vertical strip of galaxy systems, retrying each system up to maxRetries times on error.
* @param {number} x Galaxy coordinate
* @param {number} y Center system coordinate
* @param {number} range How far above/below y to scan
* @param {number} filter Minimum tf.total to include
* @param {number} [maxRetries=3] How many attempts per system
* @param {number} [retryDelay=1000] Delay between retries in ms
*/
/**
* Scan a vertical strip of galaxy systems, retrying each system up to maxRetries times on error.
* Collect raw position objects into validTargets for return, and into storageTargets
* (with their sysY) for building the final coords string at save‐time.
*
* @param {number} x Galaxy coordinate
* @param {number} y Center system coordinate
* @param {number} range How far above/below y to scan
* @param {number} filter Minimum tf.total to include
* @param {number} [maxRetries=3] How many attempts per system
* @param {number} [retryDelay=1000] Delay between retries in ms
* @returns {Promise<Array>} Array of raw position objects passing the filter
*/
async function findTfs(x, y, range, filter, maxRetries = 3, retryDelay = 1000) {
const startY = Math.max(1, y - range);
const endY = Math.min(499, y + range);
const size = endY - startY + 1;
const validTargets = []; // raw position objects for return
const storageTargets = []; // { sysY, position } tuples for saving & dispatch
let progressIndex = 1;
for (let sysY = startY; sysY <= endY; sysY++) {
let scanResult = null;
let attempt = 0;
// —— retry this single system ——
while (attempt < maxRetries) {
try {
scanResult = await scanGalaxy(x, sysY);
break;
} catch (err) {
attempt++;
console.warn(`Error scanning ${x}:${sysY} (try ${attempt}/${maxRetries}):`, err);
if (attempt < maxRetries) {
await new Promise(r => setTimeout(r, retryDelay));
}
}
}
// advance progress bar once, regardless
updateProgress(progressIndex++, size);
if (!scanResult) {
console.error(`→ Giving up on ${x}:${sysY} after ${maxRetries} tries.`);
continue;
}
// throttle so you don’t DDOS
await sleep(500);
// filter & collect raw positions
const filtered = augmentScanResult(scanResult)
.filter(pos => pos.tf.total >= filter);
for (const pos of filtered) {
validTargets.push(pos);
storageTargets.push({ sysY, pos });
}
// —— *live* save & dispatch *so far* ——
const nowISO = new Date().toISOString();
// build entries from all storageTargets so far:
const newEntriesSoFar = storageTargets.map(({ sysY, pos }) => ({
timestamp: nowISO,
coords: `${x}:${sysY}:${pos.position}`,
tf: {
metal: pos.tf.metal.amount,
crystal: pos.tf.crystal.amount,
deuterium: pos.tf.deuterium.amount,
total: pos.tf.total
}
}));
// merge with existing, prune, save, then dispatch
const mergedSoFar = pruneTargets([
...getStoredTargets(),
...newEntriesSoFar
]);
saveTargets(mergedSoFar);
window.dispatchEvent(
new CustomEvent('debreeTargetsFound', { detail: mergedSoFar })
);
}
// — once more at very end (optional) —
console.log("Final scan complete:", validTargets);
return validTargets;
}
const topEl = document.querySelector('#top');
if (!topEl) return;
// Create container
const panel = document.createElement('div');
panel.style.cssText = `
right: -192px;
position: absolute;
width: 164px;
padding: 8px;
border: 2px solid #000;
background: #0d1014;
color: white;
font-family: sans-serif;
font-size: 14px;
z-index: 9999;
`;
panel.innerHTML = `<b><strong>Bot Control</strong></b><div id="bot-control-checkboxes"></div>`;
topEl.appendChild(panel);
const features = [
{ key: 'grow_planet', label: 'Grow planet' },
{ key: 'grow_lifeforms', label: 'Grow Lifeforms' },
{ key: 'auto_collect', label: 'Auto collect resources' },
{ key: 'auto_build', label: 'Auto build next' }
];
const boxContainer = panel.querySelector('#bot-control-checkboxes');
features.forEach(({ key, label }) => {
const wrapper = document.createElement('label');
wrapper.style.display = 'block';
wrapper.style.margin = '4px 0';
wrapper.style.fontSize = '12px';
wrapper.style.color = '#848484';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = localStorage.getItem(`bot_${key}`) === 'true';
checkbox.addEventListener('change', () => {
localStorage.setItem(`bot_${key}`, checkbox.checked);
});
wrapper.appendChild(checkbox);
wrapper.append(` ${label}`);
boxContainer.appendChild(wrapper);
});
// Horizontal spacer
const hr = document.createElement('hr');
hr.style.margin = '8px 0';
panel.appendChild(hr);
// Debris Field Section
const debrisSection = document.createElement('div');
debrisSection.style.fontSize = '12px';
debrisSection.style.color = '#ccc';
debrisSection.innerHTML = `
<div style="font-weight:bold; margin-bottom:4px;">Scan Debris Field</div>
<div style="display:flex; gap:4px; margin-bottom:4px;">
<label style="flex:1;">X: <input type="number" id="debris-x" style="width:100%;" /></label>
<label style="flex:1;">Y: <input type="number" id="debris-y" style="width:100%;" /></label>
</div>
<div style="display:flex; gap:4px; margin-bottom:4px;">
<label style="flex:1;">Range: <input type="number" id="debris-range" style="width:100%;" min="1" max="100" /></label>
<label style="flex:1;">Min TF: <input type="number" id="debris-min-tf" style="width:100%;" min="0" step="100" /></label>
</div>
<div id="progress-indicator" style="text-align:center; font-size:10px; margin-bottom:2px;">0/0</div>
<div style="background:#333; border-radius:4px; overflow:hidden; height:10px;">
<div id="debris-progress" style="background:#2196F3; height:100%; width:0%; transition:width 0.3s;"></div>
</div>
<button id="scan-debris-btn" style="
width: 100%;
padding: 6px;
margin-top: 8px;
background: linear-gradient(90deg, #2196f3, #0d47a1);
border: none;
border-radius: 6px;
color: white;
font-size: 12px;
font-weight: bold;
cursor: pointer;
transition: background 0.3s ease;
">
Scan Debris Fields
</button>
`;
panel.appendChild(debrisSection);
// spacer
const hr2 = document.createElement('hr');
hr2.style.margin = '8px 0';
panel.appendChild(hr2);
// results container
const resultsSection = document.createElement('div');
resultsSection.style.cssText = 'font-size:12px;color:#ccc';
resultsSection.innerHTML = `
<div style="font-weight:bold; margin-bottom:4px;">Scan Results</div>
<ul id="debris-results" style="list-style:none; padding-left:0; max-height:164px; overflow-y:auto"></ul>
`;
panel.appendChild(resultsSection);
// helper to format and render
function fmt(n){ return Number(n).toLocaleString('en-US'); }
function renderResults(arr){
const ul = panel.querySelector('#debris-results');
ul.innerHTML = '';
arr
.sort((a,b)=> b.tf.total - a.tf.total)
.forEach(e => {
const li = document.createElement('li');
li.textContent = `[${e.coords}] ${fmt(e.tf.total)}`;
ul.appendChild(li);
});
}
// initial render from storage
renderResults(pruneTargets(getStoredTargets()));
// re-render on every scan event
window.addEventListener('debreeTargetsFound', ev => {
renderResults(ev.detail);
});
// Example update logic (you can hook this to your scan loop)
function updateProgress(current, total) {
const progressBar = panel.querySelector('#debris-progress');
const progressText = panel.querySelector('#progress-indicator');
const percent = total ? (current / total) * 100 : 0;
progressBar.style.width = `${percent}%`;
progressText.textContent = `${current}/${total}`;
}
async function scanWithRetries(x, y, range, filter, maxRetries = 3, delayMs = 1000) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const results = await findTfs(x, y, range, filter);
// Stop retrying as soon as we get any result (even an empty array)
console.log(`Attempt ${attempt}: Valid debris fields found:`, results);
return results;
} catch (err) {
console.warn(`Attempt ${attempt} failed:`, err);
if (attempt < maxRetries) {
// wait before retrying
await new Promise(res => setTimeout(res, delayMs));
} else {
// all retries exhausted
console.error(`All ${maxRetries} attempts failed:`, err);
throw err;
}
}
}
}
setTimeout(() => {
try {
const planet = getActivePlanet();
if (planet && planet.coords) {
document.getElementById('debris-x').value = planet.coords.x;
document.getElementById('debris-y').value = planet.coords.y;
}
} catch (e) {
console.warn("Couldn't initialize coordinates:", e);
}
// Load from localStorage or default
const savedRange = localStorage.getItem('bot_control_debree_range');
const savedMinTF = localStorage.getItem('bot_control_debree_min_tf');
const rangeInput = document.getElementById('debris-range');
const minTFInput = document.getElementById('debris-min-tf');
rangeInput.value = savedRange !== null ? parseInt(savedRange, 10) : 100;
minTFInput.value = savedMinTF !== null ? parseInt(savedMinTF, 10) : 1000;
// On change, update localStorage
rangeInput.addEventListener('input', () => {
localStorage.setItem('bot_control_debree_range', rangeInput.value);
});
minTFInput.addEventListener('input', () => {
localStorage.setItem('bot_control_debree_min_tf', minTFInput.value);
});
// Hook Scan Debris button to findTfs
panel.querySelector('#scan-debris-btn').addEventListener('click', async () => {
const x = parseInt(document.getElementById('debris-x').value, 10);
const y = parseInt(document.getElementById('debris-y').value, 10);
const range = parseInt(rangeInput.value, 10);
const filter = parseInt(minTFInput.value, 10);
if (isNaN(x) || isNaN(y) || isNaN(range) || isNaN(filter)) {
alert("Please provide valid numeric input.");
return;
}
const btn = panel.querySelector('#scan-debris-btn');
btn.disabled = true;
btn.textContent = 'Scanning...';
try {
const results = await findTfs(x, y, range, filter);
console.log("Valid debris fields found:", results);
} catch (err) {
console.error("Scan failed:", err);
}
btn.disabled = false;
btn.textContent = 'Scan Debris Fields';
});
}, 500);
// Example usage
updateProgress(0, 0); // Remove or control this from your bot logic
}
injectBotControlPanel();
})();
(function runUserscript() {
'use strict';
function getActivePlanet() {
// find the active anchor inside the highlighted planet div
const anchor = document.querySelector(
'#planetList .smallplanet.hightlightPlanet a.planetlink.active'
);
if (!anchor) return null;
const planetDiv = anchor.closest('.smallplanet');
const id = planetDiv.id.replace(/^planet-/, '');
// basic info
const name = anchor.querySelector('.planet-name').textContent.trim();
const coordStr = anchor.querySelector('.planet-koords').textContent.trim();
const coords = parseCoords(coordStr);
const imageUrl = anchor.querySelector('img.planetPic').src;
// tooltip HTML comes from data-tooltip-title
const tooltipHtml = anchor.getAttribute('data-tooltip-title');
const lines = tooltipHtml.split(/<br\s*\/?>/);
// line 1: "<b>Name [x:y:z]</b>"
// line 2: "Lebensform: ...", line 3: "14.660km (106/239)", line 4: "-10 °C bis 30 °C"
const lifeform = lines[1].replace(/^Lebensform:\s*/, '').trim();
// parse diameter and fields
const diameterMatch = lines[2].match(/([\d.,]+)km\s*\((\d+)\/(\d+)\)/);
const diameterKm = diameterMatch
? parseFloat(diameterMatch[1].replace(/\./g, ''))
: null;
const fields = diameterMatch
? { used: +diameterMatch[2], total: +diameterMatch[3] }
: null;
// parse temperature
const tempMatch = lines[3].match(/(-?\d+)\s*°C\s*bis\s*(-?\d+)\s*°C/);
const temperature = tempMatch
? { min: +tempMatch[1], max: +tempMatch[2] }
: null;
// extract all the per-component links (Übersicht, Versorgung, …)
const tmp = document.createElement('div');
tmp.innerHTML = tooltipHtml;
const linkEls = tmp.querySelectorAll('a');
const links = {};
linkEls.forEach(a => {
links[a.textContent.trim()] = a.href;
});
return {
id,
name,
coords,
lifeform,
diameterKm,
fields,
temperature,
imageUrl,
links
};
}
unsafeWindow.getActivePlanet = getActivePlanet;
/**
* Returns the current URL object plus pathname, page and component params.
*
* @param {string} [href=window.location.href] — URL to parse; defaults to current page.
* @returns {{ url: URL, pathname: string, page: string|null, component: string|null }}
*/
function parseUrlLocation(href = window.location.href) {
const url = new URL(href);
const pathname = url.pathname;
const page = url.searchParams.get('page');
const component = url.searchParams.get('component');
return { url, pathname, page, component };
}
unsafeWindow.parseUrlLocation = parseUrlLocation;
})();
(function runUserscript() {
'use strict';
// return;
const url = new URL(window.location.href);
const pathname = url.pathname;
const page = url.searchParams.get('page');
const component = url.searchParams.get('component');
console.log( {url,pathname,page,component})
// Only run if we're on the correct relative page
// if (!(pathname === '/game/index.php' && page === 'ingame' && ['lfbuildings','supplies','facilities'].includes(component))) {
// return;
// }
// async function clickUpgradeButton() {
// const technologies = getTechnologies();
// if(technologies[1].current_level >= 21) return;
// try {
// const button = await waitFor('li.lifeformTech12102 button.upgrade');
// button.click();
// console.log('[AutoUpgrade] ✅ Clicked Biosphären-Farm upgrade button');
// } catch (err) {
// console.warn('[AutoUpgrade] ❌ Could not find upgrade button:', err);
// }
// }
// Run after page load
// if (document.readyState === 'complete') {
// clickUpgradeButton();
// } else {
// window.addEventListener('load', clickUpgradeButton);
// }
////////////////////////////////////
function injectTechnologies(containerSelector = '#technologies ul.icons') {
const container = document.querySelector(containerSelector);
if (!container) return;
container.querySelectorAll('li.technology').forEach((li, index) => {
// Create index badge
const indexBubble = document.createElement('div');
indexBubble.classList.add('tech-index-bubble');
indexBubble.dataset.index = index;
const idxSpan = document.createElement('span');
idxSpan.textContent = index;
indexBubble.appendChild(idxSpan);
li.appendChild(indexBubble);
// Read current level
const stockAmountEl = li.querySelector('.stockAmount');
const currentLevel = parseInt(stockAmountEl?.textContent || '0', 10);
// Build storage key
const { component } = parseUrlLocation();
const activePlanet = getActivePlanet();
const key = `${component}_${activePlanet.coords.raw}_${index}`;
// Try to load saved target, fallback to currentLevel
const saved = localStorage.getItem(key);
const initialTarget = saved !== null ? parseInt(saved, 10) : currentLevel;
// Create target row
const targetWrapper = document.createElement('span');
targetWrapper.classList.add('level', 'tech-target');
targetWrapper.dataset.value = initialTarget;
targetWrapper.dataset.index = index;
const labelSpan = document.createElement('span');
labelSpan.textContent = 'Target:';
const inputSpan = document.createElement('span');
inputSpan.classList.add('stockAmount', 'tech-input');
inputSpan.contentEditable = 'true';
inputSpan.textContent = initialTarget;
inputSpan.setAttribute('type', 'number');
// Wheel listener: clamp >= currentLevel, save & emit
inputSpan.addEventListener('wheel', e => {
e.preventDefault();
const delta = e.deltaY < 0 ? 1 : -1;
let val = parseInt(inputSpan.textContent, 10) || currentLevel;
val = Math.max(currentLevel, val + delta);
inputSpan.textContent = val;
localStorage.setItem(key, val);
const ev = new CustomEvent('targetLevelChanged', {
detail: { key, value: val }
});
inputSpan.dispatchEvent(ev);
});
targetWrapper.appendChild(labelSpan);
targetWrapper.appendChild(inputSpan);
li.appendChild(targetWrapper);
});
// apply all the CSS and icon-swap at once
applyTechnologyStyles();
}
function applyTechnologyStyles() {
// 1) Inject global <style> rules
const style = document.createElement('style');
style.textContent = `
#supplies #technologies li.metalStorage::before,
#supplies #technologies li.crystalStorage::before,
#supplies #technologies li.crystalStorage::after,
#supplies #technologies li.deuteriumStorage::before {
display: contents;
}
#supplies #technologies span.metalStorage,
#supplies #technologies span.crystalStorage,
#supplies #technologies span.deuteriumStorage {
margin-bottom: 0px;
}
`;
document.head.appendChild(style);
// 2) Per-<li> styles and class swaps
document.querySelectorAll('li.technology').forEach(li => {
// relative positioning
li.style.position = 'relative';
});
// 3) Swap all small sprites to medium
document.querySelectorAll('span.sprite_small.small').forEach(icon => {
icon.classList.remove('sprite_small', 'small');
icon.classList.add('sprite_medium', 'medium');
});
// 4) Style every index badge
document.querySelectorAll('.tech-index-bubble').forEach(b => {
// top: 0;
// right: 0;
b.style.cssText = `
bottom: 25px;
left: 4px;
position: absolute;
padding: 4px;
background: black;
width: 0.6rem;
height: 0.6rem;
z-index: 10;
border-radius: 8px;
color: white;
font-size: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
`;
});
// 5) Style every injected target row
document.querySelectorAll('.tech-target').forEach(wrapper => {
wrapper.style.cssText = `
display: flex;
width: 100%;
justify-content: space-between;
padding: 7px;
box-sizing: border-box;
cursor: auto;
`;
});
// 6) Style every editable input span
document.querySelectorAll('.tech-input').forEach(input => {
input.style.cssText = `
min-width: 2rem;
text-align: right;
`;
});
}
unsafeWindow.injectTechnologies = injectTechnologies;
console.log("injectTechnologies");
injectTechnologies();
})();
(function runUserscript() {
function getCurrentLevel(li) {
const levelEl = li.querySelector('.stockAmount, .level');
return parseInt(levelEl?.textContent?.trim() || '0', 10);
}
/**
* Generates a storage key for a technology based on its index,
* the current component, and the active planet.
* @param {number} index - The index of the technology element.
* @returns {string} The storage key.
*/
function getStorageKey(index) {
// Use parseUrlLocation() if available; else fallback to default.
const { component = 'default' } =
typeof parseUrlLocation === 'function' ? parseUrlLocation() : {};
// Use getActivePlanet() if available; else fallback to unknown.
const activePlanet =
typeof getActivePlanet === 'function'
? getActivePlanet()
: { coords: { raw: 'unknown' } };
return `${component}_${activePlanet.coords.raw}_${index}`;
}
/**
* Loads the saved target level from storage for a given technology index,
* or returns the fallback level (usually the current level) if none is saved.
* @param {number} index - The index of the technology element.
* @param {number} fallbackLevel - The level to use if no saved target is found.
* @returns {number} The target level.
*/
function loadTargetLevel(index, fallbackLevel) {
const key = getStorageKey(index);
const saved = localStorage.getItem(key);
return saved !== null ? parseInt(saved, 10) : fallbackLevel;
}
/**
* Saves a new target level to localStorage for a given technology index.
* @param {number} index - The index of the technology element.
* @param {number} targetLevel - The new target level to save.
*/
function saveTargetLevel(index, targetLevel) {
const key = getStorageKey(index);
localStorage.setItem(key, targetLevel);
}
function determineTechnologyUpgrades(containerSelector = '#technologies ul.icons') {
const container = document.querySelector(containerSelector);
if (!container) return [];
const result = [];
container.querySelectorAll('li.technology').forEach((li, index) => {
const technologyId = li.getAttribute('data-technology');
const upgradeBtn = li.querySelector('button.upgrade');
const tech = {
technology_id: technologyId,
status: li.getAttribute('data-status'),
is_spaceprovider: li.getAttribute('data-is-spaceprovider'),
label: li.getAttribute('aria-label')?.trim(),
tooltip: li.getAttribute('data-tooltip-title'),
current_level: null,
target_level: null,
delta_level: null,
upgrade: null
};
// Current level from data-value or fallback
const levelEl = li.querySelector('.level, .amount');
if (levelEl?.dataset?.value) {
tech.current_level = parseInt(levelEl.dataset.value, 10);
} else {
tech.current_level = getCurrentLevel(li); // fallback helper
}
// Target level from button tooltip if present
if (upgradeBtn) {
const tooltip = upgradeBtn.getAttribute('data-tooltip-title');
const match = tooltip?.match(/Stufe\s+(\d+)/);
if (match) {
tech.upgrade = () => upgradeBtn.click();
}
}
// Fallback to stored target if no target_level from tooltip
if (tech.target_level === null) {
tech.target_level = loadTargetLevel(index, tech.current_level);
}
// Compute delta
tech.delta_level = tech.target_level - tech.current_level;
result[index] = tech;
});
return result;
}
/* Register Functions on unsafeWindow */
unsafeWindow.getCurrentLevel = getCurrentLevel;
unsafeWindow.getStorageKey = getStorageKey;
unsafeWindow.loadTargetLevel = loadTargetLevel;
unsafeWindow.saveTargetLevel = saveTargetLevel;
unsafeWindow.determineTechnologyUpgrades = determineTechnologyUpgrades;
const { url, pathname, page, component } = parseUrlLocation();
if (!(pathname === '/game/index.php' && page === 'ingame' && ['lfbuildings','supplies','facilities'].includes(component))) {
return;
}
function upgradeNextBest(techList = determineTechnologyUpgrades()) {
const sortedUpgradable = techList
.filter(e => typeof e.upgrade === 'function' && e.delta_level > 0)
.sort((a, b) => b.delta_level - a.delta_level);
const best = sortedUpgradable[0];
console.log(
`[upgradeNextBest] ✅ Upgrading "${best.label}" from level ${best.current_level} to ${best.current_level + 1} | Target: ${best.target_level}`
);
best.upgrade();
}
unsafeWindow.upgradeNextBest = upgradeNextBest;
upgradeNextBest();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment