Greasy Fork is available in English.
Status panel for OGame running in Tampermonkey.
// ==UserScript==
// @name GROS
// @namespace local.ogame.status-panel
// @version 0.5.0
// @description Status panel for OGame running in Tampermonkey.
// @author GR
// @license MIT
// @match https://*.ogame.gameforge.com/game/index.php*
// @grant GM_addStyle
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
const APP_ID = 'ogame-status-panel';
const SETTINGS = {
// Language for panel labels and tooltips: 'auto', 'pl', or 'en'.
language: 'auto',
};
const SESSION_ID = `${Date.now()}:${Math.random().toString(16).slice(2)}`;
const MOON_ACTIVITY_RUN_LIMIT_MS = 60 * 1000;
const INITIAL_SCAN_RETRY_LIMIT = 5;
const INITIAL_SCAN_RETRY_DELAY_MS = 1000;
const EMPIRE_CACHE_KEY = `${APP_ID}:empire-ships:v1`;
const LOCATION_CACHE_KEY = `${APP_ID}:location-ships:v1`;
const EXPEDITION_SLOTS_CACHE_KEY = `${APP_ID}:expedition-slots:v1`;
const PANEL_CACHE_KEY = `${APP_ID}:panel:v1`;
const FLEET_INVENTORY_CACHE_KEY = `${APP_ID}:fleet-inventory:v1`;
const FLEET_INVENTORY_CACHE_VERSION = 4;
const EMPIRE_CACHE_VERSION = 3;
const LOCATION_CACHE_VERSION = 3;
const LOCALIZED_SHIP_NAMES_CACHE_KEY = `${APP_ID}:localized-ship-names:v1`;
const MOON_ACTIVITY_CACHE_KEY = `${APP_ID}:moon-activity:v1`;
const DISCOVERY_MISSION_TYPE = '18';
const EXPEDITION_MISSION_TYPES = new Set(['15', DISCOVERY_MISSION_TYPE]);
const MOON_ACTIVITY_FRESH_MS = 15 * 60 * 1000;
const MOON_ACTIVITY_VISIBLE_MS = 60 * 60 * 1000;
const SHIP_IDS = new Set(['202', '203', '204', '205', '206', '207', '208', '209', '210', '211', '213', '214', '215', '218', '219']);
const AGO_CIVIL_SHIP_BY_ROW = ['202', '203', '208', '209', '210'];
const AGO_COMBAT_SHIP_BY_ROW = ['204', '205', '206', '207', '211', '213', '214', '215', '218', '219'];
const SHIP_NAME_PATTERNS = [
{ id: '202', pattern: /\bsmall cargo\b/ },
{ id: '203', pattern: /\blarge cargo\b/ },
{ id: '204', pattern: /\blight fighter\b/ },
{ id: '205', pattern: /\bheavy fighter\b/ },
{ id: '206', pattern: /\bcruiser\b/ },
{ id: '207', pattern: /\bbattleship\b/ },
{ id: '208', pattern: /\bcolony ship\b/ },
{ id: '209', pattern: /\brecycler\b/ },
{ id: '210', pattern: /\bespionage probe\b/ },
{ id: '211', pattern: /\bbomber\b/ },
{ id: '213', pattern: /\bdestroyer\b/ },
{ id: '214', pattern: /\bdeathstar\b/ },
{ id: '215', pattern: /\bbattlecruiser\b/ },
{ id: '218', pattern: /\breaper\b/ },
{ id: '219', pattern: /\bpathfinder\b/ },
];
const STATUS_COLORS = {
good: '#86c977',
caution: '#d7c35a',
warn: '#d79a4a',
bad: '#c96a62',
unknown: '#7f8790',
timerBlue: '#57ddff',
};
const TIMER_TONES = {
nextExpedition: [
{ maxMs: 5 * 60 * 1000, tone: 'caution' },
],
fleetSave: [
{ maxMs: 5 * 60 * 1000, tone: 'bad' },
{ maxMs: 15 * 60 * 1000, tone: 'warn' },
{ maxMs: 30 * 60 * 1000, tone: 'caution' },
],
};
let pageObserver = null;
let fleetObserver = null;
let moonDotsObserver = null;
let scheduledMoonDotsRender = null;
let scheduledDomScan = null;
let panelTooltip = null;
let panelTooltipTarget = null;
const Storage = {
read(key, fallback = null) {
try {
const raw = window.localStorage.getItem(key);
if (!raw) {
return fallback;
}
const parsed = JSON.parse(raw);
return parsed === null || parsed === undefined ? fallback : parsed;
} catch {
return fallback;
}
},
write(key, value) {
try {
window.localStorage.setItem(key, JSON.stringify(value));
return true;
} catch {
return false;
}
},
remove(key) {
try {
window.localStorage.removeItem(key);
return true;
} catch {
return false;
}
},
};
const I18N = {
pl: {
sectionExpeditions: 'Ekspedycje',
sectionFleet: 'Flota',
sectionMoonActivity: 'Aktywność księżyców',
labelReturn: 'Powrót:',
labelSlots: 'Sloty:',
labelInFlight: 'W powietrzu:',
labelFleetSave: 'FS:',
labelLast: 'Ostatnia:',
dataQualityMissing: 'Jakość danych: brak pełnych danych.',
dataQualityNeedsAttention: 'Jakość danych: wynik wymaga uwagi.',
dataQualityStale: 'Jakość danych: dane są starsze niż 15 minut.',
dataQualityGood: 'Jakość danych: wynik wiarygodny.',
fleetSaveLanding: 'Lądowanie',
fleetSaveReturn: 'Powrót',
location: 'Lokalizacja',
noFleetRowsData: 'Brak danych o pojedynczych flotach w locie.',
largestFleet: 'Najwieksza flota',
time: 'Czas',
arrival: 'Dotarcie',
direction: 'Kierunek',
returnDirection: 'powrot',
outboundDirection: 'dolot do celu',
mission: 'Misja',
route: 'Trasa',
shareTotal: 'Udzial w calosci',
shareInFlight: 'Udzial we flocie w locie',
description: 'Opis',
noFullShipData: 'Brak pelnych danych o statkach.',
inFlight: 'W locie',
coverage: 'Pokrycie',
totalKnown: 'Znane lacznie',
sources: 'Zrodla',
none: 'brak',
stationary: 'Na miejscu',
audit: 'Audyt',
flight: 'lot',
rows: 'wiersze',
ships: 'statki',
occupiedExpeditions: 'Zajete ekspedycje',
source: 'Zrodlo',
lastUpdate: 'Ostatnia aktualizacja',
empireRead: 'Imperium pobrane',
empireParsed: 'Imperium sparsowane',
missionsInFlight: 'Misje w powietrzu',
empireCache: 'Cache Imperium',
empireFleetStatus: 'Status Imperium/Flota',
eventlistStatus: 'Status eventlisty',
unknownRoute: 'trasa nieznana',
returnPrefix: 'Powrót',
activeMoons: 'Aktywowane moony',
runStart: 'Start obchodu',
fullRun: 'Pelny obchod',
oldestActivity: 'Najstarsza aktywnosc',
missingMoons: 'Brakujace moony:',
noData: 'Brak danych.',
unparsedFlightRowsKept: 'Wykryto floty w powietrzu, ale nie odczytano liczby statkow. Zachowano ostatni poprawny odczyt.',
unparsedFlightRows: 'Wykryto floty w powietrzu, ale nie odczytano liczby statkow.',
incompleteEmpireWarning: 'Nie odczytano jeszcze kompletnego Imperium, wiec procent jest ukryty.',
insufficientEmpireRead: 'Odczyt {source} nie wystarcza do policzenia procentu.',
sourceView: 'Widok',
locationCacheStationary: 'Cache lokalizacji: statki stojace',
locationCache: 'Cache lokalizacji',
empireCacheLabel: 'Imperium cache',
cacheEmpty: 'cache pusty',
cacheInvalid: 'cache niepoprawny',
cacheOk: 'cache OK: {total}',
cacheSaved: 'cache zapisany: {total}',
cacheWriteError: 'cache zapis blad',
},
en: {
sectionExpeditions: 'Expeditions',
sectionFleet: 'Fleet',
sectionMoonActivity: 'Moon activity',
labelReturn: 'Return:',
labelSlots: 'Slots:',
labelInFlight: 'In flight:',
labelFleetSave: 'FS:',
labelLast: 'Last:',
dataQualityMissing: 'Data quality: missing complete data.',
dataQualityNeedsAttention: 'Data quality: result needs attention.',
dataQualityStale: 'Data quality: data is older than 15 minutes.',
dataQualityGood: 'Data quality: result is reliable.',
fleetSaveLanding: 'Landing',
fleetSaveReturn: 'Return',
location: 'Location',
noFleetRowsData: 'No data for individual fleets in flight.',
largestFleet: 'Largest fleet',
time: 'Time',
arrival: 'Arrival',
direction: 'Direction',
returnDirection: 'return',
outboundDirection: 'outbound',
mission: 'Mission',
route: 'Route',
shareTotal: 'Share of total',
shareInFlight: 'Share of in-flight fleet',
description: 'Description',
noFullShipData: 'Missing complete ship data.',
inFlight: 'In flight',
coverage: 'Coverage',
totalKnown: 'Known total',
sources: 'Sources',
none: 'none',
stationary: 'Stationary',
audit: 'Audit',
flight: 'flight',
rows: 'rows',
ships: 'ships',
occupiedExpeditions: 'Occupied expeditions',
source: 'Source',
lastUpdate: 'Last update',
empireRead: 'Empire read',
empireParsed: 'Empire parsed',
missionsInFlight: 'Missions in flight',
empireCache: 'Empire cache',
empireFleetStatus: 'Empire/Fleet status',
eventlistStatus: 'Event list status',
unknownRoute: 'unknown route',
returnPrefix: 'Return',
activeMoons: 'Activated moons',
runStart: 'Round start',
fullRun: 'Full round',
oldestActivity: 'Oldest activity',
missingMoons: 'Missing moons:',
noData: 'No data.',
unparsedFlightRowsKept: 'Fleets in flight were detected, but ship counts were not read. The last valid reading was kept.',
unparsedFlightRows: 'Fleets in flight were detected, but ship counts were not read.',
incompleteEmpireWarning: 'Complete Empire data has not been read yet, so the percentage is hidden.',
insufficientEmpireRead: '{source} read is not enough to calculate the percentage.',
sourceView: 'View',
locationCacheStationary: 'Location cache: stationary ships',
locationCache: 'Location cache',
empireCacheLabel: 'Empire cache',
cacheEmpty: 'cache empty',
cacheInvalid: 'cache invalid',
cacheOk: 'cache OK: {total}',
cacheSaved: 'cache saved: {total}',
cacheWriteError: 'cache write error',
},
};
function t(key, params = {}) {
const dictionary = I18N[getUiLanguage()] || I18N.en;
const fallback = I18N.en[key] || key;
return String(dictionary[key] || fallback).replace(/\{(\w+)\}/g, (_, name) => (
params[name] === undefined || params[name] === null ? '' : String(params[name])
));
}
function getUiLanguage() {
if (SETTINGS.language === 'pl' || SETTINGS.language === 'en') {
return SETTINGS.language;
}
return detectUiLanguage();
}
function detectUiLanguage() {
const candidates = [
document.documentElement?.lang,
document.querySelector('html')?.getAttribute('lang'),
navigator.language,
...(navigator.languages || []),
].filter(Boolean).map((value) => String(value).toLowerCase());
return candidates.some((value) => value.startsWith('pl')) ? 'pl' : 'en';
}
const state = {
expeditions: [],
expeditionSlots: {
used: 0,
total: null,
source: '',
lastUpdatedAt: null,
},
fleetInventory: {
inFlight: 0,
totalKnown: 0,
stationary: 0,
flightPercent: null,
complete: false,
coverage: 'unknown',
warning: 'No data.',
sourceLabels: [],
sourceAudit: [],
inFlightCounts: {},
stationaryCounts: {},
largestInFlight: null,
lastUpdatedAt: null,
},
lastScanAt: null,
lastEmpireReadAt: null,
lastEmpireParsedAt: null,
lastMissionsReadAt: null,
lastCacheStatus: '',
missionScanReliable: false,
lastRowsSeen: 0,
lastReturnCandidates: 0,
serverTimeOffsetMs: readServerTimeOffset(),
localizedShipNames: readLocalizedShipNamesCache(),
moonActivity: readMoonActivityCache(),
};
function init() {
state.fleetInventory.warning = t('noData');
hydrateStateFromPanelCache();
startEarlyMoonDotsRender();
updateMoonActivity();
scanCurrentShipPage();
startEarlyPanelMount();
installPanelTooltips();
window.setInterval(refreshPanel, 1000);
runInitialScansWithRetry();
}
function startEarlyPanelMount() {
ensurePanel();
refreshPanel();
if (document.body) {
window.setTimeout(startInitialScans, 0);
return;
}
const observer = new MutationObserver(() => {
if (!document.body) {
return;
}
observer.disconnect();
ensurePanel();
refreshPanel();
window.setTimeout(startInitialScans, 0);
});
observer.observe(document.documentElement, { childList: true });
document.addEventListener('DOMContentLoaded', () => {
observer.disconnect();
ensurePanel();
refreshPanel();
window.setTimeout(startInitialScans, 0);
}, { once: true });
}
function startInitialScans() {
updateMoonActivity();
scanCurrentShipPage();
observePageChanges();
if (!isOverviewPage()) {
return;
}
observeFleetEvents();
scanFleetEvents();
scanShipInventory();
refreshPanel();
}
function runInitialScansWithRetry(attempt = 1) {
ensurePanel();
updateMoonActivity();
scanCurrentShipPage();
scanFleetEvents();
scanShipInventory();
refreshPanel();
if (isDomReadyForCurrentPage() || attempt >= INITIAL_SCAN_RETRY_LIMIT) {
return;
}
window.setTimeout(() => runInitialScansWithRetry(attempt + 1), INITIAL_SCAN_RETRY_DELAY_MS);
}
function startEarlyMoonDotsRender() {
renderMoonActivityDots(state.moonActivity);
if (document.querySelector('#planetList .moonlink')) {
scheduleMoonDotsRender();
return;
}
if (moonDotsObserver || !document.documentElement) {
return;
}
moonDotsObserver = new MutationObserver(() => {
if (!document.querySelector('#planetList .moonlink')) {
return;
}
moonDotsObserver.disconnect();
moonDotsObserver = null;
scheduleMoonDotsRender();
});
moonDotsObserver.observe(document.documentElement, {
childList: true,
subtree: true,
});
}
function scheduleMoonDotsRender() {
if (scheduledMoonDotsRender) {
return;
}
scheduledMoonDotsRender = window.setTimeout(() => {
scheduledMoonDotsRender = null;
updateMoonActivity();
renderMoonActivityDots(state.moonActivity);
}, 0);
}
function isDomReadyForCurrentPage() {
const component = getCurrentComponent();
if (isOverviewPage()) {
return Boolean(document.getElementById(APP_ID));
}
if (component === 'empire') {
return document.querySelectorAll('.planet[id^="planet"], .values.ships.groupships').length > 0;
}
if (component === 'fleet' || component === 'fleetdispatch') {
return Boolean(document.querySelector('#slots, #technologies, [name="ogame-planet-id"]'));
}
return Boolean(document.body);
}
function ensurePanel() {
if (!isOverviewPage()) {
removePanel();
return;
}
const panel = document.getElementById(APP_ID) || createPanel();
const target = getPanelTarget();
if (panel && target && !isPanelPlaced(panel, target)) {
insertPanel(panel, target);
}
updatePanelLanguage(panel);
updatePanelOffset(panel);
}
function createPanel() {
if (!isOverviewPage()) {
return null;
}
const target = getPanelTarget();
if (!target) {
return null;
}
const existing = document.getElementById(APP_ID);
if (existing) {
return existing;
}
const panel = document.createElement('div');
panel.id = APP_ID;
panel.className = 'ogh-production-row';
const next = state.expeditions[0] || null;
const displayFleetInventory = getDisplayFleetInventory();
panel.innerHTML = `
<div class="ogh-status-column">
<div class="ogh-status-component injectedComponent parent overview">
<div class="content-box-s">
<div class="header">
<h3 data-i18n="sectionExpeditions">${escapeHtml(t('sectionExpeditions'))}</h3>
</div>
<div class="content">
<table cellspacing="0" cellpadding="0" class="construction active ogh-status-table">
<tbody>
<tr>
<td colspan="2" class="idle">
<div class="ogh-panel-main">
<table cellspacing="0" cellpadding="0" class="ogh-metrics-table">
<tbody>
<tr>
<td class="ogh-label-cell"><span class="ogh-label" data-i18n="labelReturn">${escapeHtml(t('labelReturn'))}</span></td>
<td class="ogh-value-cell"><span class="ogh-time ogh-timer-blue" data-role="next-expedition-time">${next ? escapeHtml(formatCountdown(next.arrivalAt - getNow())) : '-'}</span></td>
</tr>
<tr>
<td class="ogh-label-cell"><span class="ogh-label" data-i18n="labelSlots">${escapeHtml(t('labelSlots'))}</span></td>
<td class="ogh-value-cell"><span class="ogh-time" data-role="expedition-slots">${escapeHtml(formatExpeditionSlots(state.expeditionSlots))}</span></td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="footer"></div>
</div>
</div>
</div>
<div class="ogh-status-column">
<div class="ogh-status-component injectedComponent parent overview">
<div class="content-box-s">
<div class="header">
<h3 data-i18n="sectionFleet">${escapeHtml(t('sectionFleet'))}</h3>
</div>
<div class="content">
<table cellspacing="0" cellpadding="0" class="construction active ogh-status-table">
<tbody>
<tr>
<td colspan="2" class="idle">
<div class="ogh-panel-main">
<table cellspacing="0" cellpadding="0" class="ogh-metrics-table">
<tbody>
<tr>
<td class="ogh-label-cell"><span class="ogh-label" data-i18n="labelInFlight">${escapeHtml(t('labelInFlight'))}</span></td>
<td class="ogh-value-cell"><span class="ogh-time" data-role="fleet-flight-percent">${escapeHtml(formatFleetFlightPercent(displayFleetInventory))}</span><span class="ogh-data-quality" data-role="fleet-data-quality">●</span></td>
</tr>
<tr>
<td class="ogh-label-cell"><span class="ogh-label" data-i18n="labelFleetSave">${escapeHtml(t('labelFleetSave'))}</span></td>
<td class="ogh-value-cell"><span class="ogh-time ogh-timer-blue" data-role="largest-fleet-percent">${escapeHtml(formatLargestInFlightTime(displayFleetInventory))}</span></td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="footer"></div>
</div>
</div>
</div>
<div class="ogh-status-column">
<div class="ogh-status-component injectedComponent parent overview">
<div class="content-box-s">
<div class="header">
<h3 data-i18n="sectionMoonActivity">${escapeHtml(t('sectionMoonActivity'))}</h3>
</div>
<div class="content">
<table cellspacing="0" cellpadding="0" class="construction active ogh-status-table">
<tbody>
<tr>
<td colspan="2" class="idle">
<div class="ogh-panel-main">
<table cellspacing="0" cellpadding="0" class="ogh-metrics-table">
<tbody>
<tr>
<td class="ogh-label-cell"><span class="ogh-label" data-i18n="labelLast">${escapeHtml(t('labelLast'))}</span></td>
<td class="ogh-value-cell"><span class="ogh-time" data-role="moon-activity-timer" data-ogh-tooltip="${escapeHtml(buildMoonActivityTitleV2(state.moonActivity))}">${escapeHtml(formatMoonActivityTimer(state.moonActivity))}</span></td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="footer"></div>
</div>
</div>
</div>
`;
insertPanel(panel, target);
return panel;
}
function updatePanelLanguage(panel) {
if (!panel) {
return;
}
for (const element of panel.querySelectorAll('[data-i18n]')) {
element.textContent = t(element.dataset.i18n);
}
}
function getPanelTarget() {
return document.querySelector('#overviewcomponent')
|| document.querySelector('#productionboxBottom')
|| null;
}
function insertPanel(panel, target) {
if (!target) {
return;
}
if (target?.id === 'overviewcomponent' || target?.id === 'productionboxBottom') {
target.insertAdjacentElement('afterend', panel);
} else {
target.appendChild(panel);
}
}
function isPanelPlaced(panel, target) {
if (target?.id === 'overviewcomponent' || target?.id === 'productionboxBottom') {
return target.nextElementSibling === panel;
}
return panel.parentElement === target;
}
function updatePanelOffset(panel) {
const overview = document.querySelector('#overviewcomponent');
if (!overview || !panel || panel.previousElementSibling !== overview || getCurrentPlanetType() !== 'moon') {
panel?.style.removeProperty('--ogh-overview-offset');
return;
}
const overviewBottom = overview.getBoundingClientRect().bottom;
const visibleBottom = getOverviewVisibleBottom(overview);
const emptyBottomSpace = Math.min(60, Math.max(0, Math.round(overviewBottom - visibleBottom)));
const correctedSpace = Math.max(0, emptyBottomSpace - 27);
if (correctedSpace > 2) {
panel.style.setProperty('--ogh-overview-offset', `${-correctedSpace}px`);
} else {
panel.style.removeProperty('--ogh-overview-offset');
}
}
function getOverviewVisibleBottom(overview) {
let bottom = overview.getBoundingClientRect().top;
for (const element of overview.querySelectorAll('*')) {
if (!isVisibleOverviewContentElement(element)) {
continue;
}
const rect = element.getBoundingClientRect();
bottom = Math.max(bottom, rect.bottom);
}
return bottom;
}
function isVisibleOverviewContentElement(element) {
if (element.id === APP_ID || ['SCRIPT', 'STYLE', 'TEMPLATE'].includes(element.tagName)) {
return false;
}
const style = window.getComputedStyle(element);
if (style.display === 'none' || style.visibility === 'hidden' || style.position === 'fixed') {
return false;
}
const rect = element.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) {
return false;
}
if (hasVisibleElementChild(element)) {
return false;
}
return element.textContent.trim() !== ''
|| ['IMG', 'CANVAS', 'SVG', 'FIGURE'].includes(element.tagName);
}
function hasVisibleElementChild(element) {
for (const child of element.children) {
const style = window.getComputedStyle(child);
const rect = child.getBoundingClientRect();
if (style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0) {
return true;
}
}
return false;
}
function removePanel() {
document.getElementById(APP_ID)?.remove();
}
function isOverviewPage() {
const pageValue = typeof currentPage !== 'undefined' ? currentPage : '';
const params = new URLSearchParams(window.location.search);
const wantsOverview = pageValue === 'overview' || params.get('component') === 'overview';
return wantsOverview && Boolean(document.querySelector('#overviewcomponent') || document.querySelector('#productionboxBottom'));
}
function getCurrentPlanetType() {
return document.querySelector('[name="ogame-planet-type"]')?.content || '';
}
function observeFleetEvents() {
if (fleetObserver) {
fleetObserver.disconnect();
}
const target = document.querySelector('#eventboxContent') || document.querySelector('#message-wrapper') || document.body;
fleetObserver = new MutationObserver(() => {
scanFleetEvents();
refreshPanel();
});
fleetObserver.observe(target, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
attributeFilter: ['data-time', 'data-arrival-time', 'class'],
});
}
function observePageChanges() {
if (!document.body) {
return;
}
if (pageObserver) {
pageObserver.disconnect();
}
pageObserver = new MutationObserver((mutations) => {
if (mutations.every((mutation) => isOwnPanelMutation(mutation))) {
return;
}
scheduleDomScan();
});
pageObserver.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
attributeFilter: ['class', 'data-time', 'data-arrival-time', 'data-return-time', 'data-end-time'],
});
}
function isOwnPanelMutation(mutation) {
const target = mutation.target?.nodeType === Node.ELEMENT_NODE
? mutation.target
: mutation.target?.parentElement;
return Boolean(target?.closest?.(`#${APP_ID}, .ogh-moon-activity-dot, .ogh-tooltip`));
}
function installPanelTooltips() {
document.addEventListener('mouseover', handlePanelTooltipEnter, true);
document.addEventListener('focusin', handlePanelTooltipEnter, true);
document.addEventListener('mousemove', handlePanelTooltipMove, true);
document.addEventListener('mouseout', handlePanelTooltipLeave, true);
document.addEventListener('focusout', handlePanelTooltipLeave, true);
}
function handlePanelTooltipEnter(event) {
const target = findPanelTooltipTarget(event.target);
if (!target || target === panelTooltipTarget) {
return;
}
const title = target.getAttribute('title') || target.dataset.oghTooltip || '';
if (!title) {
return;
}
target.dataset.oghTooltip = title;
target.removeAttribute('title');
showPanelTooltip(target, title, event);
}
function handlePanelTooltipMove(event) {
if (panelTooltipTarget && event.type === 'mousemove') {
positionPanelTooltip(event);
}
}
function handlePanelTooltipLeave(event) {
const target = findPanelTooltipTarget(event.target);
if (!target || target !== panelTooltipTarget) {
return;
}
hidePanelTooltip();
}
function findPanelTooltipTarget(target) {
const element = target?.nodeType === Node.ELEMENT_NODE ? target : target?.parentElement;
return element?.closest?.(`#${APP_ID} [title], #${APP_ID} [data-ogh-tooltip]`) || null;
}
function showPanelTooltip(target, title, event) {
panelTooltipTarget = target;
if (!panelTooltip) {
panelTooltip = document.createElement('div');
panelTooltip.className = 'ogh-tooltip';
document.body.appendChild(panelTooltip);
}
panelTooltip.textContent = title;
panelTooltip.style.display = 'block';
positionPanelTooltip(event);
}
function hidePanelTooltip() {
if (panelTooltip) {
panelTooltip.style.display = 'none';
}
panelTooltipTarget = null;
}
function positionPanelTooltip(event) {
if (!panelTooltip) {
return;
}
const offset = 12;
const rect = panelTooltip.getBoundingClientRect();
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
let left = event.clientX + offset;
let top = event.clientY + offset;
if (left + rect.width > viewportWidth - 8) {
left = Math.max(8, event.clientX - rect.width - offset);
}
if (top + rect.height > viewportHeight - 8) {
top = Math.max(8, event.clientY - rect.height - offset);
}
panelTooltip.style.left = `${left}px`;
panelTooltip.style.top = `${top}px`;
}
function scheduleDomScan() {
if (scheduledDomScan) {
return;
}
scheduledDomScan = window.setTimeout(() => {
scheduledDomScan = null;
updateMoonActivity();
scanCurrentShipPage();
scanFleetEvents();
scanShipInventory();
refreshPanel();
}, 100);
}
function scanFleetEvents() {
if (!isOverviewPage()) {
return;
}
const rows = getFleetEventRows(document);
state.lastRowsSeen = rows.length;
if (rows.length <= 0 && preserveCachedNextExpedition()) {
state.lastScanAt = new Date();
updateExpeditionSlots();
return;
}
if (rows.length > 0) {
state.missionScanReliable = true;
}
state.expeditions = rows
.map(readFleetEvent)
.filter(isActiveExpedition);
state.expeditions = mergeExpeditions(state.expeditions.filter(isActiveExpedition));
state.lastReturnCandidates = state.expeditions.length;
state.lastScanAt = new Date();
state.lastMissionsReadAt = state.lastScanAt;
updateExpeditionSlots();
}
function scanCurrentShipPage() {
const component = getCurrentComponent();
if (!['empire', 'fleet', 'fleetdispatch'].includes(component)) {
return;
}
learnLocalizedShipNames(document);
updateLocationShipCacheFromCurrentSources();
if (component === 'fleet' || component === 'fleetdispatch') {
updateExpeditionSlots();
}
if (component === 'empire') {
const locationCache = readLocationShipCache();
if (locationCache?.isComplete) {
const readAt = new Date();
state.lastEmpireReadAt = readAt;
state.lastEmpireParsedAt = locationCache.updatedAt || readAt;
writeEmpireShipCache(locationCache.counts, state.lastEmpireParsedAt);
}
}
}
function getCurrentComponent() {
const params = new URLSearchParams(window.location.search);
return params.get('component') || '';
}
function getFleetEventRows(root) {
const rows = new Set([
...root.querySelectorAll('#eventContent tr, #eventboxContent tr, #eventListWrap tr, tr.eventFleet, .eventFleet'),
]);
for (const arrivalTime of root.querySelectorAll('.arrivalTime, td[class*="arrival"]')) {
const row = arrivalTime.closest('tr');
if (row) {
rows.add(row);
}
}
for (const countdown of root.querySelectorAll('.countDown, [id^="counter-eventlist-"]')) {
const row = countdown.closest('tr');
if (row) {
rows.add(row);
}
}
return [...rows];
}
function scanShipInventory() {
scanCurrentShipPage();
if (!isOverviewPage()) {
return;
}
updateLocationShipCacheFromCurrentSources();
const cachedEmpire = readEmpireShipCache();
const locationCache = readLocationShipCache();
const knownLocationCount = readKnownFleetLocationCount(document);
const sources = [
{ label: t('sourceView'), type: getCurrentComponent() || 'current', doc: document, fetchedAt: new Date() },
...(cachedEmpire ? [cachedEmpire] : []),
];
let bestInFlight = { label: '', counts: createShipCounts(), total: 0, fleets: [] };
let bestEmpireStationary = { label: '', type: '', counts: createShipCounts(), total: 0, fetchedAt: null };
const sourceAudit = [];
for (const source of sources) {
if (source.doc) {
learnLocalizedShipNames(source.doc);
}
const flightData = source.doc ? scrapeInFlightFleetData(source.doc, source.label) : { counts: createShipCounts(), fleets: [], total: 0, rowsSeen: 0 };
const flightCounts = flightData.counts;
const pageCounts = scrapeStationaryShipCounts(source);
const flightTotal = flightData.total;
const pageTotal = sumShipCounts(pageCounts);
const isEmpire = isCompleteEmpireSource(source);
sourceAudit.push({
label: source.label,
type: source.type,
fetchedAt: source.fetchedAt || null,
inFlight: flightTotal,
fleetRows: flightData.rowsSeen || 0,
stationary: pageTotal,
parsed: pageTotal > 0,
coverage: isEmpire && pageTotal > 0 ? 'empire-full' : 'partial',
});
if (flightTotal > bestInFlight.total) {
bestInFlight = { label: `${source.label}: lot`, counts: flightCounts, total: flightTotal, fleets: flightData.fleets };
}
if (isEmpire && pageTotal > bestEmpireStationary.total) {
bestEmpireStationary = {
label: `${source.label}: ${t('stationary').toLowerCase()}`,
type: source.type,
counts: pageCounts,
total: pageTotal,
fetchedAt: source.fetchedAt || null,
};
}
}
if (locationCache && shouldUseLocationShipCache(locationCache, bestEmpireStationary.total, knownLocationCount, bestEmpireStationary.fetchedAt)) {
bestEmpireStationary = {
label: t('locationCacheStationary'),
type: 'location-cache',
counts: locationCache.counts,
total: locationCache.total,
fetchedAt: locationCache.updatedAt,
};
sourceAudit.push({
label: t('locationCache'),
type: 'location-cache',
fetchedAt: locationCache.updatedAt,
inFlight: 0,
stationary: locationCache.total,
parsed: true,
coverage: 'empire-full',
locations: locationCache.locationCount,
expectedLocations: locationCache.expectedLocationCount || knownLocationCount || null,
});
} else if (locationCache) {
if (isLocationShipCacheImplausible(locationCache, bestEmpireStationary.total)) {
clearLocationShipCache();
}
sourceAudit.push({
label: t('locationCache'),
type: 'location-cache',
fetchedAt: locationCache.updatedAt,
inFlight: 0,
stationary: locationCache.total,
parsed: false,
coverage: 'ignored',
locations: locationCache.locationCount,
expectedLocations: locationCache.expectedLocationCount || knownLocationCount || null,
});
}
if (bestEmpireStationary.total > 0) {
if (bestEmpireStationary.type === 'location-cache') {
state.lastEmpireParsedAt = locationCache?.empireParsedAt || state.lastEmpireParsedAt;
} else {
state.lastEmpireParsedAt = bestEmpireStationary.fetchedAt || new Date();
}
if (bestEmpireStationary.type === 'location-cache') {
writeEmpireShipCache(bestEmpireStationary.counts, state.lastEmpireParsedAt);
}
}
const inFlight = bestInFlight.total;
const stationary = bestEmpireStationary.total;
const hasCompleteEmpire = stationary > 0;
const cachedFleetInventory = readFleetInventoryCache();
const hasReliableZeroInFlight = inFlight > 0 || state.missionScanReliable;
const hasUnparsedFlightRows = inFlight === 0 && sourceAudit.some((source) => source.type !== 'empire-cache' && source.fleetRows > 0);
if (hasCompleteEmpire && hasUnparsedFlightRows) {
state.fleetInventory = cachedFleetInventory?.complete
? {
...state.fleetInventory,
...cachedFleetInventory,
warning: t('unparsedFlightRowsKept'),
sourceAudit,
lastUpdatedAt: new Date(),
}
: {
...state.fleetInventory,
complete: false,
coverage: 'incomplete',
warning: t('unparsedFlightRows'),
sourceAudit,
lastUpdatedAt: new Date(),
};
updateExpeditionSlots();
return;
}
if (hasCompleteEmpire && inFlight === 0 && !hasReliableZeroInFlight && cachedFleetInventory?.complete) {
state.fleetInventory = {
...state.fleetInventory,
...cachedFleetInventory,
warning: cachedFleetInventory.warning || '',
largestInFlight: cachedFleetInventory.largestInFlight || null,
sourceAudit,
lastUpdatedAt: new Date(),
};
updateExpeditionSlots();
return;
}
const totalKnown = hasCompleteEmpire ? inFlight + stationary : 0;
const sourceLabels = [bestInFlight.label, bestEmpireStationary.label].filter(Boolean);
const warning = buildFleetInventoryWarning(inFlight, stationary, bestEmpireStationary.label);
const largestInFlight = getLargestInFlightFleet(bestInFlight.fleets, hasCompleteEmpire ? totalKnown : inFlight, inFlight);
if (!hasCompleteEmpire && state.fleetInventory.complete) {
state.fleetInventory = {
...state.fleetInventory,
inFlight,
inFlightCounts: bestInFlight.counts,
largestInFlight: largestInFlight || state.fleetInventory.largestInFlight,
sourceAudit,
lastUpdatedAt: new Date(),
};
updateExpeditionSlots();
return;
}
state.fleetInventory = {
inFlight,
totalKnown,
stationary,
flightPercent: hasCompleteEmpire ? (inFlight / totalKnown) * 100 : null,
complete: hasCompleteEmpire,
coverage: hasCompleteEmpire ? 'empire-full' : 'incomplete',
warning,
sourceLabels,
sourceAudit,
inFlightCounts: bestInFlight.counts,
stationaryCounts: bestEmpireStationary.counts,
largestInFlight,
lastUpdatedAt: new Date(),
};
updateExpeditionSlots();
}
function buildFleetInventoryWarning(inFlight, stationary, stationarySourceLabel) {
if (stationary > 0) {
return '';
}
if (inFlight > 0) {
return t('incompleteEmpireWarning');
}
return t('insufficientEmpireRead', { source: stationarySourceLabel || 'Empire' });
}
function updateExpeditionSlots() {
const sources = [
{ label: t('sourceView'), doc: document },
];
const countedUsed = countActiveExpeditionSlots(sources);
const parsedLimit = readBestExpeditionSlotLimit(sources);
const cachedLimit = readExpeditionSlotsCache();
if (!parsedLimit && countedUsed === 0 && state.expeditionSlots.lastUpdatedAt) {
return;
}
const used = parsedLimit?.used ?? countedUsed;
const total = parsedLimit?.total ?? cachedLimit?.total ?? state.expeditionSlots.total;
if (parsedLimit?.total) {
writeExpeditionSlotsCache(parsedLimit);
}
state.expeditionSlots = {
used,
total: total ?? null,
source: parsedLimit?.source || (cachedLimit ? 'cache' : '') || (countedUsed > 0 ? 'eventlist' : state.expeditionSlots.source || ''),
lastUpdatedAt: new Date(),
};
}
function updateMoonActivity() {
const cache = readMoonActivityCache();
const moons = readKnownMoons(document);
const now = getNow();
const pageKey = getMoonActivityPageKey();
let changed = false;
for (const moon of moons) {
const current = cache.moons[moon.id] || {};
if (!cache.moons[moon.id] && cache.lastCompletedAt) {
cache.lastCompletedAt = null;
cache.currentRunCompleted = false;
}
cache.moons[moon.id] = {
...current,
...moon,
};
changed = true;
}
if (isMoonActivityRunExpired(cache, now)) {
expireMoonActivityRun(cache);
changed = true;
}
const currentLocation = readCurrentMoonActivityLocation(document, cache);
if (currentLocation.type === 'moon' && currentLocation.id) {
const currentMoon = cache.moons[currentLocation.id] || {
id: currentLocation.id,
name: currentLocation.name || '',
coordinates: currentLocation.coordinates || '',
};
if (!cache.currentRunStartedAt || isMoonActivityRunExpired(cache, now) || shouldStartNewMoonActivityRun(cache, currentLocation.id, pageKey)) {
startMoonActivityRun(cache, now);
}
if (!cache.currentRunCompleted) {
cache.moons[currentLocation.id] = {
...currentMoon,
id: currentLocation.id,
name: currentLocation.name || currentMoon.name || '',
coordinates: currentLocation.coordinates || currentMoon.coordinates || '',
lastActivityAt: now,
lastSeenAt: now,
lastRunId: cache.currentRunId,
};
cache.currentRunVisits = cache.currentRunVisits || {};
cache.currentRunVisits[currentLocation.id] = now;
cache.lastVisitedMoonId = currentLocation.id;
cache.lastPageKey = pageKey;
cache.lastSessionId = SESSION_ID;
changed = true;
}
}
const completedAt = calculateMoonActivityCompletedAt(cache);
if (completedAt) {
cache.lastCompletedAt = completedAt;
cache.currentRunCompleted = true;
}
cache.updatedAt = now;
state.moonActivity = cache;
renderMoonActivityDots(cache);
if (changed) {
writeMoonActivityCache(cache);
}
}
function renderMoonActivityDots(cache = state.moonActivity) {
const now = getNow();
for (const link of document.querySelectorAll('#planetList .moonlink')) {
const href = link.getAttribute('href') || link.getAttribute('data-link') || '';
const id = href ? new URL(href, window.location.origin).searchParams.get('cp') || '' : '';
if (!id) {
continue;
}
const target = link;
for (const staleDot of link.querySelectorAll(':scope > .ogh-moon-activity-dot')) {
if (staleDot.parentElement !== target) {
staleDot.remove();
}
}
for (const staleDot of link.querySelectorAll('.planetBarSpaceObjectContainer > .ogh-moon-activity-dot')) {
staleDot.remove();
}
let dot = target.querySelector(':scope > .ogh-moon-activity-dot');
if (!dot) {
dot = document.createElement('span');
dot.className = 'ogh-moon-activity-dot';
target.insertAdjacentElement('afterbegin', dot);
}
positionMoonActivityDot(link, dot);
const moon = cache?.moons?.[id] || null;
const lastSeenAt = Number(moon?.lastSeenAt) || null;
dot.classList.remove('ogh-moon-activity-dot-fresh', 'ogh-moon-activity-dot-stale', 'ogh-moon-activity-dot-unknown');
if (!lastSeenAt || now - lastSeenAt > MOON_ACTIVITY_VISIBLE_MS) {
dot.classList.add('ogh-moon-activity-dot-unknown');
dot.title = '';
} else if (now - lastSeenAt <= MOON_ACTIVITY_FRESH_MS) {
dot.classList.add('ogh-moon-activity-dot-fresh');
dot.title = formatElapsedDuration(now - lastSeenAt);
} else {
dot.classList.add('ogh-moon-activity-dot-stale');
dot.title = formatElapsedDuration(now - lastSeenAt);
}
}
}
function positionMoonActivityDot(moonLink, dot) {
const image = moonLink.querySelector('img.icon-moon, img[id^="planetBarSpaceObjectImg_"]');
if (!image?.getBoundingClientRect || !moonLink.getBoundingClientRect) {
dot.style.removeProperty('--ogh-moon-activity-dot-left');
dot.style.removeProperty('--ogh-moon-activity-dot-top');
return;
}
const linkRect = moonLink.getBoundingClientRect();
const imageRect = image.getBoundingClientRect();
if (linkRect.width <= 0 || linkRect.height <= 0 || imageRect.width <= 0 || imageRect.height <= 0) {
dot.style.removeProperty('--ogh-moon-activity-dot-left');
dot.style.removeProperty('--ogh-moon-activity-dot-top');
return;
}
dot.style.setProperty('--ogh-moon-activity-dot-left', `${Math.round(imageRect.left - linkRect.left - 9)}px`);
dot.style.setProperty('--ogh-moon-activity-dot-top', `${Math.round(imageRect.top - linkRect.top + imageRect.height / 2)}px`);
}
function resetMoonActivityRun(cache) {
cache.currentRunVisits = {};
for (const moon of Object.values(cache.moons || {})) {
moon.lastActivityAt = null;
}
}
function expireMoonActivityRun(cache) {
resetMoonActivityRun(cache);
cache.currentRunStartedAt = null;
cache.currentRunCompleted = false;
cache.currentRunVisits = {};
}
function startMoonActivityRun(cache, now) {
cache.currentRunId = (Number(cache.currentRunId) || 0) + 1;
cache.currentRunStartedAt = now;
cache.currentRunCompleted = false;
resetMoonActivityRun(cache);
}
function shouldStartNewMoonActivityRun(cache, currentMoonId, pageKey) {
if (!cache.currentRunCompleted) {
return false;
}
return cache.lastVisitedMoonId !== currentMoonId
|| cache.lastPageKey !== pageKey
|| cache.lastSessionId !== SESSION_ID;
}
function isMoonActivityRunExpired(cache, now) {
return !cache.currentRunCompleted
&& Number(cache.currentRunStartedAt) > 0
&& now - Number(cache.currentRunStartedAt) > MOON_ACTIVITY_RUN_LIMIT_MS;
}
function getMoonActivityPageKey() {
const params = new URLSearchParams(window.location.search);
return [
params.get('component') || '',
params.get('cp') || '',
window.location.pathname,
].join('|');
}
function readCurrentMoonActivityLocation(root, cache = null) {
const info = readCurrentLocationInfo(root);
const params = new URLSearchParams(window.location.search);
const cp = params.get('cp') || '';
if (info.type === 'planet') {
return info;
}
const cachedMoon = cp ? cache?.moons?.[cp] : null;
if (!info.type && cp && cachedMoon) {
return {
...info,
...cachedMoon,
id: cp,
type: 'moon',
name: info.name || cachedMoon.name || '',
coordinates: info.coordinates || cachedMoon.coordinates || '',
};
}
if (info.type === 'moon' && cp) {
return { ...info, id: cp };
}
return info;
}
function readKnownMoons(root) {
const moons = [];
const seen = new Set();
for (const link of root.querySelectorAll('#planetList .moonlink')) {
const href = link.getAttribute('href') || link.getAttribute('data-link') || '';
const id = new URL(href, window.location.origin).searchParams.get('cp') || '';
if (!id || seen.has(id)) {
continue;
}
const title = stripHtml(link.getAttribute('data-tooltip-title') || '');
const nameMatch = title.match(/^([^[]+)/);
const coordinatesMatch = title.match(/\[(\d+:\d+:\d+)\]/);
seen.add(id);
moons.push({
id,
name: cleanText(nameMatch?.[1] || ''),
coordinates: coordinatesMatch?.[1] || '',
});
}
return moons;
}
function calculateMoonActivityCompletedAt(cache) {
const moons = Object.values(cache.moons || {});
const currentRunId = Number(cache.currentRunId) || 0;
const startedAt = Number(cache.currentRunStartedAt) || null;
if (!startedAt || !currentRunId || !moons.length || moons.some((moon) => Number(moon.lastRunId) !== currentRunId || !moon.lastActivityAt || Number(moon.lastActivityAt) < startedAt)) {
return null;
}
return Math.max(...moons.map((moon) => Number(moon.lastActivityAt) || 0));
}
function hydrateStateFromPanelCache() {
const cache = readPanelCache();
const fleetInventoryCache = readFleetInventoryCache();
const panelFleetInventory = normalizeFleetInventoryCache(cache?.fleetInventory);
if (panelFleetInventory) {
state.fleetInventory = {
...state.fleetInventory,
...panelFleetInventory,
warning: panelFleetInventory.warning || '',
};
} else if (fleetInventoryCache?.complete) {
state.fleetInventory = {
...state.fleetInventory,
...fleetInventoryCache,
warning: fleetInventoryCache.warning || '',
};
}
if (cache?.expeditionSlots) {
state.expeditionSlots = {
...state.expeditionSlots,
...cache.expeditionSlots,
};
}
const cachedNext = readCachedNextExpedition(cache);
if (cachedNext) {
state.expeditions = [cachedNext];
}
}
function preserveCachedNextExpedition() {
const currentNext = state.expeditions[0];
if (currentNext?.arrivalAt > getNow()) {
return true;
}
const cachedNext = readCachedNextExpedition(readPanelCache());
if (!cachedNext) {
return false;
}
state.expeditions = [cachedNext];
return true;
}
function readCachedNextExpedition(cache) {
const arrivalAt = Number(cache?.nextExpeditionArrivalAt);
if (!Number.isFinite(arrivalAt) || arrivalAt <= getNow()) {
return null;
}
return {
id: 'panel-cache-next-expedition',
isExpedition: true,
arrivalAt,
returning: true,
label: 'cache',
};
}
function readPanelCache() {
const parsed = Storage.read(PANEL_CACHE_KEY, null);
if (!parsed || typeof parsed !== 'object') {
return null;
}
return parsed;
}
function writePanelCache() {
try {
const previous = readPanelCache();
const currentNextArrivalAt = state.expeditions[0]?.arrivalAt || null;
const previousNextArrivalAt = Number(previous?.nextExpeditionArrivalAt) || null;
const nextExpeditionArrivalAt = currentNextArrivalAt
|| (previousNextArrivalAt && previousNextArrivalAt > getNow() ? previousNextArrivalAt : null);
const previousFleetInventory = normalizeFleetInventoryCache(previous?.fleetInventory);
const fleetInventory = state.fleetInventory.complete ? serializeFleetInventory(state.fleetInventory) : previousFleetInventory || readFleetInventoryCache();
const expeditionSlots = state.expeditionSlots.lastUpdatedAt
? state.expeditionSlots
: previous?.expeditionSlots || state.expeditionSlots;
if (state.fleetInventory.complete) {
writeFleetInventoryCache(state.fleetInventory);
}
Storage.write(PANEL_CACHE_KEY, {
savedAt: new Date().toISOString(),
nextExpeditionArrivalAt,
expeditionSlots,
fleetInventory,
});
} catch {
// Panel cache is only used to paint the initial state faster.
}
}
function readFleetInventoryCache() {
return normalizeFleetInventoryCache(Storage.read(FLEET_INVENTORY_CACHE_KEY, null));
}
function normalizeFleetInventoryCache(parsed) {
if (!parsed?.complete
|| parsed.cacheVersion !== FLEET_INVENTORY_CACHE_VERSION
|| parsed.coverage !== 'empire-full'
|| parsed.flightPercent === null
|| parsed.inFlight <= 0) {
return null;
}
return parsed;
}
function readLocalizedShipNamesCache() {
const parsed = Storage.read(LOCALIZED_SHIP_NAMES_CACHE_KEY, null);
if (!parsed?.names || typeof parsed.names !== 'object') {
return {};
}
return Object.fromEntries(
Object.entries(parsed.names)
.filter(([name, shipId]) => isUsefulShipName(name) && SHIP_IDS.has(String(shipId)))
.map(([name, shipId]) => [name, String(shipId)]),
);
}
function writeLocalizedShipNamesCache() {
Storage.write(LOCALIZED_SHIP_NAMES_CACHE_KEY, {
savedAt: new Date().toISOString(),
names: state.localizedShipNames,
});
}
function readMoonActivityCache() {
const parsed = Storage.read(MOON_ACTIVITY_CACHE_KEY, null);
if (!parsed || typeof parsed !== 'object') {
return createEmptyMoonActivityCache();
}
const moons = {};
for (const [id, moon] of Object.entries(parsed.moons || {})) {
if (!id || !moon || typeof moon !== 'object') {
continue;
}
moons[id] = {
id,
name: cleanText(moon.name || ''),
coordinates: cleanText(moon.coordinates || ''),
lastActivityAt: Number(moon.lastActivityAt) || null,
lastSeenAt: Number(moon.lastSeenAt) || null,
lastRunId: Number(moon.lastRunId) || 0,
};
}
return {
version: 1,
moons,
currentRunId: Number(parsed.currentRunId) || 0,
currentRunStartedAt: Number(parsed.currentRunStartedAt) || null,
currentRunVisits: normalizeMoonActivityVisits(parsed.currentRunVisits),
currentRunCompleted: Boolean(parsed.currentRunCompleted),
lastVisitedMoonId: cleanText(parsed.lastVisitedMoonId || ''),
lastPageKey: cleanText(parsed.lastPageKey || ''),
lastSessionId: cleanText(parsed.lastSessionId || ''),
lastCompletedAt: Number(parsed.lastCompletedAt) || null,
updatedAt: Number(parsed.updatedAt) || null,
};
}
function writeMoonActivityCache(cache) {
Storage.write(MOON_ACTIVITY_CACHE_KEY, {
version: 1,
moons: cache.moons || {},
currentRunId: cache.currentRunId || 0,
currentRunStartedAt: cache.currentRunStartedAt || null,
currentRunVisits: cache.currentRunVisits || {},
currentRunCompleted: Boolean(cache.currentRunCompleted),
lastVisitedMoonId: cache.lastVisitedMoonId || '',
lastPageKey: cache.lastPageKey || '',
lastSessionId: cache.lastSessionId || '',
lastCompletedAt: cache.lastCompletedAt || null,
updatedAt: cache.updatedAt || getNow(),
});
}
function createEmptyMoonActivityCache() {
return {
version: 1,
moons: {},
currentRunId: 0,
currentRunStartedAt: null,
currentRunVisits: {},
currentRunCompleted: false,
lastVisitedMoonId: '',
lastPageKey: '',
lastSessionId: '',
lastCompletedAt: null,
updatedAt: null,
};
}
function normalizeMoonActivityVisits(value) {
const visits = {};
for (const [id, visitedAt] of Object.entries(value || {})) {
const timestamp = Number(visitedAt) || null;
if (id && timestamp) {
visits[id] = timestamp;
}
}
return visits;
}
function writeFleetInventoryCache(inventory) {
Storage.write(FLEET_INVENTORY_CACHE_KEY, serializeFleetInventory(inventory));
}
function serializeFleetInventory(inventory) {
return {
cacheVersion: FLEET_INVENTORY_CACHE_VERSION,
inFlight: inventory.inFlight,
totalKnown: inventory.totalKnown,
stationary: inventory.stationary,
flightPercent: inventory.flightPercent,
complete: inventory.complete,
coverage: inventory.coverage,
warning: inventory.warning,
sourceLabels: inventory.sourceLabels,
largestInFlight: inventory.largestInFlight || null,
lastUpdatedAt: inventory.lastUpdatedAt,
savedAt: new Date().toISOString(),
};
}
function readExpeditionSlotsCache() {
const parsed = Storage.read(EXPEDITION_SLOTS_CACHE_KEY, null);
const total = Number.parseInt(parsed?.total, 10);
if (!Number.isFinite(total) || total <= 0) {
return null;
}
return {
total,
updatedAt: normalizeDate(parsed.updatedAt),
};
}
function writeExpeditionSlotsCache(slots) {
Storage.write(EXPEDITION_SLOTS_CACHE_KEY, {
total: slots.total,
updatedAt: new Date().toISOString(),
});
}
function countActiveExpeditionSlots(sources) {
const byKey = new Map();
const events = [];
const outboundEventRowIds = new Set();
for (const source of sources) {
for (const row of getFleetEventRows(source.doc)) {
const event = readFleetEvent(row);
if (!event?.isExpedition || event.arrivalAt <= getNow()) {
continue;
}
const eventRowId = readEventRowId(row, event);
if (eventRowId !== null && !event.returning) {
outboundEventRowIds.add(eventRowId);
}
events.push({ row, event, eventRowId });
}
}
for (const entry of events) {
byKey.set(getExpeditionSlotKey(entry, outboundEventRowIds), entry.event);
}
return byKey.size;
}
function getExpeditionSlotKey(entry, outboundEventRowIds) {
const { row, event, eventRowId } = entry;
const agoPair = row.getAttribute('ago-events-pair');
if (agoPair) {
return `ago:${agoPair}`;
}
const fleetId = row.getAttribute('data-fleet-id')
|| row.dataset?.fleetId
|| row.querySelector('[data-fleet-id]')?.getAttribute('data-fleet-id');
if (fleetId) {
return `fleet:${fleetId}`;
}
if (eventRowId !== null) {
if (event.returning && outboundEventRowIds.has(eventRowId - 1)) {
return `event-row-pair:${eventRowId - 1}`;
}
return event.returning ? `event-row:${eventRowId}` : `event-row-pair:${eventRowId}`;
}
return [
event.id || '',
Math.floor(event.arrivalAt / 1000),
event.origin || '',
event.destination || '',
event.returning ? 'R' : 'O',
].join('|');
}
function readEventRowId(row, event = null) {
const raw = row.id || event?.id || '';
const match = String(raw).match(/^eventRow-(\d+)$/);
return match ? Number(match[1]) : null;
}
function readBestExpeditionSlotLimit(sources) {
for (const source of sources) {
const slots = readExpeditionSlotsFromDocument(source.doc);
if (slots) {
return { ...slots, source: source.label };
}
}
return null;
}
function readExpeditionSlotsFromDocument(root) {
const slots = root.querySelector('#slots');
const text = cleanText(slots?.textContent || root.body?.textContent || '');
const match = normalizeText(text).match(/(?:ekspedycje|expeditions):\s*(\d+)\s*\/\s*(\d+)/);
if (match) {
return {
used: Number(match[1]),
total: Number(match[2]),
};
}
const usedFromScript = readScriptNumber(root, 'expeditionCount');
const totalFromScript = readScriptNumber(root, 'maxExpeditionCount');
if (totalFromScript > 0) {
return {
used: Math.max(0, usedFromScript),
total: totalFromScript,
};
}
return null;
}
function readEmpireShipCache() {
const parsed = Storage.read(EMPIRE_CACHE_KEY, null);
if (!parsed) {
state.lastCacheStatus = t('cacheEmpty');
return null;
}
const coverage = parsed.coverage || '';
const counts = normalizeShipCounts(parsed.counts);
const total = sumShipCounts(counts);
const parsedAt = normalizeDate(parsed.parsedAt);
if (parsed.cacheVersion !== EMPIRE_CACHE_VERSION || coverage !== 'empire-full' || total <= 0 || !parsedAt) {
state.lastCacheStatus = t('cacheInvalid');
return null;
}
state.lastCacheStatus = t('cacheOk', { total: formatInteger(total) });
return {
label: t('empireCacheLabel'),
type: 'empire-cache',
coverage,
counts,
fetchedAt: parsedAt,
};
}
function writeEmpireShipCache(counts, parsedAt) {
const normalizedCounts = normalizeShipCounts(counts);
const total = sumShipCounts(normalizedCounts);
if (total <= 0) {
return;
}
const saved = Storage.write(EMPIRE_CACHE_KEY, {
counts: normalizedCounts,
cacheVersion: EMPIRE_CACHE_VERSION,
coverage: 'empire-full',
parsedAt: normalizeDate(parsedAt)?.toISOString() || new Date().toISOString(),
});
if (saved) {
state.lastCacheStatus = t('cacheSaved', { total: formatInteger(total) });
} else {
state.lastCacheStatus = t('cacheWriteError');
}
}
function normalizeShipCounts(value) {
const counts = createShipCounts();
const source = value || {};
for (const key of Object.keys(counts)) {
const numeric = Number.parseInt(source[key], 10);
counts[key] = Number.isFinite(numeric) && numeric > 0 ? numeric : 0;
}
return counts;
}
function updateLocationShipCacheFromCurrentSources() {
const now = new Date();
const cache = readRawLocationShipCache();
let changed = false;
for (const source of [
{ label: t('sourceView'), type: getCurrentComponent() || 'current', doc: document, fetchedAt: now },
]) {
if (source.type === 'empire') {
const locations = scrapeEmpireLocationShipCounts(source.doc, source.fetchedAt || now);
if (locations.length > 0) {
mergeEmpireLocationCache(cache, locations, source.doc, source.fetchedAt || now);
changed = true;
}
continue;
}
if (source.type === 'fleet' || source.type === 'fleetdispatch') {
const location = scrapeFleetDispatchLocationShipCounts(source.doc, source.fetchedAt || now);
if (location) {
setLocationCacheEntry(cache, location);
changed = true;
}
}
}
if (changed) {
writeRawLocationShipCache(cache);
}
}
function readLocationShipCache() {
const cache = readRawLocationShipCache();
const counts = createShipCounts();
let updatedAt = null;
let locationCount = 0;
for (const location of Object.values(cache.locations || {})) {
const locationCounts = normalizeShipCounts(location.counts);
locationCount += 1;
if (sumShipCounts(locationCounts) <= 0) {
continue;
}
addShipCounts(counts, locationCounts);
const locationDate = normalizeDate(location.updatedAt);
if (locationDate && (!updatedAt || locationDate > updatedAt)) {
updatedAt = locationDate;
}
}
const total = sumShipCounts(counts);
if (total <= 0 || locationCount <= 0) {
return null;
}
return {
counts,
total,
locationCount,
updatedAt,
empireParsedAt: normalizeDate(cache.empireParsedAt),
expectedLocationCount: getExpectedLocationCacheCount(cache),
expectedPlanets: Number(cache.empireParts?.expectedPlanets) || 0,
expectedMoons: Number(cache.empireParts?.expectedMoons) || 0,
planetLocationsReadAt: normalizeDate(cache.empireParts?.planetsParsedAt),
moonLocationsReadAt: normalizeDate(cache.empireParts?.moonsParsedAt),
hasFullEmpire: isRawLocationCacheComplete(cache, locationCount),
isComplete: isRawLocationCacheComplete(cache, locationCount),
locations: cache.locations,
};
}
function shouldUseLocationShipCache(locationCache, referenceTotal, expectedLocationCount = 0, referenceFetchedAt = null) {
if (!locationCache || locationCache.total <= 0) {
return false;
}
if (!isCompleteLocationShipCache(locationCache, expectedLocationCount)) {
return false;
}
if (isLocationShipCacheImplausible(locationCache, referenceTotal)) {
return false;
}
const cacheDate = normalizeDate(locationCache.updatedAt) || normalizeDate(locationCache.empireParsedAt);
const referenceDate = normalizeDate(referenceFetchedAt);
if (cacheDate && referenceDate) {
return cacheDate >= referenceDate;
}
return referenceTotal <= 0 || Boolean(cacheDate);
}
function isCompleteLocationShipCache(locationCache, expectedLocationCount = 0) {
if (!locationCache) {
return false;
}
return Boolean(locationCache.isComplete);
}
function isLocationShipCacheImplausible(locationCache, referenceTotal) {
return Boolean(locationCache && referenceTotal > 0 && locationCache.total > referenceTotal * 5);
}
function readKnownFleetLocationCount(root) {
const counts = readKnownPlanetMoonCounts(root);
return counts.planets + counts.moons;
}
function clearLocationShipCache() {
Storage.remove(LOCATION_CACHE_KEY);
}
function readRawLocationShipCache() {
const parsed = Storage.read(LOCATION_CACHE_KEY, null);
if (!parsed || parsed.version !== LOCATION_CACHE_VERSION) {
return createEmptyLocationShipCache();
}
return {
version: LOCATION_CACHE_VERSION,
empireParsedAt: parsed.empireParsedAt || null,
empireParts: normalizeEmpireParts(parsed.empireParts),
locations: parsed.locations && typeof parsed.locations === 'object' ? parsed.locations : {},
};
}
function writeRawLocationShipCache(cache) {
Storage.write(LOCATION_CACHE_KEY, {
version: LOCATION_CACHE_VERSION,
empireParsedAt: cache.empireParsedAt || null,
empireParts: cache.empireParts || normalizeEmpireParts(null),
locations: cache.locations || {},
});
}
function createEmptyLocationShipCache() {
return {
version: LOCATION_CACHE_VERSION,
empireParsedAt: null,
empireParts: normalizeEmpireParts(null),
locations: {},
};
}
function normalizeEmpireParts(parts) {
return {
planetsParsedAt: parts?.planetsParsedAt || null,
moonsParsedAt: parts?.moonsParsedAt || null,
expectedPlanets: Number(parts?.expectedPlanets) || 0,
expectedMoons: Number(parts?.expectedMoons) || 0,
};
}
function mergeEmpireLocationCache(cache, locations, root, parsedAt) {
const empireInfo = readEmpireViewInfo(root);
const parts = normalizeEmpireParts(cache.empireParts);
const parsedAtIso = normalizeDate(parsedAt)?.toISOString() || new Date().toISOString();
const locationTypes = new Set(locations.map((location) => location.locationType).filter(Boolean));
if (empireInfo.expectedPlanets > 0) {
parts.expectedPlanets = Math.max(parts.expectedPlanets, empireInfo.expectedPlanets);
}
if (empireInfo.expectedMoons > 0) {
parts.expectedMoons = Math.max(parts.expectedMoons, empireInfo.expectedMoons);
}
if (empireInfo.viewKind) {
locationTypes.add(empireInfo.viewKind === 'moons' ? 'moon' : 'planet');
}
for (const type of locationTypes) {
for (const [key, location] of Object.entries(cache.locations || {})) {
if (location?.source === 'empire' && location.locationType === type) {
delete cache.locations[key];
}
}
}
cache.locations = cache.locations || {};
for (const location of locations) {
cache.locations[location.key] = location;
}
if (locationTypes.has('planet')) {
parts.planetsParsedAt = parsedAtIso;
}
if (locationTypes.has('moon')) {
parts.moonsParsedAt = parsedAtIso;
}
cache.empireParts = parts;
cache.empireParsedAt = isRawLocationCacheComplete(cache, Object.keys(cache.locations || {}).length) ? parsedAtIso : null;
}
function getExpectedLocationCacheCount(cache) {
const parts = normalizeEmpireParts(cache?.empireParts);
return parts.expectedPlanets + parts.expectedMoons;
}
function isRawLocationCacheComplete(cache, locationCount = 0) {
const parts = normalizeEmpireParts(cache?.empireParts);
const expected = getExpectedLocationCacheCount(cache);
const hasPlanets = parts.expectedPlanets <= 0 || Boolean(parts.planetsParsedAt);
const hasMoons = parts.expectedMoons <= 0 || Boolean(parts.moonsParsedAt);
return expected > 0 && locationCount >= expected && hasPlanets && hasMoons;
}
function setLocationCacheEntry(cache, location) {
const current = cache.locations[location.key];
const currentDate = normalizeDate(current?.updatedAt);
const nextDate = normalizeDate(location.updatedAt) || new Date();
if (!currentDate || nextDate >= currentDate) {
cache.locations[location.key] = location;
}
}
function scrapeEmpireLocationShipCounts(root, updatedAt) {
const payloadLocations = scrapeEmpirePayloadLocationShipCounts(root, updatedAt);
if (payloadLocations.length > 0) {
return payloadLocations;
}
const locations = [];
for (const planet of root.querySelectorAll('.planet[id^="planet"]')) {
const shipRows = planet.querySelectorAll('.values.ships.groupships');
if (!shipRows.length) {
continue;
}
const counts = createShipCounts();
for (const row of shipRows) {
addShipCounts(counts, readEmpireShipCountsFromRow(row));
}
const id = String(planet.id || '').replace(/^planet/, '');
const name = cleanText(planet.querySelector('.planetname')?.textContent);
const key = id ? `id:${id}` : `empire:${locations.length}`;
const locationType = readEmpireDomLocationType(planet);
locations.push({
key,
id,
name,
type: 'empire-location',
locationType,
source: 'empire',
updatedAt: normalizeDate(updatedAt)?.toISOString() || new Date().toISOString(),
counts,
});
}
return locations;
}
function scrapeEmpirePayloadLocationShipCounts(root, updatedAt) {
const payload = readEmpirePayload(root);
const ships = payload?.groups?.ships || [];
if (!Array.isArray(payload?.planets) || !Array.isArray(ships)) {
return [];
}
const updatedAtIso = normalizeDate(updatedAt)?.toISOString() || new Date().toISOString();
const locations = [];
for (const entry of payload.planets) {
const counts = readEmpirePayloadShipCounts(entry, ships);
const id = String(entry.id || '');
const locationType = Number(entry.type) === 3 ? 'moon' : 'planet';
locations.push({
key: id ? `id:${id}` : `empire:${locationType}:${locations.length}`,
id,
parentId: entry.planetID ? String(entry.planetID) : '',
name: cleanText(entry.name || ''),
coordinates: cleanText(String(entry.coordinates || '').replace(/^\[|\]$/g, '')),
type: 'empire-location',
locationType,
source: 'empire',
updatedAt: updatedAtIso,
counts,
});
}
return locations;
}
function readEmpirePayloadShipCounts(entry, shipIds) {
const counts = createShipCounts();
for (const shipId of shipIds) {
const id = String(shipId);
if (!SHIP_IDS.has(id)) {
continue;
}
const amount = Number.parseInt(entry?.[id], 10);
if (Number.isFinite(amount) && amount > 0) {
counts[id] += amount;
}
}
return counts;
}
function readEmpireDomLocationType(planet) {
const text = normalizeText(`${planet.className || ''} ${planet.textContent || ''}`);
if (text.includes('ksiezyc') || text.includes('moon')) {
return 'moon';
}
return 'planet';
}
function readEmpireViewInfo(root) {
const payload = readEmpirePayload(root);
const counts = readKnownPlanetMoonCounts(root);
const viewKind = readEmpireViewKind(root, payload);
const expectedFromPayload = Array.isArray(payload?.planets) ? payload.planets.length : 0;
const expectedPlanetsFromMoonParents = countEmpirePayloadMoonParents(payload);
const expectedMoonsFromScript = readScriptNumber(root, 'moonCount');
return {
viewKind,
expectedPlanets: Math.max(
counts.planets,
expectedPlanetsFromMoonParents,
viewKind === 'planets' ? expectedFromPayload : 0,
),
expectedMoons: Math.max(
counts.moons,
expectedMoonsFromScript,
viewKind === 'moons' ? expectedFromPayload : 0,
),
};
}
function countEmpirePayloadMoonParents(payload) {
if (!Array.isArray(payload?.planets)) {
return 0;
}
const parentIds = new Set();
for (const entry of payload.planets) {
if (Number(entry?.type) !== 3 || !entry?.planetID) {
continue;
}
parentIds.add(String(entry.planetID));
}
return parentIds.size;
}
function readEmpireViewKind(root, payload = null) {
const firstType = Number(payload?.planets?.[0]?.type);
if (firstType === 3) {
return 'moons';
}
if (firstType === 1) {
return 'planets';
}
const planetType = readScriptNumber(root, 'planetType');
if (planetType === 1) {
return 'moons';
}
if (planetType === 0) {
return 'planets';
}
return '';
}
function readKnownPlanetMoonCounts(root) {
const planets = new Set();
const moons = new Set();
for (const link of root.querySelectorAll('#planetList .planetlink[href*="cp="], #planetList .planetlink[data-link*="cp="]')) {
const cp = readCpFromLink(link);
if (cp) {
planets.add(cp);
}
}
for (const link of root.querySelectorAll('#planetList .moonlink[href*="cp="], #planetList .moonlink[data-link*="cp="]')) {
const cp = readCpFromLink(link);
if (cp) {
moons.add(cp);
}
}
return {
planets: planets.size,
moons: moons.size,
};
}
function readCpFromLink(link) {
const href = link.getAttribute('href') || link.getAttribute('data-link') || '';
return href.match(/[?&]cp=(\d+)/)?.[1] || '';
}
function readScriptNumber(root, variableName) {
const scriptsText = [...root.querySelectorAll('script')].map((script) => script.textContent || '').join('\n');
const match = scriptsText.match(new RegExp(`var\\s+${variableName}\\s*=\\s*(\\d+)\\s*;`));
return match ? Number(match[1]) : 0;
}
function readEmpirePayload(root) {
const scripts = [...root.querySelectorAll('script')];
for (const script of scripts) {
const text = script.textContent || '';
const markerIndex = text.indexOf('createImperiumHtml(');
if (markerIndex < 0) {
continue;
}
const objectStart = text.indexOf('{', markerIndex);
const jsonText = extractBalancedObject(text, objectStart);
if (!jsonText) {
continue;
}
try {
return JSON.parse(jsonText);
} catch {
return null;
}
}
return null;
}
function extractBalancedObject(text, start) {
if (start < 0 || text[start] !== '{') {
return '';
}
let depth = 0;
let quote = '';
let escaped = false;
for (let index = start; index < text.length; index += 1) {
const char = text[index];
if (quote) {
if (escaped) {
escaped = false;
} else if (char === '\\') {
escaped = true;
} else if (char === quote) {
quote = '';
}
continue;
}
if (char === '"' || char === "'") {
quote = char;
continue;
}
if (char === '{') {
depth += 1;
} else if (char === '}') {
depth -= 1;
if (depth === 0) {
return text.slice(start, index + 1);
}
}
}
return '';
}
function scrapeFleetDispatchLocationShipCounts(root, updatedAt) {
const info = readCurrentLocationInfo(root);
const counts = readFleetDispatchCurrentShipCounts(root);
if (!info.key) {
return null;
}
return {
...info,
source: 'fleet',
updatedAt: normalizeDate(updatedAt)?.toISOString() || new Date().toISOString(),
counts,
};
}
function readCurrentLocationInfo(root) {
const id = root.querySelector('meta[name="ogame-planet-id"]')?.content || '';
const name = root.querySelector('meta[name="ogame-planet-name"]')?.content || '';
const coordinates = root.querySelector('meta[name="ogame-planet-coordinates"]')?.content || '';
const type = root.querySelector('meta[name="ogame-planet-type"]')?.content || '';
const key = id ? `id:${id}` : (coordinates && type ? `${coordinates}:${type}` : '');
return { key, id, name, coordinates, type };
}
function readFleetDispatchCurrentShipCounts(root) {
const fromShipsOnPlanet = readShipCountsFromScriptArray(root, 'shipsOnPlanet', 'id', 'number');
if (sumShipCounts(fromShipsOnPlanet) > 0) {
return fromShipsOnPlanet;
}
const fromApiBase = readShipCountsFromApiShipBaseData(root);
if (sumShipCounts(fromApiBase) > 0) {
return fromApiBase;
}
return createShipCounts();
}
function readShipCountsFromScriptArray(root, variableName, idKey, amountKey) {
const counts = createShipCounts();
const scriptsText = [...root.querySelectorAll('script')].map((script) => script.textContent || '').join('\n');
const match = scriptsText.match(new RegExp(`var\\s+${variableName}\\s*=\\s*(\\[[\\s\\S]*?\\]);`));
if (!match) {
return counts;
}
try {
const rows = JSON.parse(match[1]);
for (const row of rows) {
const id = String(row[idKey] || '');
if (SHIP_IDS.has(id)) {
counts[id] += Number(row[amountKey]) || 0;
}
}
} catch {
return createShipCounts();
}
return counts;
}
function readShipCountsFromApiShipBaseData(root) {
const counts = createShipCounts();
const scriptsText = [...root.querySelectorAll('script')].map((script) => script.textContent || '').join('\n');
const match = scriptsText.match(/var\s+apiShipBaseData\s*=\s*(\[[\s\S]*?\]);/);
if (!match) {
return counts;
}
try {
const rows = JSON.parse(match[1]);
for (const row of rows) {
const id = String(row[0] || '');
if (SHIP_IDS.has(id)) {
counts[id] += Number(row[1]) || 0;
}
}
} catch {
return createShipCounts();
}
return counts;
}
function scrapeInFlightShipCounts(root) {
return scrapeInFlightFleetData(root).counts;
}
function scrapeInFlightFleetData(root, sourceLabel = '') {
const rowsSeen = getFleetEventRows(root).length + root.querySelectorAll('.ago_eventlist_fleet').length;
const agoFleets = scrapeAgoEventlistFleets(root, sourceLabel);
const detailsFleets = scrapeDetailsFleetRows(root, sourceLabel);
if (agoFleets.length > 0) {
const agoData = buildFlightFleetData(agoFleets, rowsSeen);
const detailsData = buildFlightFleetData(detailsFleets, rowsSeen);
return detailsData.total > agoData.total ? detailsData : agoData;
}
const fleets = [];
const rows = getFleetEventRows(root);
for (const row of rows) {
const counts = readShipCountsFromElement(row);
const total = sumShipCounts(counts);
if (total <= 0) {
continue;
}
fleets.push({
key: row.id || row.getAttribute('data-fleet-id') || cleanText(row.textContent).slice(0, 80),
label: buildFleetEntryLabel(row, sourceLabel),
...readGenericFleetEvent(row),
counts,
total,
});
}
const rowData = buildFlightFleetData(fleets, rowsSeen);
const detailsData = buildFlightFleetData(detailsFleets, rowsSeen);
return detailsData.total > rowData.total ? detailsData : rowData;
}
function scrapeDetailsFleetRows(root, sourceLabel = '') {
const fleets = [];
const seen = new Set();
for (const row of getFleetEventRows(root)) {
const amount = readTotalShipAmount(row);
if (amount <= 0) {
continue;
}
const key = row.getAttribute('ago-events-pair')
|| row.getAttribute('data-fleet-id')
|| row.id
|| cleanText(row.textContent).slice(0, 80);
if (seen.has(key)) {
continue;
}
seen.add(key);
const counts = createShipCounts();
counts.unknown = amount;
fleets.push({
key,
label: buildFleetEntryLabel(row, sourceLabel),
...readGenericFleetEvent(row),
counts,
total: amount,
});
}
return fleets;
}
function buildFlightFleetData(fleets, rowsSeen = 0) {
const counts = createShipCounts();
for (const fleet of fleets) {
addShipCounts(counts, fleet.counts);
}
return {
counts,
fleets,
total: sumShipCounts(counts),
rowsSeen,
};
}
function scrapeAgoEventlistShipCounts(root) {
return buildFlightFleetData(scrapeAgoEventlistFleets(root)).counts;
}
function scrapeAgoEventlistFleets(root, sourceLabel = '') {
const fleets = [];
const seenPairs = new Set();
let fallbackIndex = 0;
for (const fleetDetails of root.querySelectorAll('.ago_eventlist_fleet')) {
const row = fleetDetails.closest('tr');
const eventRow = findAgoEventRow(fleetDetails);
const pair = row?.getAttribute('ago-events-pair') || eventRow?.getAttribute('ago-events-pair') || '';
const key = pair || `single-${fallbackIndex++}`;
if (seenPairs.has(key)) {
continue;
}
const detailCounts = readShipCountsFromAgoFleetDetails(fleetDetails);
if (sumShipCounts(detailCounts) <= 0) {
continue;
}
seenPairs.add(key);
fleets.push({
key,
label: buildFleetEntryLabel(eventRow || row || fleetDetails, sourceLabel),
...readGenericFleetEvent(eventRow || row || fleetDetails),
counts: detailCounts,
total: sumShipCounts(detailCounts),
});
}
return fleets;
}
function findAgoEventRow(fleetDetails) {
const row = fleetDetails.closest('tr');
const pair = row?.getAttribute('ago-events-pair') || row?.previousElementSibling?.getAttribute('ago-events-pair') || '';
if (!pair) {
return findNearbyEventFleetRow(row) || row;
}
const table = row?.closest('table') || fleetDetails.ownerDocument;
for (const candidate of table.querySelectorAll(`tr[ago-events-pair="${cssEscape(pair)}"]`)) {
if (candidate.querySelector('.arrivalTime, [id^="counter-eventlist-"], .countDown, .missionFleet, [data-mission-type]')) {
return candidate;
}
}
return findNearbyEventFleetRow(row) || row;
}
function findNearbyEventFleetRow(row) {
for (let current = row?.previousElementSibling; current; current = current.previousElementSibling) {
if (isEventFleetRow(current)) {
return current;
}
if (!current.classList?.contains('ago_eventlist')) {
break;
}
}
for (let current = row?.nextElementSibling; current; current = current.nextElementSibling) {
if (isEventFleetRow(current)) {
return current;
}
if (!current.classList?.contains('ago_eventlist')) {
break;
}
}
return null;
}
function isEventFleetRow(row) {
return Boolean(row?.matches?.('tr.eventFleet, .eventFleet'))
|| Boolean(row?.querySelector?.('.arrivalTime, [id^="counter-eventlist-"], .countDown, .missionFleet, [data-mission-type]'));
}
function readGenericFleetEvent(row) {
if (!row) {
return {};
}
const searchableText = getSearchableText(row);
const mission = readMissionTitle(row) || readMissionCellText(row);
const missionType = row.getAttribute('data-mission-type')
|| row.dataset?.missionType
|| row.querySelector('[data-mission-type]')?.getAttribute('data-mission-type')
|| '';
const normalizedText = normalizeText(`${searchableText} ${mission}`);
const coordinates = readAllCoordinates(searchableText);
return {
arrivalAt: readAgoArrivalTimestamp(row) || readArrivalTimestamp(row) || null,
mission,
missionType,
returning: isReturnFlight(row, normalizedText),
origin: readCoordinates(row, ['coordsOrigin', 'origin', 'originFleet', 'start', 'from']) || coordinates[0] || null,
destination: readCoordinates(row, ['destCoords', 'destination', 'destFleet', 'target', 'dest', 'to']) || coordinates[1] || null,
};
}
function buildFleetEntryLabel(row, sourceLabel = '') {
const mission = readMissionTitle(row) || readMissionCellText(row);
const coordinates = readAllCoordinates(getSearchableText(row));
const route = coordinates.length ? coordinates.join(' -> ') : '';
return [sourceLabel, cleanText(mission), route].filter(Boolean).join(' | ') || cleanText(row?.textContent).slice(0, 80);
}
function getLargestInFlightFleet(fleets, totalKnown, inFlightTotal) {
if (!Array.isArray(fleets) || fleets.length <= 0 || !Number.isFinite(totalKnown) || totalKnown <= 0) {
return null;
}
const largest = fleets.reduce((best, fleet) => (fleet.total > (best?.total || 0) ? fleet : best), null);
if (!largest || largest.total <= 0) {
return null;
}
return {
key: largest.key || '',
label: largest.label || '',
total: largest.total,
percentOfTotal: (largest.total / totalKnown) * 100,
percentOfInFlight: inFlightTotal > 0 ? (largest.total / inFlightTotal) * 100 : null,
arrivalAt: largest.arrivalAt || null,
mission: largest.mission || '',
missionType: largest.missionType || '',
returning: Boolean(largest.returning),
origin: largest.origin || null,
destination: largest.destination || null,
counts: largest.counts,
};
}
function readShipCountsFromAgoFleetDetails(fleetDetails) {
const counts = createShipCounts();
const rows = [...fleetDetails.querySelectorAll('tr')];
const aggregateTotal = readAgoFleetAggregateTotal(rows[0]);
for (const [rowIndex, row] of rows.entries()) {
if (rowIndex === 0) {
continue;
}
for (const [cellIndex, cell] of [...row.querySelectorAll('td')].entries()) {
const labelElement = cell.querySelector('.ago_eventlist_label');
if (!labelElement) {
continue;
}
const shipId = findShipIdByName(labelElement.textContent) || inferAgoShipIdByPosition(rowIndex - 1, cellIndex);
if (!shipId) {
continue;
}
const valueText = cleanText(cell.textContent).replace(cleanText(labelElement.textContent), '');
const amount = parseLocalizedInteger(valueText);
if (amount !== null) {
counts[shipId] += amount;
}
}
}
const parsedTotal = sumShipCounts(counts);
if (aggregateTotal > parsedTotal) {
counts.unknown += aggregateTotal - parsedTotal;
}
return counts;
}
function readAgoFleetAggregateTotal(row) {
if (!row) {
return 0;
}
let total = 0;
for (const cell of [...row.querySelectorAll('td')].slice(0, 2)) {
const labelElement = cell.querySelector('.ago_eventlist_label');
if (!labelElement) {
continue;
}
const valueText = cleanText(cell.textContent).replace(cleanText(labelElement.textContent), '');
total += parseLocalizedInteger(valueText) || 0;
}
return total;
}
function inferAgoShipIdByPosition(rowIndex, cellIndex) {
if (cellIndex === 0) {
return AGO_CIVIL_SHIP_BY_ROW[rowIndex] || null;
}
if (cellIndex === 1) {
return AGO_COMBAT_SHIP_BY_ROW[rowIndex] || null;
}
return null;
}
function scrapeStationaryShipCounts(source) {
if (source.type === 'empire-cache') {
return source.counts || createShipCounts();
}
if (source.type === 'empire') {
const empireCounts = scrapeEmpireShipCounts(source.doc);
if (sumShipCounts(empireCounts) > 0) {
return empireCounts;
}
}
return scrapePageShipCounts(source.doc);
}
function isCompleteEmpireSource(source) {
return source.type === 'empire-cache' || source.type === 'location-cache';
}
function scrapeEmpireShipCounts(root) {
const counts = createShipCounts();
const payload = readEmpirePayload(root);
if (Array.isArray(payload?.planets) && Array.isArray(payload?.groups?.ships)) {
for (const entry of payload.planets) {
addShipCounts(counts, readEmpirePayloadShipCounts(entry, payload.groups.ships));
}
if (sumShipCounts(counts) > 0) {
return counts;
}
}
const empireRows = root.querySelectorAll('.values.ships.groupships');
for (const row of empireRows) {
addShipCounts(counts, readEmpireShipCountsFromRow(row));
}
if (sumShipCounts(counts) > 0) {
return counts;
}
const rows = root.querySelectorAll('tr, li, .technology, .ship, .building, [class*="ship"], [class*="technology"]');
for (const row of rows) {
if (hasNestedShipCountCandidate(row)) {
continue;
}
const shipId = readShipId(row);
if (shipId) {
const amount = sumNumericCells(row);
if (amount > 0) {
counts[shipId] += amount;
continue;
}
}
addShipCounts(counts, readShipCountsFromText(getSearchableText(row)));
}
return counts;
}
function readEmpireShipCountsFromRow(row) {
const counts = createShipCounts();
for (const cell of row.children) {
const shipId = readShipId(cell);
if (!shipId) {
continue;
}
const amount = parseLocalizedInteger(cleanText(cell.textContent));
if (amount !== null) {
counts[shipId] += amount;
}
}
return counts;
}
function hasNestedShipCountCandidate(element) {
if (element.matches?.('tr')) {
return false;
}
for (const child of element.children) {
if (child.matches?.('tr, li, .technology, .ship, .building, [class*="ship"], [class*="technology"]')) {
return true;
}
}
return false;
}
function scrapePageShipCounts(root) {
const structuredCounts = createShipCounts();
for (const element of root.querySelectorAll('[data-technology], [data-tech-id], [data-ship-id], [rel], [ref], [class*="technology"], [class*="ship"]')) {
const shipId = readShipId(element);
if (!shipId) {
continue;
}
const amount = readShipAmountNearElement(element);
if (amount > 0) {
structuredCounts[shipId] += amount;
}
}
if (sumShipCounts(structuredCounts) > 0) {
return structuredCounts;
}
const counts = createShipCounts();
for (const row of root.querySelectorAll('tr, li, .technology, .ship, .building')) {
const rowCounts = readShipCountsFromText(getSearchableText(row));
addShipCounts(counts, rowCounts);
}
return counts;
}
function readShipCountsFromElement(element) {
const tooltipCounts = readShipCountsFromFleetInfoTooltip(element);
if (sumShipCounts(tooltipCounts) > 0) {
return tooltipCounts;
}
const counts = readShipCountsFromText(getSearchableText(element));
if (sumShipCounts(counts) > 0) {
return counts;
}
const exactAmount = readTotalShipAmount(element);
if (exactAmount > 0) {
counts.unknown += exactAmount;
}
return counts;
}
function readShipCountsFromFleetInfoTooltip(element) {
const counts = createShipCounts();
const tooltipHtml = getSearchableHtml(element);
if (!tooltipHtml || !tooltipHtml.includes('fleetinfo')) {
return counts;
}
const doc = new DOMParser().parseFromString(tooltipHtml, 'text/html');
let readingShips = false;
for (const row of doc.querySelectorAll('.fleetinfo tr')) {
if (row.querySelector('th')) {
if (readingShips) {
break;
}
readingShips = true;
continue;
}
if (!readingShips) {
continue;
}
const label = cleanText(row.querySelector('td')?.textContent).replace(/:$/, '');
const amount = parseLocalizedInteger(row.querySelector('.value')?.textContent);
if (!label && amount === null) {
break;
}
if (!label || amount === null) {
continue;
}
const shipId = findShipIdByName(label);
if (shipId) {
counts[shipId] += amount;
} else if (!isResourceName(label)) {
counts.unknown += amount;
}
}
return counts;
}
function isResourceName(value) {
const text = normalizeText(value);
return /\b(metal|crystal|cristal|krysztal|deuterium|deuterio|deuter|deut|food|alimento|comida|population|poblacion|energia|energy|dark matter|materia oscura|antymateria|resources|recursos|surowce)\b/.test(text);
}
function getSearchableHtml(element) {
const parts = [];
const attributes = ['title', 'data-title', 'data-tooltip-title'];
for (const node of [element, ...element.querySelectorAll('*')]) {
for (const attribute of attributes) {
const value = node.getAttribute?.(attribute);
if (value) {
parts.push(value);
}
}
}
return parts.join('\n');
}
function readTotalShipAmount(element) {
const value = readNumericAttribute(element, [
'data-fleet-amount',
'data-fleet-size',
'data-ship-count',
'data-ships',
]);
if (value !== null) {
return value;
}
const fleetAmountElement = element.querySelector(
'.detailsFleet span, .detailsFleet, .fleetAmount, .fleet_amount, .shipAmount, .ships, [class*="fleetAmount"], [class*="shipAmount"]',
);
return parseLocalizedInteger(cleanText(fleetAmountElement?.textContent)) || 0;
}
function readShipCountsFromText(text) {
const counts = createShipCounts();
const normalized = normalizeText(text);
for (const ship of SHIP_NAME_PATTERNS) {
if (!ship.pattern.test(normalized)) {
continue;
}
const amount = readAmountForShipName(normalized, ship.pattern);
if (amount > 0) {
counts[ship.id] += amount;
}
}
return counts;
}
function readAmountForShipName(text, pattern) {
const match = text.match(pattern);
if (!match) {
return 0;
}
const afterName = text.slice(match.index + match[0].length);
const beforeName = text.slice(0, match.index);
const nextShipIndex = findNextShipNameIndex(afterName);
const valueSegment = nextShipIndex === -1 ? afterName : afterName.slice(0, nextShipIndex);
const valuesAfterName = readIntegerValues(valueSegment);
if (valuesAfterName.length > 0) {
return valuesAfterName.reduce((sum, value) => sum + value, 0);
}
const valuesBeforeName = readIntegerValues(beforeName);
return valuesBeforeName.at(-1) || 0;
}
function findNextShipNameIndex(text) {
const indexes = SHIP_NAME_PATTERNS
.map((ship) => text.search(ship.pattern))
.filter((index) => index >= 0);
return indexes.length ? Math.min(...indexes) : -1;
}
function readIntegerValues(text) {
return [...String(text || '').matchAll(/\d[\d.,]*/g)]
.map((match) => parseLocalizedInteger(match[0]))
.filter((value) => value !== null);
}
function readShipId(element) {
const attributeShipId = readShipIdFromAttributes(element);
if (attributeShipId) {
return attributeShipId;
}
const searchableText = normalizeText(getSearchableText(element));
return findShipIdByName(searchableText);
}
function readShipIdFromAttributes(element) {
const values = [
element.getAttribute('data-technology'),
element.getAttribute('data-technology-id'),
element.getAttribute('data-tech-id'),
element.getAttribute('data-ship-id'),
element.getAttribute('name'),
element.getAttribute('rel'),
element.getAttribute('ref'),
element.id,
element.className,
];
for (const value of values) {
const match = String(value || '').match(/\b(20[2-9]|21[01345]|218|219)\b/);
if (match && SHIP_IDS.has(match[1])) {
return match[1];
}
}
return null;
}
function findShipIdByName(value) {
const searchableText = normalizeText(value);
const localizedShipId = findLocalizedShipIdByName(searchableText);
if (localizedShipId) {
return localizedShipId;
}
for (const ship of SHIP_NAME_PATTERNS) {
if (ship.pattern.test(searchableText)) {
return ship.id;
}
}
return null;
}
function learnLocalizedShipNames(root) {
let changed = false;
const candidates = new Set([
...root.querySelectorAll('#technologies .technology[data-technology]'),
...root.querySelectorAll('[data-technology], [data-technology-id], [data-tech-id], [data-ship-id], [name^="ships["], [rel], [ref]'),
]);
for (const element of candidates) {
const shipId = readShipIdFromAttributes(element);
if (!shipId) {
continue;
}
const names = [
element.getAttribute('aria-label'),
element.getAttribute('title'),
element.getAttribute('data-title'),
element.getAttribute('data-tooltip-title'),
element.querySelector('[aria-label]')?.getAttribute('aria-label'),
element.querySelector('[title]')?.getAttribute('title'),
element.querySelector('[data-title]')?.getAttribute('data-title'),
element.querySelector('[data-tooltip-title]')?.getAttribute('data-tooltip-title'),
];
for (const name of names) {
for (const normalized of normalizeShipNameCandidates(name)) {
if (state.localizedShipNames[normalized] === shipId) {
continue;
}
state.localizedShipNames[normalized] = shipId;
changed = true;
}
}
}
if (changed) {
writeLocalizedShipNamesCache();
}
}
function normalizeShipNameCandidates(value) {
const normalized = normalizeText(stripHtml(value));
const withoutAmount = normalized.replace(/\s*\(\s*[\d.,]+\s*\)\s*$/, '');
return [...new Set([normalized, withoutAmount])]
.filter(isUsefulShipName);
}
function findLocalizedShipIdByName(normalizedText) {
const entries = Object.entries(state.localizedShipNames);
entries.sort((a, b) => b[0].length - a[0].length);
for (const [name, shipId] of entries) {
if (name && normalizedText.includes(name)) {
return shipId;
}
}
return null;
}
function isUsefulShipName(value) {
return value
&& value.length >= 3
&& !/^\d+$/.test(value)
&& !value.includes('data-')
&& !value.includes('<')
&& !value.includes('>');
}
function sumNumericCells(row) {
const cells = row.querySelectorAll('td, .cell, .value, .amount, .level, .stockAmount, [class*="amount"], [class*="level"], [data-value], [data-amount], [data-count]');
let total = 0;
for (const cell of cells) {
if (cell.querySelector('td, .cell, .value, .amount, .level, .stockAmount, [class*="amount"], [class*="level"], [data-value], [data-amount], [data-count]')) {
continue;
}
const attributeValue = readNumericAttribute(cell, ['data-value', 'data-amount', 'data-count', 'data-level']);
if (attributeValue !== null) {
total += attributeValue;
continue;
}
const cellText = cleanText(cell.textContent);
if (looksLikePlainInteger(cellText)) {
total += parseLocalizedInteger(cellText) || 0;
}
}
return total;
}
function looksLikePlainInteger(value) {
return /^[\d\s.,]+$/.test(String(value || '').trim());
}
function readShipAmountNearElement(element) {
const directValue = readNumericAttribute(element, ['data-value', 'data-amount', 'data-count', 'data-level']);
if (directValue !== null) {
return directValue;
}
const valueElement = element.querySelector('.amount, .level, .count, .stockAmount, .available, [class*="amount"], [class*="level"]');
const elementValue = parseLocalizedInteger(cleanText(valueElement?.textContent));
if (elementValue !== null) {
return elementValue;
}
const titleValue = parseLocalizedInteger(element.getAttribute('title') || element.getAttribute('data-tooltip-title'));
if (titleValue !== null) {
return titleValue;
}
return 0;
}
function readNumericAttribute(element, names) {
const candidates = [element, ...element.querySelectorAll(names.map((name) => `[${name}]`).join(','))];
for (const candidate of candidates) {
for (const name of names) {
const value = parseLocalizedInteger(candidate.getAttribute(name));
if (value !== null) {
return value;
}
}
}
return null;
}
function createShipCounts() {
const counts = { unknown: 0 };
for (const shipId of SHIP_IDS) {
counts[shipId] = 0;
}
return counts;
}
function addShipCounts(target, source) {
for (const key of Object.keys(source)) {
target[key] = (target[key] || 0) + source[key];
}
}
function sumShipCounts(counts) {
return Object.values(counts).reduce((sum, value) => sum + value, 0);
}
function isActiveExpedition(event) {
return event && event.isExpedition && event.returning && event.arrivalAt > getNow();
}
function mergeExpeditions(events) {
const byKey = new Map();
for (const event of events) {
const key = [
event.id,
Math.floor(event.arrivalAt / 1000),
event.origin || '',
event.destination || '',
].join('|');
byKey.set(key, event);
}
return [...byKey.values()].sort((a, b) => a.arrivalAt - b.arrivalAt);
}
function readFleetEvent(row) {
const agoReturnEvent = readAgoReturnExpedition(row);
if (agoReturnEvent) {
return agoReturnEvent;
}
const text = cleanText(row.textContent);
const searchableText = getSearchableText(row);
const missionTitle = readMissionTitle(row);
const missionType = row.getAttribute('data-mission-type')
|| row.dataset?.missionType
|| row.querySelector('[data-mission-type]')?.getAttribute('data-mission-type');
const normalizedText = normalizeText(`${searchableText} ${missionTitle}`);
const normalizedMission = normalizeText(missionTitle || readMissionCellText(row));
const coordinates = readAllCoordinates(searchableText);
const isExpedition = isExpeditionMissionType(missionType)
|| isExpeditionText(normalizedMission);
if (!isExpedition) {
return null;
}
const arrivalAt = readArrivalTimestamp(row);
if (!arrivalAt) {
return null;
}
const origin = readCoordinates(row, ['coordsOrigin', 'origin', 'start', 'from']) || coordinates[0] || null;
const destination = readCoordinates(row, ['destCoords', 'destination', 'target', 'dest', 'to']) || coordinates[1] || null;
const returning = isReturnFlight(row, normalizedText);
return {
id: row.id || row.getAttribute('data-fleet-id') || text.slice(0, 80),
isExpedition,
arrivalAt,
origin,
destination,
returning,
label: text,
};
}
function readAgoReturnExpedition(row) {
const missionType = row.getAttribute('data-mission-type') || row.dataset?.missionType || '';
const normalizedMission = normalizeText(readMissionTitle(row) || readMissionCellText(row));
const isReturn = row.getAttribute('data-return-flight') === 'true'
|| row.getAttribute('data-return-flight') === '1'
|| row.classList.contains('ago_events_reverse');
const isExpedition = isExpeditionMissionType(missionType)
|| isExpeditionText(normalizedMission);
if (!isReturn || !isExpedition) {
return null;
}
const arrivalAt = readAgoArrivalTimestamp(row) || readArrivalTimestamp(row);
if (!arrivalAt) {
return null;
}
const origin = readCoordinates(row, ['coordsOrigin', 'originFleet']) || null;
const destination = readCoordinates(row, ['destCoords', 'destFleet']) || null;
return {
id: row.id || `ago-return-${arrivalAt}`,
isExpedition: true,
arrivalAt,
origin,
destination,
returning: true,
label: cleanText(row.textContent),
};
}
function readAgoArrivalTimestamp(row) {
const rawTimestamp = row.getAttribute('data-arrival-time');
const timestamp = normalizeTimestamp(rawTimestamp);
if (timestamp) {
return timestamp;
}
const counter = row.querySelector('[id^="counter-eventlist-"], .countDown');
const seconds = parseDurationToSeconds(cleanText(counter?.textContent));
return seconds === null ? null : getNow() + seconds * 1000;
}
function readArrivalTimestamp(row) {
const arrivalClockTimestamp = readArrivalClockTimestamp(row);
if (arrivalClockTimestamp) {
return arrivalClockTimestamp;
}
const timestamp = readTimestampAttribute(row, [
'data-arrival-time',
'data-return-time',
'data-time',
'data-end-time',
]);
if (timestamp) {
return timestamp;
}
const timerElement = row.querySelector('[data-arrival-time], [data-return-time], [data-time], [data-end-time], .countDown, .countdown, .timer');
const seconds = parseDurationToSeconds(cleanText(timerElement?.textContent) || cleanText(row.textContent));
return seconds === null ? null : getNow() + seconds * 1000;
}
function readArrivalClockTimestamp(row) {
const arrivalTime = row.querySelector('.arrivalTime, td[class*="arrival"]');
const value = arrivalTime?.getAttribute('original') || cleanText(arrivalTime?.textContent);
const match = String(value || '').match(/^(\d{1,2}):(\d{2}):(\d{2})$/);
if (!match) {
return null;
}
const serverDate = new Date(getNow());
serverDate.setHours(Number(match[1]), Number(match[2]), Number(match[3]), 0);
if (serverDate.getTime() < getNow() - 1000) {
serverDate.setDate(serverDate.getDate() + 1);
}
return serverDate.getTime();
}
function readTimestampAttribute(row, names) {
const candidates = [row, ...row.querySelectorAll(names.map((name) => `[${name}]`).join(','))];
for (const element of candidates) {
for (const name of names) {
const value = element.getAttribute(name);
const timestamp = normalizeTimestamp(value);
if (timestamp) {
return timestamp;
}
}
}
return null;
}
function normalizeTimestamp(value) {
if (!value) {
return null;
}
const numeric = Number.parseInt(String(value), 10);
if (!Number.isFinite(numeric) || numeric <= 0) {
return null;
}
return numeric > 100000000000 ? numeric : numeric * 1000;
}
function parseDurationToSeconds(value) {
const text = cleanText(value);
if (!text) {
return null;
}
const clock = text.match(/(\d{1,2}):(\d{2}):(\d{2})/);
if (clock) {
return Number(clock[1]) * 3600 + Number(clock[2]) * 60 + Number(clock[3]);
}
const units = {
t: 7 * 24 * 3600,
d: 24 * 3600,
g: 3600,
h: 3600,
min: 60,
sek: 1,
s: 1,
};
let total = 0;
const regex = /(\d+)\s*(t|d|g|h|min\.?|sek\.?|s)(?=\s|$|[.,;:])/gi;
let match;
while ((match = regex.exec(text)) !== null) {
const unit = match[2].replace('.', '').toLowerCase();
total += Number(match[1]) * (units[unit] || 0);
}
return total > 0 ? total : null;
}
function readCoordinates(row, hints) {
const selector = hints.map((hint) => `.${hint}, [class*="${hint}"], [data-${hint}]`).join(',');
const element = row.querySelector(selector);
return readCoordinatesFromText(cleanText(element?.textContent), 0);
}
function readCoordinatesFromText(text, index) {
return readAllCoordinates(text)[index] || null;
}
function readAllCoordinates(text) {
return [...String(text || '').matchAll(/\[(\d+:\d+:\d+)\]/g)].map((match) => match[1]);
}
function isReturnFlight(row, normalizedText) {
const value = row.getAttribute('data-return-flight') || row.dataset?.returnFlight;
if (value === 'true' || value === '1') {
return true;
}
if (value === 'false' || value === '0') {
return false;
}
return normalizedText.includes('powrot')
|| normalizedText.includes('ekspedycja (r)')
|| normalizedText.includes('wraca')
|| normalizedText.includes('return')
|| normalizedText.includes('retorno')
|| normalizedText.includes('regreso')
|| normalizedText.includes('regresa')
|| normalizedText.includes('vuelve')
|| normalizedText.includes('retour')
|| normalizedText.includes('ruckkehr')
|| row.classList.contains('return')
|| row.classList.contains('returning')
|| Boolean(row.querySelector('.return, .returning, .mission_return, .icon_movement_return'));
}
function isExpeditionMissionType(missionType) {
return EXPEDITION_MISSION_TYPES.has(String(missionType || ''));
}
function isExpeditionText(normalizedText) {
return normalizedText.includes('ekspedycj')
|| normalizedText.includes('expedition')
|| normalizedText.includes('expedicion')
|| normalizedText.includes('expedicao');
}
function refreshPanel() {
ensurePanel();
const panel = document.getElementById(APP_ID);
if (!panel) {
return;
}
const next = state.expeditions[0] || null;
const timer = panel.querySelector('[data-role="next-expedition-time"]');
const fleetPercent = panel.querySelector('[data-role="fleet-flight-percent"]');
let fleetDataQuality = panel.querySelector('[data-role="fleet-data-quality"]');
const largestFleetPercent = panel.querySelector('[data-role="largest-fleet-percent"]');
const expeditionSlots = panel.querySelector('[data-role="expedition-slots"]');
const moonActivityTimer = panel.querySelector('[data-role="moon-activity-timer"]');
if (!timer || !fleetPercent || !expeditionSlots || !largestFleetPercent || !moonActivityTimer) {
return;
}
fleetDataQuality = fleetDataQuality || ensureFleetDataQualityElement(fleetPercent);
if (!next) {
timer.textContent = '-';
} else {
timer.textContent = formatCountdown(next.arrivalAt - getNow());
}
setValueTone(timer, getNextExpeditionTone(next));
const displayFleetInventory = getDisplayFleetInventory();
fleetPercent.textContent = formatFleetFlightPercent(displayFleetInventory);
fleetPercent.removeAttribute('title');
setValueTone(fleetPercent, getFleetPercentTone(displayFleetInventory));
setDataQualityIndicator(fleetDataQuality, getFleetDataQuality(displayFleetInventory));
largestFleetPercent.textContent = formatLargestInFlightTime(displayFleetInventory);
setPanelTooltipIfChanged(largestFleetPercent, buildFleetSaveTimerTitle(displayFleetInventory));
setValueTone(largestFleetPercent, getLargestInFlightTone(displayFleetInventory));
expeditionSlots.textContent = formatExpeditionSlots(state.expeditionSlots);
removePanelTooltip(expeditionSlots);
setValueTone(expeditionSlots, getExpeditionSlotsTone(state.expeditionSlots));
moonActivityTimer.textContent = formatMoonActivityTimer(state.moonActivity);
setPanelTooltipIfChanged(moonActivityTimer, buildMoonActivityTitleV2(state.moonActivity));
setValueTone(moonActivityTimer, getMoonActivityTone(state.moonActivity));
writePanelCache();
}
function setPanelTooltipIfChanged(element, title) {
if (element.dataset.oghTooltip !== title) {
element.dataset.oghTooltip = title;
}
if (element.hasAttribute('title')) {
element.removeAttribute('title');
}
}
function removePanelTooltip(element) {
delete element.dataset.oghTooltip;
if (element.hasAttribute('title')) {
element.removeAttribute('title');
}
}
function ensureFleetDataQualityElement(fleetPercent) {
const indicator = document.createElement('span');
indicator.className = 'ogh-data-quality';
indicator.dataset.role = 'fleet-data-quality';
indicator.textContent = '●';
fleetPercent.insertAdjacentElement('afterend', indicator);
return indicator;
}
function setValueTone(element, tone) {
element.classList.remove('ogh-value-good', 'ogh-value-caution', 'ogh-value-warn', 'ogh-value-bad');
if (tone) {
element.classList.add(`ogh-value-${tone}`);
}
}
function setDataQualityIndicator(element, quality) {
element.classList.remove('ogh-quality-good', 'ogh-quality-warn', 'ogh-quality-bad', 'ogh-quality-unknown');
element.classList.add(`ogh-quality-${quality.level}`);
setPanelTooltipIfChanged(element, quality.title);
}
function getFleetPercentTone(inventory) {
if (!inventory || inventory.coverage !== 'empire-full' || inventory.flightPercent === null) {
return '';
}
if (inventory.flightPercent >= 90) {
return 'good';
}
return 'warn';
}
function getFleetDataQuality(inventory) {
if (!inventory || inventory.coverage !== 'empire-full' || inventory.flightPercent === null) {
return {
level: 'unknown',
title: t('dataQualityMissing'),
};
}
if (inventory.warning) {
return {
level: 'bad',
title: t('dataQualityNeedsAttention'),
};
}
const lastUpdatedAt = normalizeDate(inventory.lastUpdatedAt);
const stale = !lastUpdatedAt || Date.now() - lastUpdatedAt.getTime() > 15 * 60 * 1000;
if (stale) {
return {
level: 'warn',
title: t('dataQualityStale'),
};
}
return {
level: 'good',
title: t('dataQualityGood'),
};
}
function getLargestInFlightTone(inventory) {
return getTimerTone(inventory?.largestInFlight?.arrivalAt, TIMER_TONES.fleetSave);
}
function getNextExpeditionTone(event) {
return getTimerTone(event?.arrivalAt, TIMER_TONES.nextExpedition);
}
function getTimerTone(arrivalAt, thresholds) {
const timestamp = Number(arrivalAt);
if (!Number.isFinite(timestamp) || timestamp <= 0) {
return '';
}
const remainingMs = timestamp - getNow();
for (const threshold of thresholds) {
if (remainingMs <= threshold.maxMs) {
return threshold.tone;
}
}
return '';
}
function getExpeditionSlotsTone(slots) {
if (!slots || !Number.isFinite(slots.used) || !Number.isFinite(slots.total) || slots.total <= 0) {
return '';
}
if (slots.used >= slots.total) {
return 'good';
}
if (slots.used > 0) {
return 'warn';
}
return 'bad';
}
function getMoonActivityTone(activity) {
if (!activity?.lastCompletedAt) {
return '';
}
const elapsed = getNow() - Number(activity.lastCompletedAt);
if (elapsed <= 15 * 60 * 1000) {
return 'good';
}
if (elapsed <= 60 * 60 * 1000) {
return 'caution';
}
if (elapsed <= 3 * 60 * 60 * 1000) {
return 'warn';
}
return 'bad';
}
function getDisplayFleetInventory() {
if (state.fleetInventory.complete) {
return state.fleetInventory;
}
return readFleetInventoryCache() || state.fleetInventory;
}
function formatFleetFlightPercent(inventory) {
if (!inventory || inventory.coverage !== 'empire-full' || inventory.flightPercent === null) {
return '-';
}
return `${inventory.flightPercent.toFixed(0)}%`;
}
function formatMoonActivityTimer(activity) {
if (!activity?.lastCompletedAt) {
return '-';
}
return formatElapsedDuration(getNow() - Number(activity.lastCompletedAt));
}
function buildMoonActivityTitle(activity) {
const moons = Object.values(activity?.moons || {});
const startedAt = Number(activity?.currentRunStartedAt) || null;
const currentRunId = Number(activity?.currentRunId) || 0;
const hasCurrentRun = currentRunId > 0 && startedAt;
const activeMoons = hasCurrentRun
? moons.filter((moon) => Number(moon.lastRunId) === currentRunId && Number(moon.lastActivityAt) >= startedAt)
: [];
const lines = [
`${t('activeMoons')}: ${activeMoons.length}/${moons.length}`,
`${t('runStart')}: ${formatDateTimeOrDash(startedAt)}`,
`${t('fullRun')}: ${formatDateTimeOrDash(activity?.lastCompletedAt)}`,
];
const oldest = activeMoons
.sort((a, b) => Number(a.lastActivityAt) - Number(b.lastActivityAt))[0];
if (oldest) {
lines.push(`${t('oldestActivity')}: ${formatMoonLabel(oldest)} - ${formatElapsedDuration(getNow() - Number(oldest.lastActivityAt))}`);
}
return lines.join('\n');
}
function formatMoonLabel(moon) {
return [moon.name, moon.coordinates ? `[${moon.coordinates}]` : ''].filter(Boolean).join(' ') || moon.id || '-';
}
function buildMoonActivityTitleV2(activity) {
const moons = Object.values(activity?.moons || {});
const startedAt = Number(activity?.currentRunStartedAt) || null;
const currentRunId = Number(activity?.currentRunId) || 0;
const hasCurrentRun = currentRunId > 0 && startedAt;
const activeMoons = hasCurrentRun
? moons.filter((moon) => Number(moon.lastRunId) === currentRunId && Number(moon.lastActivityAt) >= startedAt)
: [];
const activeMoonIds = new Set(activeMoons.map((moon) => moon.id));
const missingMoons = moons.filter((moon) => !activeMoonIds.has(moon.id));
const lines = [
`${t('activeMoons')}: ${activeMoons.length}/${moons.length}`,
];
if (missingMoons.length) {
lines.push('');
lines.push(t('missingMoons'));
for (const moon of missingMoons.sort(compareMoonCoordinates)) {
lines.push(`- ${formatMoonLabel(moon)}`);
}
}
return lines.join('\n');
}
function compareMoonCoordinates(a, b) {
const first = parseCoordinates(a?.coordinates);
const second = parseCoordinates(b?.coordinates);
if (first && second) {
return first.galaxy - second.galaxy
|| first.system - second.system
|| first.position - second.position;
}
if (first) {
return -1;
}
if (second) {
return 1;
}
return formatMoonLabel(a).localeCompare(formatMoonLabel(b), undefined, { numeric: true, sensitivity: 'base' });
}
function parseCoordinates(value) {
const match = String(value || '').match(/(\d+):(\d+):(\d+)/);
if (!match) {
return null;
}
return {
galaxy: Number(match[1]),
system: Number(match[2]),
position: Number(match[3]),
};
}
function formatLargestInFlightPercent(inventory) {
const percent = inventory?.largestInFlight?.percentOfTotal;
if (!inventory || inventory.coverage !== 'empire-full' || !Number.isFinite(percent)) {
return '-';
}
const precision = percent > 0 && percent < 10 ? 1 : 0;
return `${percent.toFixed(precision)}%`;
}
function formatLargestInFlightTime(inventory) {
const largest = inventory?.largestInFlight;
if (!largest?.arrivalAt) {
return '-';
}
return formatCountdown(largest.arrivalAt - getNow());
}
function buildLargestInFlightTitle(inventory) {
const largest = inventory?.largestInFlight;
if (!largest) {
return t('noFleetRowsData');
}
return [
`${t('largestFleet')}: ${formatInteger(largest.total)}`,
`${t('time')}: ${largest.arrivalAt ? formatCountdown(largest.arrivalAt - getNow()) : '-'}`,
`${t('arrival')}: ${formatDateTimeOrDash(largest.arrivalAt)}`,
`${t('direction')}: ${largest.returning ? t('returnDirection') : t('outboundDirection')}`,
`${t('mission')}: ${largest.mission || largest.missionType || '--'}`,
`${t('route')}: ${[largest.origin, largest.destination].filter(Boolean).join(' -> ') || '--'}`,
`${t('shareTotal')}: ${formatLargestInFlightPercent(inventory)}`,
`${t('shareInFlight')}: ${Number.isFinite(largest.percentOfInFlight) ? `${largest.percentOfInFlight.toFixed(0)}%` : '--'}`,
`${t('description')}: ${largest.label || '--'}`,
].join('\n');
}
function buildFleetSaveTimerTitle(inventory) {
const largest = inventory?.largestInFlight;
if (!largest) {
return t('noFleetRowsData');
}
const eventLabel = largest.returning ? t('fleetSaveReturn') : t('fleetSaveLanding');
const location = (largest.returning ? largest.origin : largest.destination)
|| largest.destination
|| largest.origin
|| '--';
return [
`${eventLabel}: ${formatFleetSaveDateTimeOrDash(largest.arrivalAt)}`,
`${t('location')}: ${location}`,
].join('\n');
}
function buildFleetInventoryTitle(inventory) {
if (!inventory) {
return t('noFullShipData');
}
const sources = inventory.sourceLabels.length ? inventory.sourceLabels.join(', ') : t('none');
const lines = [
`${t('inFlight')}: ${formatInteger(inventory.inFlight)}`,
`${t('coverage')}: ${inventory.coverage}`,
`${t('totalKnown')}: ${formatInteger(inventory.totalKnown)}`,
`${t('sources')}: ${sources}`,
];
if (inventory.complete) {
lines.splice(2, 0, `${t('stationary')}: ${formatInteger(inventory.stationary)}`);
if (inventory.largestInFlight) {
lines.splice(3, 0, `${t('largestFleet')}: ${formatInteger(inventory.largestInFlight.total)} (${formatLargestInFlightTime(inventory)}, ${formatLargestInFlightPercent(inventory)})`);
}
} else if (inventory.warning) {
lines.splice(2, 0, inventory.warning);
}
if (inventory.sourceAudit?.length) {
lines.push(`${t('audit')}:`);
for (const source of inventory.sourceAudit) {
lines.push(`${source.label}: ${t('flight')} ${formatInteger(source.inFlight)}, ${t('rows')} ${formatInteger(source.fleetRows || 0)}, ${t('ships')} ${formatInteger(source.stationary)}, ${source.coverage}`);
}
}
return lines.join('\n');
}
function formatExpeditionSlots(slots) {
if (!slots) {
return '-';
}
const used = Number.isFinite(slots.used) ? slots.used : 0;
const total = Number.isFinite(slots.total) ? slots.total : '-';
return `${used}/${total}`;
}
function buildExpeditionSlotsTitle(slots) {
return [
`${t('occupiedExpeditions')}: ${formatExpeditionSlots(slots)}`,
`${t('source')}: ${slots?.source || '--'}`,
`${t('lastUpdate')}: ${formatDateTimeOrDash(slots?.lastUpdatedAt)}`,
`${t('empireRead')}: ${formatDateTimeOrDash(state.lastEmpireReadAt)}`,
`${t('empireParsed')}: ${formatDateTimeOrDash(state.lastEmpireParsedAt)}`,
`${t('missionsInFlight')}: ${formatDateTimeOrDash(state.lastMissionsReadAt)}`,
`${t('empireCache')}: ${state.lastCacheStatus || '--'}`,
`${t('empireFleetStatus')}: ${state.lastShipFetchStatus || '--'}`,
`${t('eventlistStatus')}: ${state.lastFetchStatus || '--'}`,
].join('\n');
}
function renderExpeditionRow(event) {
const route = [event.origin, event.destination].filter(Boolean).join(' -> ');
const routeText = route ? `<span>${escapeHtml(route)}</span>` : `<span>${escapeHtml(t('unknownRoute'))}</span>`;
return `
<div class="ogh-expedition">
<strong>${escapeHtml(formatCountdown(event.arrivalAt - getNow()))}</strong>
${routeText}
</div>
`;
}
function buildEventDetails(event) {
const route = [event.origin, event.destination].filter(Boolean).join(' -> ');
const prefix = event.returning ? t('returnPrefix') : 'Event';
return route ? `${prefix}: ${route}` : prefix;
}
function formatCountdown(ms) {
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const clock = [hours, minutes, seconds].map((value) => String(value).padStart(2, '0')).join(':');
return days > 0 ? `${days}d ${clock}` : clock;
}
function formatElapsedDuration(ms) {
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const clock = [hours, minutes, seconds].map((value) => String(value).padStart(2, '0')).join(':');
return days > 0 ? `${days}d ${clock}` : clock;
}
function formatInteger(value) {
return String(Math.max(0, Math.round(value))).replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
}
function formatClockOrDash(value) {
const date = normalizeDate(value);
if (!date) {
return '-';
}
return [
date.getHours(),
date.getMinutes(),
date.getSeconds(),
].map((part) => String(part).padStart(2, '0')).join(':');
}
function formatDateTimeOrDash(value) {
const date = normalizeDate(value);
if (!date) {
return '-';
}
return `${date.toLocaleDateString()} ${formatClockOrDash(date)}`;
}
function formatFleetSaveDateTimeOrDash(value) {
const date = normalizeDate(value);
if (!date) {
return '-';
}
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
return `${day}.${month}.${date.getFullYear()} ${formatClockOrDash(date)}`;
}
function normalizeDate(value) {
if (!value) {
return null;
}
const date = value instanceof Date ? value : new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
}
function readServerTimeOffset() {
const timestamp = document.querySelector('meta[name="ogame-timestamp"]')?.content;
const serverSeconds = Number.parseInt(timestamp, 10);
if (!Number.isFinite(serverSeconds) || serverSeconds <= 0) {
return 0;
}
return serverSeconds * 1000 - Date.now();
}
function getNow() {
return Date.now() + state.serverTimeOffsetMs;
}
function cleanText(value) {
return value ? String(value).replace(/\s+/g, ' ').trim() : '';
}
function stripHtml(value) {
return cleanText(String(value || '').replace(/<[^>]*>/g, ' '));
}
function getSearchableText(element) {
const parts = [element.textContent || ''];
const attributes = ['title', 'alt', 'aria-label', 'data-title', 'data-tooltip-title', 'data-mission-name'];
for (const node of [element, ...element.querySelectorAll('*')]) {
for (const attribute of attributes) {
const value = node.getAttribute?.(attribute);
if (value) {
parts.push(value);
}
}
if (node.className && typeof node.className === 'string') {
parts.push(node.className);
}
}
return cleanText(parts.join(' '));
}
function readMissionTitle(row) {
const mission = row.querySelector('.missionFleet [data-tooltip-title], .missionFleet [title], .missionFleet img');
return mission?.getAttribute('data-tooltip-title')
|| mission?.getAttribute('title')
|| mission?.getAttribute('alt')
|| '';
}
function readMissionCellText(row) {
const mission = row.querySelector('.missionFleet');
return mission ? getSearchableText(mission) : '';
}
function normalizeText(value) {
return cleanText(value)
.toLowerCase()
.replace(/ł/g, 'l')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '');
}
function parseLocalizedInteger(value) {
const digits = String(value || '').replace(/[^\d]/g, '');
if (!digits) {
return null;
}
const parsed = Number.parseInt(digits, 10);
return Number.isFinite(parsed) ? parsed : null;
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function cssEscape(value) {
if (window.CSS?.escape) {
return window.CSS.escape(String(value));
}
return String(value).replace(/["\\]/g, '\\$&');
}
GM_addStyle(`
#${APP_ID} {
box-sizing: border-box;
clear: both;
display: grid !important;
grid-template-columns: repeat(3, 220px) !important;
gap: 3px;
justify-content: start;
width: 670px !important;
min-width: 670px !important;
max-width: none !important;
margin: var(--ogh-overview-offset, 0px) auto 8px;
padding: 0;
color: lightgrey;
font-family: Verdana, Arial, Helvetica, sans-serif;
line-height: 1;
text-shadow: none;
}
#overviewcomponent:has(+ #${APP_ID}) {
margin-bottom: 7px !important;
}
#${APP_ID} .ogh-status-column {
box-sizing: border-box;
min-width: 0;
width: 220px;
margin: 0;
float: none;
}
#${APP_ID} .ogh-status-component {
width: 220px;
}
#${APP_ID} .content-box-s {
width: 220px !important;
margin: 0 0 5px 0 !important;
float: none !important;
overflow: hidden;
}
#${APP_ID} .content {
overflow: hidden;
}
#${APP_ID} .ogh-status-table {
display: table;
table-layout: fixed;
width: 100% !important;
min-width: 0 !important;
max-width: 100% !important;
}
#${APP_ID} .ogh-status-table tbody,
#${APP_ID} .ogh-status-table tr {
width: 100% !important;
max-width: 100% !important;
}
#${APP_ID} .ogh-status-table td.idle {
box-sizing: border-box;
width: 100% !important;
min-width: 0 !important;
max-width: 100% !important;
}
#${APP_ID} .ogh-panel-main {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 4px;
min-height: 0;
padding: 0 8px;
width: 100%;
max-width: 100%;
font: 700 11px/1.35 Verdana, Arial, Helvetica, sans-serif;
white-space: nowrap;
}
#${APP_ID} .ogh-metrics-table {
border-collapse: collapse !important;
border-spacing: 0 !important;
margin: 0 auto !important;
table-layout: fixed !important;
width: 160px !important;
min-width: 160px !important;
max-width: 160px !important;
}
#${APP_ID} .ogh-metrics-table td {
background: none !important;
border: 0 !important;
box-sizing: border-box;
height: auto !important;
line-height: 15px !important;
padding-bottom: 0 !important;
padding-top: 0 !important;
color: #ffffff;
font-weight: 700;
}
#${APP_ID} .ogh-label-cell {
min-width: 96px !important;
padding-left: 0 !important;
padding-right: 5px !important;
text-align: right !important;
white-space: nowrap !important;
width: 96px !important;
}
#${APP_ID} .ogh-value-cell {
padding-left: 0 !important;
padding-right: 0 !important;
text-align: left !important;
white-space: nowrap !important;
width: 64px !important;
}
#${APP_ID} .ogh-label {
color: #ffffff;
font-weight: 400;
}
#${APP_ID} .ogh-time {
color: #ffffff;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
#${APP_ID} .ogh-timer-blue {
color: ${STATUS_COLORS.timerBlue};
}
#${APP_ID} .ogh-data-quality {
display: inline-block;
font-size: 9px;
line-height: 1;
margin-left: 3px;
vertical-align: 1px;
}
#${APP_ID} .ogh-value-good {
color: ${STATUS_COLORS.good};
}
#${APP_ID} .ogh-value-warn {
color: ${STATUS_COLORS.warn};
}
#${APP_ID} .ogh-value-caution {
color: ${STATUS_COLORS.caution};
}
#${APP_ID} .ogh-value-bad {
color: ${STATUS_COLORS.bad};
}
#${APP_ID} .ogh-quality-good {
color: ${STATUS_COLORS.good};
}
#${APP_ID} .ogh-quality-warn {
color: ${STATUS_COLORS.warn};
}
#${APP_ID} .ogh-quality-bad {
color: ${STATUS_COLORS.bad};
}
#${APP_ID} .ogh-quality-unknown {
color: ${STATUS_COLORS.unknown};
}
.ogh-tooltip {
position: fixed;
display: none;
z-index: 100000;
box-sizing: border-box;
max-width: 360px;
padding: 7px 9px;
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 12px;
background: rgba(0, 0, 0, 0.60);
color: #ffffff;
font: 11px/1.45 Verdana, Arial, Helvetica, sans-serif;
white-space: pre-line;
pointer-events: none;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
text-shadow: none;
}
#planetList .moonlink {
position: relative;
}
#planetList .moonlink > .ogh-moon-activity-dot {
position: absolute;
left: var(--ogh-moon-activity-dot-left, -9px);
top: var(--ogh-moon-activity-dot-top, 50%);
z-index: 3;
box-sizing: border-box;
width: 6px;
height: 6px;
border: 1px solid rgba(0, 0, 0, 0.75);
border-radius: 50%;
pointer-events: none;
transform: translateY(-50%);
}
#planetList .moonlink > .ogh-moon-activity-dot-fresh {
background: ${STATUS_COLORS.good};
box-shadow: 0 0 3px rgba(134, 201, 119, 0.8);
}
#planetList .moonlink > .ogh-moon-activity-dot-stale {
background: ${STATUS_COLORS.unknown};
opacity: 0.45;
}
#planetList .moonlink > .ogh-moon-activity-dot-unknown {
background: ${STATUS_COLORS.unknown};
opacity: 0;
}
@media (max-width: 700px) {
#${APP_ID} {
margin-left: 0;
margin-right: 0;
}
#${APP_ID} .ogh-panel-main {
white-space: normal;
}
}
`);
try {
init();
} catch (error) {
console.error('[OGame Status Panel] failed', error);
}
})();