Created
June 29, 2025 13:26
-
-
Save c7x43t/3fcb281a4e536bce6fc231edfa7a1cc4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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